Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Changelog

- **Fixed** Cancelling a noninteractive task after a sibling failure now terminates its descendant processes as well, so worker runtimes and background helpers do not survive the cancelled run ([#724](https://github.com/voidzero-dev/vite-task/pull/724)).
- **Fixed** `vp run` no longer hangs or fails when a task leaves a process running behind it, such as a dev server or a background helper, or when one of a task's processes is killed. The run finishes as soon as the task itself does, and the files the task used are still recorded ([#544](https://github.com/voidzero-dev/vite-task/issues/544), [#675](https://github.com/voidzero-dev/vite-task/pull/675)).
- **Fixed** A task that reads or writes an unusually large number of files now runs to the end instead of being killed partway through. Vite+ reports the run as not cached, because it could not record every file the task used ([#533](https://github.com/voidzero-dev/vite-task/issues/533), [#675](https://github.com/voidzero-dev/vite-task/pull/675)).
- **Fixed** Vite+ diagnostics now display individual paths and working directories without Rust debug formatting such as quoted paths or escaped Windows backslashes ([#534](https://github.com/voidzero-dev/vite-task/pull/534)).
Expand Down
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.

16 changes: 16 additions & 0 deletions crates/fspy/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub struct Command {
cwd: Option<PathBuf>,
#[cfg(unix)]
arg0: Option<OsString>,
#[cfg(unix)]
process_group: Option<i32>,

stderr: Option<Stdio>,
stdout: Option<Stdio>,
Expand All @@ -42,6 +44,8 @@ impl Command {
cwd: None,
#[cfg(unix)]
arg0: None,
#[cfg(unix)]
process_group: None,
stderr: None,
stdout: None,
stdin: None,
Expand All @@ -50,6 +54,14 @@ impl Command {
}
}

/// Set the child process group, matching `std::process::Command`.
/// A value of zero creates a group whose ID is the child's process ID.
#[cfg(unix)]
pub const fn process_group(&mut self, process_group: i32) -> &mut Self {
self.process_group = Some(process_group);
self
}

#[cfg(unix)]
#[must_use]
pub(crate) fn get_exec(&self) -> Exec {
Expand Down Expand Up @@ -238,6 +250,10 @@ impl Command {
if let Some(arg0) = self.arg0 {
tokio_cmd.arg0(arg0);
}
#[cfg(unix)]
if let Some(process_group) = self.process_group {
tokio_cmd.process_group(process_group);
}
tokio_cmd.args(self.args);
tokio_cmd.env_clear();
tokio_cmd.envs(self.envs);
Expand Down
3 changes: 3 additions & 0 deletions crates/fspy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ pub struct ChildTermination {
}

pub struct TrackedChild {
/// The process ID captured at spawn, before the child can exit.
pub id: u32,

/// The handle for writing to the child's standard input (stdin), if it has
/// been captured.
pub stdin: Option<ChildStdin>,
Expand Down
1 change: 1 addition & 0 deletions crates/fspy/src/unix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ impl SpyImpl {
.map_err(SpawnError::OsSpawn)?;

Ok(TrackedChild {
id: child.id().expect("newly spawned child has a process ID"),
stdin: child.stdin.take(),
stdout: child.stdout.take(),
stderr: child.stderr.take(),
Expand Down
1 change: 1 addition & 0 deletions crates/fspy/src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ impl SpyImpl {
};

Ok(TrackedChild {
id: child.id().expect("newly spawned child has a process ID"),
stdin: child.stdin.take(),
stdout: child.stdout.take(),
stderr: child.stderr.take(),
Expand Down
9 changes: 8 additions & 1 deletion crates/vt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,23 @@ wax = { workspace = true }
zstd = { workspace = true }

[dev-dependencies]
tokio = { workspace = true, features = ["net", "time"] }
ctor = { workspace = true }
subprocess_test = { workspace = true }
tempfile = { workspace = true }

[target.'cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))'.dependencies]
fspy = { workspace = true }

[target.'cfg(unix)'.dependencies]
nix = { workspace = true, features = ["dir"] }
nix = { workspace = true, features = ["dir", "signal"] }

[target.'cfg(windows)'.dependencies]
winapi = { workspace = true, features = ["handleapi", "jobapi2", "winnt"] }

[lib]
doctest = false

[package.metadata.cargo-shear]
# Expanded by subprocess_test::command_for_fn! in the cancellation regression.
ignored = ["ctor"]
45 changes: 23 additions & 22 deletions crates/vt/src/session/execute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ async fn run(
fspy_enabled,
spawn_stdio,
fast_fail_token.clone(),
interrupt_token.clone(),
mode.injected_envs(),
)
.await
Expand Down Expand Up @@ -590,31 +591,31 @@ async fn run_child(
stop_accepting: Option<&StopAccepting>,
fast_fail_token: CancellationToken,
) -> Result<ChildOutcome, ExecutionError> {
let pipe_result: Result<(), ExecutionError> = if let Some(sinks) = sinks {
let stdout = child.stdout.take().expect("SpawnStdio::Piped yields a stdout pipe");
let stderr = child.stderr.take().expect("SpawnStdio::Piped yields a stderr pipe");
#[expect(
clippy::large_futures,
reason = "pipe_stdio streams child I/O and creates a large future"
)]
let r = pipe_stdio(stdout, stderr, sinks, fast_fail_token.clone()).await;
r.map_err(|err| ExecutionError::ForwardTaskProcessOutput(err.into()))
} else {
Ok(())
};

let wait_result = match pipe_result {
Ok(()) => {
child.wait.await.map_err(|err| ExecutionError::WaitForTaskProcessExit(err.into()))
}
Err(err) => {
// Pipe failed — cancel so `child.wait` kills the child instead of
// orphaning it. Still signal the server below so it can drain.
let wait = child.wait;
let pipe = async {
let result = if let Some(sinks) = sinks {
let stdout = child.stdout.take().expect("SpawnStdio::Piped yields a stdout pipe");
let stderr = child.stderr.take().expect("SpawnStdio::Piped yields a stderr pipe");
#[expect(
clippy::large_futures,
reason = "pipe_stdio streams child I/O and creates a large future"
)]
let result = pipe_stdio(stdout, stderr, sinks, fast_fail_token.clone()).await;
result.map_err(|err| ExecutionError::ForwardTaskProcessOutput(err.into()))
} else {
Ok(())
};
if result.is_err() {
fast_fail_token.cancel();
let _ = child.wait.await;
Err(err)
}
result
};
// The lifetime future forwards cancellation and terminal interruption to
// owned task groups while output is still being drained.
let (pipe_result, wait_result) = tokio::join!(pipe, wait);
let wait_result = pipe_result.and_then(|()| {
wait_result.map_err(|err| ExecutionError::WaitForTaskProcessExit(err.into()))
});

if let Some(stop_accepting) = stop_accepting {
stop_accepting.signal();
Expand Down
3 changes: 2 additions & 1 deletion crates/vt/src/session/execute/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ struct ExecutionContext<'a> {
fast_fail_token: CancellationToken,
/// Token cancelled by Ctrl-C. Unlike `fast_fail_token` (which kills
/// children), this only prevents scheduling new tasks and caching
/// results — running processes are left to handle SIGINT naturally.
/// results. Foreground processes receive SIGINT from the terminal; isolated
/// piped task groups receive the forwarded signal.
interrupt_token: CancellationToken,
}

Expand Down
Loading