fix(shell_exec): stream rather than fail when delayed_stream's timed wait fails - #3881
Open
max-sixty wants to merge 4 commits into
Open
fix(shell_exec): stream rather than fail when delayed_stream's timed wait fails#3881max-sixty wants to merge 4 commits into
max-sixty wants to merge 4 commits into
Conversation
…wait fails Phase 1's `Err` arm returned to the caller after joining the reader threads, and those sit in `read_to_end` until the child closes its pipes -- so the caller waited out the child's full runtime and then got `Failed to wait for command` for a command that had already finished. The arm became reachable in #3857: `shared_child` allocates a pipe and registers a SIGCHLD handler on every timed wait, where `wait-timeout` set its self-pipe up once per process, so a sandbox or an fd limit now surfaces as `Err` where it used to surface as the abort of #3856. It now falls through to streaming, like an exceeded threshold. Phase 2's `wait()` is a bare `waitid(WNOWAIT)` loop with neither a pipe nor a signal registration, so whatever broke the timed wait can't reach it and the real exit status still comes back. The wall-clock sites tear the child down instead, because a wait they can't observe bounds nothing; the module docstring records that split. `test_cmd_delayed_stream_crosses_the_threshold` covers the fall-through, which no test reached before: the suite's thresholds are `0`, which streams without calling `wait_timeout`, and `-1`, which skips phase 1.
The first version asserted elapsed time, which a phase-1 `Ok(Some)` satisfies just as well: the child runs the same wall-clock either way, so the assertion held whether or not the arm under test was taken. A line the child writes after the switch is the discriminator. Streaming sends it to stderr, so the error's buffer is empty; had phase 1 returned a status, `late` would still be buffered. Raising the threshold above the child's runtime fails the assertion with `left: "late"`.
worktrunk-bot
approved these changes
Aug 23, 2026
worktrunk-bot
left a comment
Collaborator
There was a problem hiding this comment.
Two non-blocking notes; the change itself reads right, and the rewritten test that pins the switch by where the late output lands is a much stronger assertion than the elapsed-time one.
Cmd::delayed_stream's own docstring still glosses the progress message as printing "at the moment streaming starts (the delay threshold is crossed)". After this change a failed timed wait starts streaming too, so the parenthetical names one of two triggers — the module docstring above records the split, but the item docstring is what a caller reads. It's outside the diff, so no inline suggestion; happy to push the one-liner if you want it.
A failed timed wait now starts streaming too, so naming the threshold as the moment it prints describes one of two triggers. The module docstring above already records when each happens.
150 ms between the threshold and the child's echo races the scheduler, which tests/CLAUDE.md warns against; 450 ms covers a deschedule longer than the suite produces. The threshold stays at 50 ms rather than shrinking: a spent `remaining` skips the wait, and the test would then pass without reaching the arm it covers.
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.
Problem
Cmd::delayed_streamwaits out a delay threshold before it starts streaming a child's output. When that timed wait returned an error rather than a status, the function returned the error to the caller — but only after joining the two reader threads, and those sit inread_to_enduntil the child closes its pipes. So the caller waited out the child's full runtime and then gotFailed to wait for commandfor a command that had already finished.That arm became reachable in #3857.
shared_childallocates a pipe and registers aSIGCHLDhandler on every timed wait, wherewait-timeoutset its self-pipe up once per process. A sandbox or an fd limit that used to abort the process (#3856) now surfaces as anErr.Change
Phase 1's
Errfalls through to streaming, which is what an exceeded threshold already does. Phase 2's blockingwait()is a barewaitid(WNOWAIT)loop with neither a pipe nor a signal registration, so whatever broke the timed wait cannot reach it and the real exit status still comes back. A command whose deadline machinery fails now streams its output instead of failing.The two sites where the deadline bounds wall-clock,
run_with_timeout_impland the pager, foldErrinto their teardown arm instead, because a wait they cannot observe bounds nothing. The threshold here only decides when output starts streaming, so it streams. The module docstring records that split.Testing
test_cmd_delayed_stream_crosses_the_thresholdis new, and it covers an arm nothing reached before. Of the thresholds the suite used,0streams without ever callingwait_timeout,-1disables phase 1, and5_000against a fast child returnsOk(Some)— soOk(None)never happened.The child writes a line 450 ms after the threshold passes. That line is the discriminator: streaming sends it to stderr, so the error's buffer comes back empty, where a phase 1 that had returned a status would still have it buffered. Checked by raising the threshold above the child's runtime, which fails the assertion with
left: "late". Elapsed time would not have distinguished the two, since the child runs the same wall-clock either way.The
Errarm itself stays untested: reaching it needspipeorsigactionto fail inside the wait, and it shares every line with the fall-through the new test covers.Also run:
cargo run -- hook pre-merge --yes(4677 tests, lints, doctests) and both rustdoc variants with-D warnings.