From 82f486438653d98b9bfadfe8317b836a2f47312e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 24 Jul 2024 08:33:10 -0400 Subject: [PATCH] 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](https://github.com/prefix-dev/pixi/blob/36f1bb297db04337172510f41e5b03d7da13c49f/src/cli/exec.rs#L99) and [Wasmer](https://github.com/wasmerio/wasmer/blob/49e60af8df31015cadee063c0e4618b81573fec2/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. --- Cargo.toml | 2 +- crates/uv/src/commands/project/run.rs | 6 ++++++ crates/uv/src/commands/tool/run.rs | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5a854a41e..e5218ef5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,7 +138,7 @@ tempfile = { version = "3.9.0" } textwrap = { version = "0.16.1" } thiserror = { version = "1.0.56" } 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-tar = { version = "0.3.1" } tokio-util = { version = "0.7.10", features = ["compat"] } diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 59313d6ee..69d8a6c4e 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -471,6 +471,12 @@ pub(crate) async fn run( 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")?; // Exit based on the result of the command diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 7fc47a18b..f0c335150 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -185,6 +185,11 @@ pub(crate) async fn run( } .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")?; // Exit based on the result of the command