Skip to content

Plain awaits in CompileUseCase/RunTestsUseCase can hang if Play Mode pauses mid-command #1686

Description

@hatayama

Summary

CompileUseCase and RunTestsUseCase still have plain (non-ConfigureAwait(false)) awaits that violate the invariant documented on UnityCliLoopTool.ExecuteAsync (added in #1685): every await in a tool's execution path must avoid capturing Unity's SynchronizationContext, because a continuation posted to it is never drained while Play Mode is paused.

This was flagged by CodeRabbit during review of #1685 and confirmed as a real (but out of scope for that PR) latent gap.

Affected locations

  • Packages/src/Editor/FirstPartyTools/Compile/CompileUseCase.cs:106await WaitForPlayModeExitAsync(ct)
  • Packages/src/Editor/FirstPartyTools/Compile/CompileUseCase.cs:154await _executeCompilationAsync(request, ct)
  • Packages/src/Editor/FirstPartyTools/Compile/CompileUseCase.cs:170await TimerDelay.Wait(POLL_INTERVAL_MS, ct) (inside WaitForPlayModeExitAsync's poll loop)
  • Packages/src/Editor/FirstPartyTools/RunTests/RunTestsUseCase.cs:95await _executionService.ExecutePlayModeTestAsync(filter, ct)
  • Packages/src/Editor/FirstPartyTools/RunTests/RunTestsUseCase.cs:99await _executionService.ExecuteEditModeTestAsync(filter, ct)
  • Packages/src/Editor/FirstPartyTools/RunTests/RunTestsUseCase.cs:102await _waitForTestRunnerCleanupAsync(ct)
  • Packages/src/Editor/FirstPartyTools/RunTests/RunTestsUseCase.cs:154await TimerDelay.Wait(TestRunnerCleanupFallbackDelayMilliseconds, ct)

(ExecuteDynamicCodeUseCase was also audited during #1685's review and is not affected — every await there already uses ConfigureAwait(false).)

Mechanism

Same second-layer bug class fixed for simulate-mouse-ui/simulate-keyboard/simulate-mouse-input in #1685: if one of these awaits is still suspended (not yet complete) when Play Mode pauses, and it later completes off the captured UnitySynchronizationContext, the resumed continuation is posted back to that context. That context's pump does not run while EditorApplication.isPaused is true, so the tool call hangs forever instead of returning.

run-tests --test-mode PlayMode is the most plausible trigger, since it genuinely executes inside Play Mode where a pause point could fire while a test is running. compile's exposure depends on whether StopPlayMode() itself resolves the pause as a side effect before the poll loop's await would otherwise hang — this needs to be measured, not assumed.

Why a mechanical fix is not safe here

Unlike the simulate-mouse-ui/simulate-keyboard wait loops (which pair every ConfigureAwait(false) with an explicit MainThreadSwitcher/InputSystemUpdateHelper.SwitchToMainThreadIfNeeded call before touching Unity APIs again), these awaits currently rely on the implicit context-capture to resume on the main thread. Code immediately after them reads main-thread-only Unity APIs (e.g. EditorApplication.isPlaying at CompileUseCase.cs:167,174).

Blindly adding ConfigureAwait(false) here would remove that implicit resume-on-main-thread guarantee, trading a rare pause-triggered hang for a frequent threadpool-touches-Unity-API bug. Do not "just add ConfigureAwait(false)" to these awaits.

Candidate fixes

  • Port the MouseUiEditorFrameWaiter pattern: ConfigureAwait(false) paired with an explicit main-thread switch-back before the next Unity API read.
  • Or, for run-tests specifically, reject execution via a preflight check if Play Mode is paused when the tool starts (may be the more appropriate design for test execution — this is Unity's highest-risk area per the Unity Freeze Prevention guidance in CLAUDE.md, so any behavioral change here needs to be validated very carefully, ideally starting as a non-EditMode/pure-unit-test-covered refactor before any Play Mode E2E repro is attempted).

Verification requirements before closing

  • A real, non-mocked repro proving the current hang exists (or does not, if compile's StopPlayMode() genuinely self-resolves the pause first) — do not assume the mechanism transfers 1:1 from the mouse-ui case without measuring it.
  • After the fix, a real repro showing the tool call now returns promptly instead of hanging when a pause point fires while the awaited operation is in flight.
  • Full run-tests suite green, with special attention to the Unity Freeze Prevention guardrails in CLAUDE.md (no infinite waits, no blocking the main thread, no risky EditMode test patterns).

Context

Found during E2E verification for #1685 (source-based pause points, uloop-wait-for-pause-point skill).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions