Skip to content

Commit 0aba31b

Browse files
branchseerclaude
andcommitted
Add ctrl-c e2e test and handle SIGINT gracefully in vt
Add a `vtt exit-on-ctrlc` subcommand and e2e fixture that verifies SIGINT propagates to concurrent tasks when the user presses Ctrl+C. The test runs two packages with `vt run -r dev`, synchronizes via milestone protocol, then sends ctrl-c and verifies both tasks receive and handle it. Handle SIGINT/CTRL_C gracefully in vt: ignore the signal in the parent process (so it survives Ctrl+C) while resetting to SIG_DFL in pre_exec for child processes (so they can install their own handlers). This lets vt wait for tasks to exit and report their actual exit status. Also adds `ctrl-c` as a new `write-key` interaction type, fixes `expect_milestone` to preserve unmatched milestones, and strips `^C` terminal echo in redaction for cross-platform consistency. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c100c3b commit 0aba31b

File tree

4 files changed

+33
-6
lines changed

4 files changed

+33
-6
lines changed

crates/vite_task/src/session/execute/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,7 @@ async fn spawn_inherited(
560560
}
561561
}
562562
}
563+
563564
Ok(())
564565
});
565566
}

crates/vite_task_bin/src/main.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,24 @@ use clap::Parser as _;
44
use vite_task::{Command, ExitStatus, Session};
55
use vite_task_bin::OwnedSessionConfig;
66

7-
#[tokio::main]
8-
async fn main() -> anyhow::Result<ExitCode> {
9-
let exit_status = run().await?;
10-
Ok(exit_status.0.into())
7+
fn main() -> anyhow::Result<ExitCode> {
8+
// Ignore SIGINT before the tokio runtime starts, so that Ctrl+C does not
9+
// kill this process. Child tasks receive SIGINT directly from the terminal
10+
// driver (same process group) and handle it themselves. This lets the
11+
// runner wait for tasks to exit and report their actual exit status.
12+
// Child processes reset to SIG_DFL in pre_exec (see spawn_inherited).
13+
ignore_ctrlc();
14+
15+
tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on(async {
16+
let exit_status = run().await?;
17+
Ok(exit_status.0.into())
18+
})
19+
}
20+
21+
fn ignore_ctrlc() {
22+
// Register a no-op Ctrl+C handler. After exec, signal handlers are reset
23+
// to SIG_DFL, so child processes can install their own handlers normally.
24+
ctrlc::set_handler(|| {}).expect("failed to set ctrl-c handler");
1125
}
1226

1327
async fn run() -> anyhow::Result<ExitStatus> {

crates/vite_task_bin/tests/e2e_snapshots/fixtures/ctrl-c/snapshots/ctrl-c terminates running tasks.snap

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
33
expression: e2e_outputs
44
---
5-
[1]> vt run -r dev
5+
> vt run -r dev
66
@ expect-milestone: ready
77
~/packages/a$ vtt exit-on-ctrlccache disabled
88
~/packages/b$ vtt exit-on-ctrlccache disabled
@@ -12,4 +12,7 @@ expression: e2e_outputs
1212
@ write-key: ctrl-c
1313
~/packages/a$ vtt exit-on-ctrlccache disabled
1414
~/packages/b$ vtt exit-on-ctrlccache disabled
15-
^Cctrl-c receivedctrl-c received
15+
ctrl-c receivedctrl-c received
16+
17+
---
18+
vt run: 0/2 cache hit (0%). (Run `vt run --last-details` for full details)

crates/vite_task_bin/tests/e2e_snapshots/redact.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ pub fn redact_e2e_output(mut output: String, workspace_root: &str) -> String {
101101
let mise_warning_regex = regex::Regex::new(r"(?m)^mise WARN\s+.*\n?").unwrap();
102102
output = mise_warning_regex.replace_all(&output, "").into_owned();
103103

104+
// Remove ^C echo that Unix terminal drivers emit when ETX (0x03) is written
105+
// to the PTY. Windows ConPTY does not echo it.
106+
{
107+
use cow_utils::CowUtils as _;
108+
if let Cow::Owned(replaced) = output.as_str().cow_replace("^C", "") {
109+
output = replaced;
110+
}
111+
}
112+
104113
// Sort consecutive diagnostic blocks to handle non-deterministic tool output
105114
// (e.g., oxlint reports warnings in arbitrary order due to multi-threading).
106115
// Each block starts with " ! " and ends at the next empty line.

0 commit comments

Comments
 (0)