Kill the command and rethrow when an output read fails [patch] - #96
Merged
Merged
Conversation
A handler that throws, or a strict encoding that rejects the output, faulted one read loop. Nothing read that pipe any more, so the command blocked once it filled it, while ExecuteAsync waited on the other stream and on an exit that never came. The call hung and the exception never surfaced. The reader now throws the first failed read straight away, abandoning the other, and RunAsync kills the process tree before rethrowing. Fixes #89 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TRHs5nFW38XRh3KGTHYjE6
Resolves the conflicts with #94 (kill on cancellation) and #97 (spin regression test). The read-fault kill now sits inside the else branch of #94's try/catch (OperationCanceledException), and the tests from both sides are kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TRHs5nFW38XRh3KGTHYjE6
|
This was referenced Sep 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Fixes #89
ExecuteAsynccould hang forever if an output read failed. The command it started was left behind, blocked on a write.Cause
A read fails in two ways: the handler throws, or a strict
Encoding(throwOnInvalidBytes: true) rejects the bytes. Either way, that stream'sReadToEndloop ends and nothing drains its pipe any more. Once the command fills the pipe it blocks onwrite(). Meanwhile:DrainOrAbandonwaited onTask.WhenAllfor both reads, and the other stream never reaches end of stream.RunAsyncwaited onTask.WhenAll(reader, WaitForExitAsync), and the process never exits.The call never returned, and the exception never reached the caller.
Fix
AsyncProcessStreamReader.DrainOrAbandonraces the two reads and cancellation one at a time. When a read faults, it abandons both reads, reusing the existingAbandonso nothing goes unobserved. It then awaits the faulted read, so the original exception is what gets thrown.RunAsyncawaits the reader first. If the reader throws, it callsTryKill(process)(the whole tree) and rethrows. The exit wait comes afterwards, on the success and cancellation paths only. Cancellation still ends the same way: the reader abandons its reads and returns, andWaitForExitAsync(token)throws.CLAUDE.md's Stream reading section now notes this behaviour.Tests
The two new tests are the two repros from the issue. Each runs
sh -c "echo $$ > pidfile; <fault>; sleep 0.5; exec head -c 1000000 /dev/zero". Theexeckeeps the blocked writer on the recorded pid. Each test requires three things:OperationCanceledException/proc/<pid>/statwith zombies counted as exitedThe tests are:
ExecuteAsyncShouldThrowAndKillTheCommandWhenTheOutputHandlerThrowsExecuteAsyncShouldThrowAndKillTheCommandWhenAStrictEncodingRejectsTheOutputThey are Linux only (
Assert.Inconclusiveon Windows), like the neighbouring procfs tests.Results:
main: both tests fail, hitting the 10 s bound, and theheadprocess was still alive afterwards.Note on #94
#94 wraps the same
if (useElevation) … else …block in atry/catch (OperationCanceledException). Whichever PR merges second will get a small textual conflict there. The two changes compose: this PR'stry/catchsits inside theelsebranch.🤖 Generated with Claude Code
https://claude.ai/code/session_01TRHs5nFW38XRh3KGTHYjE6
Generated by Claude Code