You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ExecuteAsync hangs forever (and leaves the child blocked) when the output handler throws or a strict encoding hits a decode error #89
When a read faults, AsyncProcessStreamReader.Start breaks out of its read loop (RunCommand/AsyncProcessStreamReader.cs:74-77) and stops reading both pipes. A read faults when the handler callback throws, or when the handler's Encoding uses throwOnInvalidBytes: true, a case the code explicitly supports ("carries a decode error out to the caller").
WhenAll doesn't complete until the process exits, and nothing kills the process. Once the child writes more than the pipe buffer holds (≈64 KB on Linux, less on Windows), it blocks on write() forever. The call never returns, and the exception is never surfaced.
Reproduction (Linux, net10.0)
new OutputHandler(s => throw new InvalidOperationException()) with sh -c "echo first; sleep 0.5; head -c 1000000 /dev/zero" → the task is still pending after 10 s, and the head process is still alive.
encoding: new UTF8Encoding(false, throwOnInvalidBytes: true) with sh -c "printf '\377\n'; sleep 0.5; head -c 1000000 /dev/zero" → the task is still pending after 10 s.
Suggested fix
In RunAsync, observe the reader task on its own. If it faults, call the existing TryKill(process) (killing the whole process tree), then await the exit and rethrow the read exception. For example:
Priority: High. ExecuteAsync never returns, the exception that caused it is never seen, and the child process is left blocked on a full pipe. Two triggers cause it: a handler that throws, and a strict Encoding, which the code documents as supported. There is no workaround short of making sure the handler never throws.
Area / suggested assignee:RunAsync completion (RunCommand.cs:484-485) and the fault path in AsyncProcessStreamReader.Start. Maintainer: @matt-edmondson.
Next steps: Land #87 first, then build this fix on its ReadToEnd structure. When a read loop faults, call TryKill(process) so the whole process tree is killed, then await exit and rethrow the original exception. Add both repros as tests, with a timeout, and assert that no child process is still alive afterwards.
Activity
matt-edmondson commentedon Sep 26, 2026
Triage
ExecuteAsyncnever returns, the exception that caused it is never seen, and the child process is left blocked on a full pipe. Two triggers cause it: a handler that throws, and a strictEncoding, which the code documents as supported. There is no workaround short of making sure the handler never throws.RunAsynccompletion (RunCommand.cs:484-485) and the fault path inAsyncProcessStreamReader.Start. Maintainer: @matt-edmondson.OutputHandlercompletion contract.Startinto aReadToEndloop for each stream, and awaits both throughDrainOrAbandon. Once one stream's loop has faulted, nothing reads that pipe any more. The child can still block onwrite(), andWaitForExitAsynccan still wait forever. So Read each output stream to end of stream, not just until exit #87 is unlikely to fix this on its own, and it conflicts with any fix made here.Next steps: Land #87 first, then build this fix on its
ReadToEndstructure. When a read loop faults, callTryKill(process)so the whole process tree is killed, then await exit and rethrow the original exception. Add both repros as tests, with a timeout, and assert that no child process is still alive afterwards.Generated by Claude Code