fix(plain): honour Ctrl+C during probes and kill the final command - #12
Open
bsg62 wants to merge 1 commit into
Open
fix(plain): honour Ctrl+C during probes and kill the final command#12bsg62 wants to merge 1 commit into
bsg62 wants to merge 1 commit into
Conversation
Plain mode built a fresh `tokio::signal::ctrl_c()` future at every `select!`, so a SIGINT delivered outside those windows was dropped — and because tokio installs a handler, the default terminate action was gone too, leaving the process uninterruptible rather than killed. With a slow readiness probe the future was armed for roughly one second in eleven, so Ctrl+C usually did nothing. A dedicated task now owns a single long-lived future and publishes to a watch channel that every wait point selects on, including the probe round itself. On shutdown the runner called `std::process::exit`, which left the final command and its descendants running. The final command is now always spawned as a process group and killed explicitly. `spawn`, `spawn_captured` and `spawn_inner` collapse into one `spawn_group`; plain mode still tees output to the terminal, the TUI still captures only. The probe loop also re-probed servers that had already reported ready, so a healthy server exhausted its attempts first and was named in the error while a different, slower server was the one holding up the run. Ready servers are now skipped. As a consequence plain mode no longer re-detects a server that dies after reporting ready, matching the TUI. Each regression test was checked against a faithful revert of its individual fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 three related shutdown bugs in plain (non-TUI) mode. Each was found by reading the code and confirmed by a regression test that fails against a faithful revert of that individual fix.
1. Ctrl+C was silently swallowed during health probes
The probe loop had no signal arm, and
tokio::signal::ctrl_c()was rebuilt at everyselect!— a fresh future only observes signals delivered while it is being polled. Worse, because tokio installs a SIGINT handler the default terminate action is gone, so a missed signal left the process uninterruptible rather than killing it.With a readiness endpoint that accepts but never answers (
timeout: 10), the future was armed for roughly one second in eleven. Six consecutive SIGINTs over 12 seconds were all ignored.A dedicated task now owns one long-lived
ctrl_c()future and publishes to awatchchannel that every wait point selects on, including the probe round itself.2. Ctrl+C orphaned the final command
The handler stopped the servers and called
std::process::exit. The final command was not a process group andkill_on_dropwas skipped on the tee path — andprocess::exitruns no destructors regardless. Verified: servers died, the runner exited, and the final command kept running. For the intended use case (command: "npm test") that leaks a whole test runner and its children.The final command is now always spawned as a process group and killed explicitly.
spawn,spawn_capturedandspawn_innercollapse into a singlespawn_group(command, tee_output)— plain mode still tees to the terminal, the TUI still captures only. This also removes three stale#[allow(dead_code)]items.3. The failure error named a healthy server
Every round re-probed all servers, including ones already reporting ready, incrementing their counters. The first server to exhaust its attempts aborted the run — whichever was first in the list, not whichever was broken. Against the old code the new test produces:
Ready Serverwas serving HTTP 200 the whole time;Never Readywas the actual problem. Ready servers are now skipped.Notes
0; a failure to stop servers still exits1, with its message now routed throughanyhowinstead of a bareeprintln+exit(1).Testing
76 tests pass (52 unit + 24 integration), clean across back-to-back runs with no leaked processes.
cargo clippy --all-targets -- -D warningsandcargo fmt --checkare clean.honours_ctrl_c_while_a_probe_is_in_flightrunner ignored Ctrl+C delivered during an in-flight probestops_final_command_descendants_on_ctrl_crunner did not exit promptly after Ctrl+Cblames_the_server_that_never_became_readyReady Serverinstead ofNever Ready🤖 Generated with Claude Code