Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions crates/pty_terminal/tests/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,12 @@ fn send_ctrl_c_interrupts_process() {
// On macOS/Windows, use ctrlc which works fine (no .init_array/musl issue).
#[cfg(not(target_os = "linux"))]
{
// On Windows, clear the "ignore CTRL_C" flag set by Rust runtime
// so that CTRL_C_EVENT reaches the ctrlc handler.
// On Windows, an ancestor process may have been created with
// CREATE_NEW_PROCESS_GROUP, which implicitly sets the per-process
// CTRL_C ignore flag (CONSOLE_IGNORE_CTRL_C in PEB ConsoleFlags).
// This flag is inherited by all descendants and silently drops
// CTRL_C_EVENT before it reaches registered handlers. Clear it.
// Ref: https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
#[cfg(windows)]
{
// SAFETY: Declaring correct signature for SetConsoleCtrlHandler from kernel32.
Expand Down
40 changes: 29 additions & 11 deletions crates/pty_terminal_test/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::io::{BufReader, Read};
use std::{
collections::VecDeque,
io::{BufReader, Read},
};

pub use portable_pty::CommandBuilder;
use pty_terminal::terminal::{PtyReader, Terminal};
Expand All @@ -25,6 +28,8 @@ pub struct TestTerminal {
pub struct Reader {
pty: BufReader<PtyReader>,
child_handle: ChildHandle,
/// OSC sequences taken from the PTY but not yet consumed by `expect_milestone`.
pending_osc: VecDeque<Vec<Vec<u8>>>,
}

impl TestTerminal {
Expand All @@ -37,7 +42,11 @@ impl TestTerminal {
let Terminal { pty_reader, pty_writer, child_handle, .. } = Terminal::spawn(size, cmd)?;
Ok(Self {
writer: pty_writer,
reader: Reader { pty: BufReader::new(pty_reader), child_handle: child_handle.clone() },
reader: Reader {
pty: BufReader::new(pty_reader),
child_handle: child_handle.clone(),
pending_osc: VecDeque::new(),
},
child_handle,
})
}
Expand Down Expand Up @@ -71,15 +80,24 @@ impl Reader {
let mut buf = [0u8; 4096];

loop {
let found = self
.pty
.get_ref()
.take_unhandled_osc_sequences()
.into_iter()
.filter_map(|params| {
pty_terminal_test_client::decode_milestone_from_osc8_params(&params)
})
.any(|decoded| decoded == name);
// Drain new sequences from the PTY into our local buffer.
self.pending_osc.append(&mut self.pty.get_ref().take_unhandled_osc_sequences());

// Scan for the first matching milestone, keeping the rest.
let mut found = false;
let mut remaining = VecDeque::with_capacity(self.pending_osc.len());
for params in self.pending_osc.drain(..) {
if !found
&& pty_terminal_test_client::decode_milestone_from_osc8_params(&params)
.is_some_and(|decoded| decoded == name)
{
found = true;
continue;
}
remaining.push_back(params);
}
self.pending_osc = remaining;

if found {
return self.screen_contents();
}
Expand Down
1 change: 1 addition & 0 deletions crates/vite_task/src/session/execute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,7 @@ async fn spawn_inherited(
}
}
}

Ok(())
});
}
Expand Down
2 changes: 2 additions & 0 deletions crates/vite_task_bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ path = "src/vtt/main.rs"

[dependencies]
anyhow = { workspace = true }
ctrlc = { workspace = true }
libc = { workspace = true }
notify = { workspace = true }
pty_terminal_test_client = { workspace = true, features = ["testing"] }
async-trait = { workspace = true }
clap = { workspace = true, features = ["derive"] }
jsonc-parser = { workspace = true }
Expand Down
26 changes: 20 additions & 6 deletions crates/vite_task_bin/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
use std::process::ExitCode;

use clap::Parser as _;
use vite_task::{Command, ExitStatus, Session};
use vite_task_bin::OwnedSessionConfig;

#[tokio::main]
async fn main() -> anyhow::Result<ExitCode> {
let exit_status = run().await?;
Ok(exit_status.0.into())
fn main() -> ! {
// Ignore SIGINT/CTRL_C before the tokio runtime starts. Child tasks
// receive the signal directly from the terminal driver and handle it
// themselves. This lets the runner wait for tasks to exit and report
// their actual exit status rather than being killed mid-flight.
let _ = ctrlc::set_handler(|| {});

let exit_code: i32 =
tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on(async {
match run().await {
Ok(status) => i32::from(status.0),
#[expect(clippy::print_stderr, reason = "top-level error reporting")]
Err(err) => {
eprintln!("Error: {err:?}");
1
}
}
});

std::process::exit(exit_code);
}

async fn run() -> anyhow::Result<ExitStatus> {
Expand Down
40 changes: 40 additions & 0 deletions crates/vite_task_bin/src/vtt/exit_on_ctrlc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/// exit-on-ctrlc
///
/// Sets up a Ctrl+C handler, emits a "ready" milestone, then waits.
/// When Ctrl+C is received, prints "ctrl-c received" and exits.
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
// On Windows, an ancestor process (e.g. cargo, the test harness) may have
// been created with CREATE_NEW_PROCESS_GROUP, which implicitly calls
// SetConsoleCtrlHandler(NULL, TRUE) and sets CONSOLE_IGNORE_CTRL_C in the
// PEB's ConsoleFlags. This flag is inherited by all descendants and takes
// precedence over registered handlers — CTRL_C_EVENT is silently dropped.
// Clear it so our handler can fire.
// Ref: https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
#[cfg(windows)]
{
// SAFETY: Passing (None, FALSE) clears the per-process CTRL_C ignore flag.
unsafe extern "system" {
fn SetConsoleCtrlHandler(
handler: Option<unsafe extern "system" fn(u32) -> i32>,
add: i32,
) -> i32;
}
// SAFETY: Clearing the inherited ignore flag.
unsafe {
SetConsoleCtrlHandler(None, 0);
}
}

ctrlc::set_handler(move || {
use std::io::Write;
let _ = write!(std::io::stdout(), "ctrl-c received");
let _ = std::io::stdout().flush();
std::process::exit(0);
})?;

pty_terminal_test_client::mark_milestone("ready");

loop {
std::thread::park();
}
}
4 changes: 3 additions & 1 deletion crates/vite_task_bin/src/vtt/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

mod barrier;
mod check_tty;
mod exit_on_ctrlc;
mod print;
mod print_cwd;
mod print_env;
Expand All @@ -21,7 +22,7 @@ fn main() {
if args.len() < 2 {
eprintln!("Usage: vtt <subcommand> [args...]");
eprintln!(
"Subcommands: barrier, check-tty, print, print-cwd, print-env, print-file, read-stdin, replace-file-content, touch-file"
"Subcommands: barrier, check-tty, exit-on-ctrlc, print, print-cwd, print-env, print-file, read-stdin, replace-file-content, touch-file"
);
std::process::exit(1);
}
Expand All @@ -32,6 +33,7 @@ fn main() {
check_tty::run();
Ok(())
}
"exit-on-ctrlc" => exit_on_ctrlc::run(),
"print" => {
print::run(&args[2..]);
Ok(())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "ctrl-c-test",
"private": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@ctrl-c/a",
"scripts": {
"dev": "vtt exit-on-ctrlc"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@ctrl-c/b",
"scripts": {
"dev": "vtt exit-on-ctrlc"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
packages:
- packages/*
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Tests that Ctrl+C (SIGINT) propagates to and terminates running tasks.
# Two packages run concurrently; both set up ctrl-c handlers and emit
# a "ready" milestone. After both are ready, ctrl-c is sent.

[[e2e]]
name = "ctrl-c terminates running tasks"
steps = [
{ argv = [
"vt",
"run",
"-r",
"dev",
], interactions = [
{ "expect-milestone" = "ready" },
{ "expect-milestone" = "ready" },
{ "write-key" = "ctrl-c" },
] },
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
expression: e2e_outputs
---
> vt run -r dev
@ expect-milestone: ready
~/packages/a$ vtt exit-on-ctrlc ⊘ cache disabled
~/packages/b$ vtt exit-on-ctrlc ⊘ cache disabled
@ expect-milestone: ready
~/packages/a$ vtt exit-on-ctrlc ⊘ cache disabled
~/packages/b$ vtt exit-on-ctrlc ⊘ cache disabled
@ write-key: ctrl-c
~/packages/a$ vtt exit-on-ctrlc ⊘ cache disabled
~/packages/b$ vtt exit-on-ctrlc ⊘ cache disabled
ctrl-c received

---
vt run: 0/2 cache hit (0%). (Run `vt run --last-details` for full details)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"cache": false
}
Loading
Loading