Skip to content
Merged
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
27 changes: 13 additions & 14 deletions crates/uv/src/commands/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ pub(crate) async fn run_to_completion(mut handle: Child) -> anyhow::Result<ExitS
// its process group, in which case the terminal driver will _not_ send SIGINT to the child
// process and uv must take ownership of forwarding the signal.
//
// Note this assumes an interactive terminal. If a signal is sent directly to the uv parent
// process (e.g., `kill -2 <pid>`), the process group is not involved and a signal is not sent
// to the child by default. In this context, uv must forward the signal to the child. We work
// around this by forwarding SIGINT if it is received more than once. We could attempt to infer
// if the parent is a terminal using TTY detection(?), but there hasn't been sufficient
// motivation to explore alternatives yet.
// Note the above only applies in an interactive terminal. If a signal is sent directly to the
// uv parent process (e.g., `kill -2 <pid>`), the process group is not involved and a signal is
// not sent to the child by default. In this context, uv must forward the signal to the child.
// uv checks if stdin is a TTY as a heuristic to determine if uv is running in an interactive
// terminal. When not in an interactive terminal, uv will forward SIGINT to the child.
//
// Use of SIGTERM is also a bit complicated. If a terminal receives a SIGTERM, it just waits for
// its children to exit — multiple SIGTERMs do not have any effect and the signals are not
Expand All @@ -38,6 +37,7 @@ pub(crate) async fn run_to_completion(mut handle: Child) -> anyhow::Result<ExitS
// to the child process or the process will not be killable.
#[cfg(unix)]
let status = {
use std::io::{IsTerminal, stdin};
use std::ops::Deref;

use anyhow::Context;
Expand Down Expand Up @@ -94,7 +94,6 @@ pub(crate) async fn run_to_completion(mut handle: Child) -> anyhow::Result<ExitS

let mut sigterm_handle = handle_signal(SignalKind::terminate())?;
let mut sigint_handle = handle_signal(SignalKind::interrupt())?;
let mut sigint_count = 0;

// The following signals are terminal by default, but can have user defined handlers.
// Forward them to the child process for handling.
Expand Down Expand Up @@ -130,6 +129,9 @@ pub(crate) async fn run_to_completion(mut handle: Child) -> anyhow::Result<ExitS
)))]
let mut siginfo_handle = PlatformSpecificSignal::Dummy;

// Use TTY detection as a heuristic
let is_interactive = stdin().is_terminal();

// Notably, we do not handle `SIGCHLD` (sent when a child process status changes) and
// `SIGIO` or `SIGPOLL` (sent when a file descriptor is ready for io) as they don't make
// sense for this parent / child relationship.
Expand All @@ -151,14 +153,11 @@ pub(crate) async fn run_to_completion(mut handle: Child) -> anyhow::Result<ExitS
// Check if the child pgid has changed
let child_pgid = getpgid(Some(child_pid)).context("Failed to get PID of child process")?;

// Increment the number of interrupts seen
sigint_count += 1;

// If the pgid _differs_ from the parent, the child will not receive a SIGINT
// and we should forward it. If we've received multiple SIGINTs, forward it
// regardless.
if child_pgid == parent_pgid && sigint_count < 2 {
debug!("Received SIGINT, assuming the child received it as part of the process group");
// and we should forward it. If we think we're not in an interactive terminal,
// forward the signal unconditionally.
if child_pgid == parent_pgid && is_interactive {
debug!("Received SIGINT, assuming the child received it from the terminal driver");
continue;
}

Expand Down
Loading