Skip to content

fix: prevent Process.HasExited race causing UnobservedTaskException#362

Closed
PureWeen wants to merge 1 commit intomainfrom
fix/session-fiesta-orchestrator-note-previou-20260312-1228
Closed

fix: prevent Process.HasExited race causing UnobservedTaskException#362
PureWeen wants to merge 1 commit intomainfrom
fix/session-fiesta-orchestrator-note-previou-20260312-1228

Conversation

@PureWeen
Copy link
Owner

Problem

The crash log shows \InvalidOperationException: No process is associated with this object\ from \Process.HasExited\ accessed on a disposed process. This surfaced as an \UnobservedTaskException\ in the SDK's \CopilotClient.StartCliServerAsync\ process monitor, and the same unsafe pattern existed in PolyPilot's own code.

Root Cause

Fire-and-forget tasks in \DevTunnelService.TryHostTunnelAsync()\ accessed the _hostProcess\ field directly in their loop conditions (\while (!_hostProcess.HasExited)). When \Stop()\ or a subsequent \TryHostTunnelAsync()\ call disposed/nulled the field concurrently, the background tasks threw \InvalidOperationException.

The same pattern existed in:

  • \CodespaceService.TunnelHandle.IsAlive\ — accessed _process.HasExited\ after \DisposeAsync()\
  • \ServerManager.StopServer()\ — called \Kill()\ without safe disposal

Fix

  • New \ProcessHelper\ utility with \SafeHasExited(), \SafeKill(), \SafeKillAndDispose()\ — never throws on disposed/invalid processes
  • DevTunnelService: capture process in local variable before passing to fire-and-forget tasks (follows the safe pattern already used in \ServerManager.StartServerAsync)
  • CodespaceService.TunnelHandle: add _disposed\ flag; \IsAlive\ returns false after disposal
  • ServerManager.StopServer: use \SafeKillAndDispose\ for clean teardown

Tests

11 new unit tests in \ProcessHelperTests.cs:

  • \SafeHasExited\ with null, disposed, exited, and running processes
  • \SafeKill\ with null, disposed, and running processes
  • \SafeKillAndDispose\ idempotency
  • Concurrent-dispose regression test simulating the exact race condition

The crash log shows InvalidOperationException ('No process is associated
with this object') when Process.HasExited is accessed on a disposed process.
Fire-and-forget tasks monitoring stdout/stderr in DevTunnelService accessed
the _hostProcess field directly, which could be nulled/disposed by Stop()
or TryHostTunnelAsync() concurrently. The same pattern existed in
CodespaceService.TunnelHandle and ServerManager.StopServer.

Changes:
- Add ProcessHelper utility with SafeHasExited(), SafeKill(), and
  SafeKillAndDispose() that never throw on disposed/invalid processes
- DevTunnelService: capture process in local variable before passing to
  fire-and-forget tasks; use ProcessHelper.SafeHasExited() in loops
- CodespaceService.TunnelHandle: add _disposed flag so IsAlive returns
  false after DisposeAsync(); use SafeHasExited for process checks
- ServerManager.StopServer: use SafeKillAndDispose for clean teardown
- Add 11 unit tests covering null, disposed, exited, and running process
  states plus a concurrent-dispose regression test

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PureWeen
Copy link
Owner Author

Closing — pushing to PR #322 branch instead

@PureWeen PureWeen closed this Mar 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant