Fix Process.WaitForExit hang when grandchild process keeps redirected pipe open#125732
Fix Process.WaitForExit hang when grandchild process keeps redirected pipe open#125732
Conversation
Add CancelDueToProcessExit and CancelDueToProcessExitAsync methods to AsyncStreamReader that wait a timeout for the read task to finish and cancel it if it doesn't. Replace all EOF.GetAwaiter().GetResult() and EOF.WaitAsync() calls in WaitForExitCore (Unix/Windows) and WaitForExitAsync with the new timeout-based methods. Use 300ms default when no timeout is provided. Add Theory test reproducing the hang scenario with a grandchild process that sleeps for 8 hours. Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
… grandchild process dispatch Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
adamsitnik
left a comment
There was a problem hiding this comment.
@copilot please address my feedback
src/libraries/System.Diagnostics.Process/tests/ProcessWaitingTests.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.cs
Outdated
Show resolved
Hide resolved
|
Tagging subscribers to this area: @dotnet/area-system-diagnostics-process |
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
|
@copilot please invoke the code-review skill and post your analysis/results as a comment on this PR |
🤖 Copilot Code Review — PR #125790Holistic AssessmentMotivation: The problem is real and well-documented — Approach: The fix introduces a timeout-based cancellation mechanism in Summary: Detailed Findings
|
adamsitnik
left a comment
There was a problem hiding this comment.
@copilot address my feedback
| if (exited && milliseconds == Timeout.Infinite) // if we have a hard timeout, we cannot wait for the streams | ||
| if (exited) | ||
| { | ||
| _output?.EOF.GetAwaiter().GetResult(); |
There was a problem hiding this comment.
remove the EOF property, it's no longer used
| /// <summary> | ||
| /// Default timeout in milliseconds to wait for redirected streams to complete after the process exits. | ||
| /// </summary> | ||
| private const int StreamDrainDefaultTimeoutMs = 300; |
There was a problem hiding this comment.
let's move it to src/libraries/System.Diagnostics.Process/src/System/Diagnostics/AsyncStreamReader.cs, and make both CancelDueToProcessExit and CancelDueToProcessExitAsync parameterless and just always use the default timeout internally.
There was a problem hiding this comment.
Done in dae1fe0. Moved StreamDrainDefaultTimeoutMs to AsyncStreamReader.cs and made both CancelDueToProcessExit and CancelDueToProcessExitAsync parameterless — all callers now use the default timeout internally.
…thods parameterless, remove EOF Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Description
Process.WaitForExit()hangs indefinitely when stdout/stderr is redirected and a grandchild process inherits the pipe handle. The root cause: after the child process exits,AsyncStreamReaderblocks forever onEOF.GetAwaiter().GetResult()because the grandchild still holds the write end of the pipe open.Changes
AsyncStreamReader.cs— Added parameterlessCancelDueToProcessExit()andCancelDueToProcessExitAsync()methods that use a privateStreamDrainDefaultTimeoutMsconstant (300ms) internally. These wait up to the default timeout for_readToBufferTaskto complete, then cancel via_cts.Cancel()if it hasn't. Removed the now-unusedEOFproperty.Process.Unix.cs/Process.Windows.cs— Replaced_output?.EOF.GetAwaiter().GetResult()withCancelDueToProcessExit(). Now drains streams for all exited processes (not justTimeout.Infinite).Process.cs— UpdatedWaitForExitAsyncto useCancelDueToProcessExitAsync()instead ofEOF.WaitAsync(cancellationToken).ProcessWaitingTests.cs— Added[Theory]covering all fourWaitForExitoverloads: spawns a child with redirected output that usesCreateProcessto create a sleeping grandchild, asserts WaitForExit completes in <2s.💬 Send tasks to Copilot coding agent from Slack and Teams to turn conversations into code. Copilot posts an update in your thread when it's finished.