Stop ExecV3 hanging, and stop it losing output - #499
Open
shellicar wants to merge 11 commits into
Open
Conversation
shellicar
marked this pull request as ready for review
August 3, 2026 13:22
shellicar
force-pushed
the
fix/exec-hang
branch
from
August 3, 2026 13:33
d2c9562 to
0d3f499
Compare
A pipeline cancelled mid-flight left its run promise unsettled, so the CLI hung with every process already dead. The stages were joined by in-process streams that simulated backpressure and SIGPIPE. The kernel now provides both.
A stage that fails to start has no child and no second file descriptor, so 2>&1 had nothing to apply to and the reason was discarded. Its consumer never opened the pipe either, leaving the producer writing into nothing until the run was cancelled.
A lone stage was delegated to run(), which takes one stderr sink where a stage has two destinations for it, so a merged command's capture was never ended and whoever drained it waited forever. Closing a sink then waiting for it has the same shape wherever the sink is a duplex: the reader on the other end is the caller, who is waiting for the executor.
Node's stdio is a socketpair rather than a pipe, so a consumer that exits leaving unread bytes makes the kernel reset the connection: the producer's write fails with ECONNRESET and it exits non-zero, where a clean close would have raised SIGPIPE. Over 40 rounds that was SIGPIPE every time on macOS and about one in three on Linux, so a test naming the signal is asserting a coin toss.
A redirect was opened lazily, so a path that could not be written failed on the stream long after the command had run, and that failure was swallowed: the output vanished and the call reported success. Opening before the program starts is the last moment the caller can still be told. Separately, a cwd that was a file passed the existence check and threw at spawn instead, which both escaped as a raw error and left stages already spawned for that pipeline running unwatched.
A capture was collected into one string with nothing limiting it, and `yes` produces about 1.5GB a second, so a single word reached V8's maximum string length in well under a second and the whole call died with a message about string lengths. Reading has to continue past the limit and discard, because a capture that stops being read stalls the process filling it. Separately, stdout and stderr aimed at the same file each opened their own stream at offset zero, so one overwrote the other and half the output went missing while the command reported success.
Validation already refuses the same path written twice, but it runs before any working directory is known, so all it can compare is the two strings. Resolved against the command's own cwd, two spellings turn out to name one file, and two streams on one file each open at offset zero: one overwrites the other and half the output goes missing while the command reports success.
Two spellings of one path, or two links to one file, both end up as a single file with two streams opened on it at offset zero, so one overwrites the other. Canonicalising answers where a path lands, and it answers for a target that does not exist yet, which a redirect target usually does not. It takes the working directory rather than being handed an already-resolved path because expansion has to come first: resolve by hand and a leading ~ becomes a directory name that no later expansion can undo.
shellicar
force-pushed
the
fix/exec-hang
branch
from
August 4, 2026 23:31
2ec90cd to
292f9fd
Compare
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.
Summary
A producer stopped by a broken pipe now reports either a SIGPIPE signal or a non-zero exit carrying the program's own message. Which one appears is timing, not anything a caller controls, so both should be read as the same outcome. The reasoning and the options are in CLAUDE.md under Known Debt.