Ignore Ctrl-C signals in uv run and uv tool run (#5395)

## Summary

This is a bit simpler than #5333, but seems to work in my testing on
macOS and Windows. It's based on implementations that I found in
[Pixi](36f1bb297d/src/cli/exec.rs (L99))
and
[Wasmer](49e60af8df/lib/wasix/src/state/builder.rs (L1058)).

Closes https://github.com/astral-sh/uv/issues/5257.

## Test Plan

On both macOS and Windows:

- `cargo run -- tool run --from jupyterlab jupyter-lab` -- hit Ctrl-C;
verify that the process exits and the terminal is left in a good state.
- `cargo run -- run python` -- hit Ctrl-C; verify that the process does
_not_ exit, but does on Ctrl-D.
This commit is contained in:
Charlie Marsh 2024-07-24 08:33:10 -04:00 committed by GitHub
parent 20018cd0fc
commit 82f4864386
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 12 additions and 1 deletions

View file

@ -138,7 +138,7 @@ tempfile = { version = "3.9.0" }
textwrap = { version = "0.16.1" } textwrap = { version = "0.16.1" }
thiserror = { version = "1.0.56" } thiserror = { version = "1.0.56" }
tl = { version = "0.7.7" } tl = { version = "0.7.7" }
tokio = { version = "1.35.1", features = ["fs", "io-util", "macros", "process", "sync"] } tokio = { version = "1.35.1", features = ["fs", "io-util", "macros", "process", "signal", "sync"] }
tokio-stream = { version = "0.1.14" } tokio-stream = { version = "0.1.14" }
tokio-tar = { version = "0.3.1" } tokio-tar = { version = "0.3.1" }
tokio-util = { version = "0.7.10", features = ["compat"] } tokio-util = { version = "0.7.10", features = ["compat"] }

View file

@ -471,6 +471,12 @@ pub(crate) async fn run(
command.executable().to_string_lossy() command.executable().to_string_lossy()
) )
})?; })?;
// Ignore signals in the parent process, deferring them to the child. This is safe as long as
// the command is the last thing that runs in this process; otherwise, we'd need to restore the
// signal handlers after the command completes.
let _handler = tokio::spawn(async { while tokio::signal::ctrl_c().await.is_ok() {} });
let status = handle.wait().await.context("Child process disappeared")?; let status = handle.wait().await.context("Child process disappeared")?;
// Exit based on the result of the command // Exit based on the result of the command

View file

@ -185,6 +185,11 @@ pub(crate) async fn run(
} }
.with_context(|| format!("Failed to spawn: `{}`", executable.to_string_lossy()))?; .with_context(|| format!("Failed to spawn: `{}`", executable.to_string_lossy()))?;
// Ignore signals in the parent process, deferring them to the child. This is safe as long as
// the command is the last thing that runs in this process; otherwise, we'd need to restore the
// signal handlers after the command completes.
let _handler = tokio::spawn(async { while tokio::signal::ctrl_c().await.is_ok() {} });
let status = handle.wait().await.context("Child process disappeared")?; let status = handle.wait().await.context("Child process disappeared")?;
// Exit based on the result of the command // Exit based on the result of the command