Skip to content

Commit 7dd4436

Browse files
committed
Handle platform differences and propagate sigterm
This commit now only handles sigterm explicitly, using tokio sigint handling, and implements the different sigterm propagation based on windows vs unix systems
1 parent 091ca23 commit 7dd4436

File tree

4 files changed

+106
-48
lines changed

4 files changed

+106
-48
lines changed

Cargo.lock

Lines changed: 27 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ url = { workspace = true }
9797
walkdir = { workspace = true }
9898
which = { workspace = true }
9999
zip = { workspace = true }
100+
cfg-if = "1.0"
100101

101102
[dev-dependencies]
102103
assert_cmd = { version = "2.0.16" }
@@ -115,6 +116,9 @@ similar = { version = "2.6.0" }
115116
tempfile = { workspace = true }
116117
zip = { workspace = true }
117118

119+
[target.'cfg(unix)'.dependencies]
120+
nix = "0.23"
121+
118122
[package.metadata.cargo-shear]
119123
ignored = [
120124
"flate2",

crates/uv/src/commands/project/run.rs

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ use std::path::{Path, PathBuf};
77

88
use anstream::eprint;
99
use anyhow::{anyhow, bail, Context};
10+
use cfg_if::cfg_if;
1011
use futures::StreamExt;
1112
use itertools::Itertools;
1213
use owo_colors::OwoColorize;
13-
use tokio::process::Command;
14-
use tokio::select;
15-
use tokio::signal::unix::{signal, SignalKind};
14+
use tokio::process::{Child, Command};
1615
use tracing::{debug, warn};
1716
use url::Url;
1817
use uv_cache::Cache;
@@ -996,30 +995,29 @@ pub(crate) async fn run(
996995
// Ignore signals in the parent process, deferring them to the child. This is safe as long as
997996
// the command is the last thing that runs in this process; otherwise, we'd need to restore the
998997
// signal handlers after the command completes.
999-
let mut term_signal = signal(SignalKind::terminate())?;
1000-
let mut int_signal = signal(SignalKind::interrupt())?;
1001-
1002-
let status = select! {
1003-
status = handle.wait() => status,
1004-
1005-
// `SIGTERM`
1006-
_ = term_signal.recv() => {
1007-
handle.kill().await?;
1008-
handle.wait().await.context("Child process disappeared")?;
1009-
return Ok(ExitStatus::Failure);
1010-
}
998+
let _handler = tokio::spawn(async { while tokio::signal::ctrl_c().await.is_ok() {} });
999+
1000+
#[cfg(unix)]
1001+
{
1002+
use tokio::select;
1003+
use tokio::signal::unix::{signal, SignalKind};
1004+
let mut term_signal = signal(SignalKind::terminate())?;
1005+
loop {
1006+
select! {
1007+
_ = handle.wait() => {
1008+
break
1009+
},
10111010

1012-
// `SIGINT`
1013-
_ = int_signal.recv() => {
1014-
handle.kill().await?;
1015-
handle.wait().await.context("Child process disappeared")?;
1016-
return Ok(ExitStatus::Failure);
1011+
// `SIGTERM`
1012+
_ = term_signal.recv() => {
1013+
let _ = terminate_process(&mut handle);
1014+
}
1015+
};
10171016
}
1018-
};
1019-
1020-
let status = status.context("Child process disappeared")?;
1017+
}
10211018

10221019
// Exit based on the result of the command
1020+
let status = handle.wait().await?;
10231021
if let Some(code) = status.code() {
10241022
debug!("Command exited with code: {code}");
10251023
if let Ok(code) = u8::try_from(code) {
@@ -1038,6 +1036,22 @@ pub(crate) async fn run(
10381036
}
10391037
}
10401038

1039+
#[allow(clippy::items_after_statements, dead_code)]
1040+
fn terminate_process(_child: &mut Child) -> Result<(), anyhow::Error> {
1041+
cfg_if! {
1042+
if #[cfg(unix)] {
1043+
let pid = _child.id().context("Failed to get child process ID")?;
1044+
use nix::sys::signal::{self, Signal};
1045+
use nix::unistd::Pid;
1046+
signal::kill(Pid::from_raw(pid.try_into().unwrap()), Signal::SIGTERM)
1047+
.context("Failed to send SIGTERM")
1048+
} else if #[cfg(windows)] {
1049+
// On Windows, use winapi to terminate the process gracefully
1050+
todo!("Implement graceful termination on Windows");
1051+
}
1052+
}
1053+
}
1054+
10411055
/// Returns `true` if we can skip creating an additional ephemeral environment in `uv run`.
10421056
fn can_skip_ephemeral(
10431057
spec: Option<&RequirementsSpecification>,

crates/uv/src/commands/tool/run.rs

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use cfg_if::cfg_if;
12
use std::fmt::Display;
23
use std::fmt::Write;
34
use std::path::PathBuf;
@@ -7,9 +8,8 @@ use anstream::eprint;
78
use anyhow::{bail, Context};
89
use itertools::Itertools;
910
use owo_colors::OwoColorize;
11+
use tokio::process::Child;
1012
use tokio::process::Command;
11-
use tokio::select;
12-
use tokio::signal::unix::{signal, SignalKind};
1313
use tracing::{debug, warn};
1414

1515
use uv_cache::{Cache, Refresh};
@@ -236,30 +236,29 @@ pub(crate) async fn run(
236236
// Ignore signals in the parent process, deferring them to the child. This is safe as long as
237237
// the command is the last thing that runs in this process; otherwise, we'd need to restore the
238238
// signal handlers after the command completes.
239-
let mut term_signal = signal(SignalKind::terminate())?;
240-
let mut int_signal = signal(SignalKind::interrupt())?;
239+
let _handler = tokio::spawn(async { while tokio::signal::ctrl_c().await.is_ok() {} });
241240

242-
let status = select! {
243-
status = handle.wait() => status,
244-
245-
// `SIGTERM`
246-
_ = term_signal.recv() => {
247-
handle.kill().await?;
248-
handle.wait().await.context("Child process disappeared")?;
249-
return Ok(ExitStatus::Failure);
250-
}
251-
252-
// `SIGINT`
253-
_ = int_signal.recv() => {
254-
handle.kill().await?;
255-
handle.wait().await.context("Child process disappeared")?;
256-
return Ok(ExitStatus::Failure);
241+
#[cfg(unix)]
242+
{
243+
use tokio::select;
244+
use tokio::signal::unix::{signal, SignalKind};
245+
let mut term_signal = signal(SignalKind::terminate())?;
246+
loop {
247+
select! {
248+
_ = handle.wait() => {
249+
break
250+
},
251+
252+
// `SIGTERM`
253+
_ = term_signal.recv() => {
254+
let _ = terminate_process(&mut handle);
255+
}
256+
};
257257
}
258-
};
259-
260-
let status = status.context("Child process disappeared")?;
258+
}
261259

262260
// Exit based on the result of the command
261+
let status = handle.wait().await?;
263262
if let Some(code) = status.code() {
264263
debug!("Command exited with code: {code}");
265264
if let Ok(code) = u8::try_from(code) {
@@ -278,6 +277,23 @@ pub(crate) async fn run(
278277
}
279278
}
280279

280+
#[allow(clippy::items_after_statements, dead_code)]
281+
fn terminate_process(_child: &mut Child) -> Result<(), anyhow::Error> {
282+
cfg_if! {
283+
if #[cfg(unix)] {
284+
let pid = _child.id().context("Failed to get child process ID")?;
285+
// On Unix, send SIGTERM for a graceful shutdown
286+
use nix::sys::signal::{self, Signal};
287+
use nix::unistd::Pid;
288+
signal::kill(Pid::from_raw(pid.try_into().unwrap()), Signal::SIGTERM)
289+
.context("Failed to send SIGTERM")
290+
} else if #[cfg(windows)] {
291+
// On Windows, use winapi to terminate the process gracefully
292+
todo!("Implement graceful termination on Windows");
293+
}
294+
}
295+
}
296+
281297
/// Return the entry points for the specified package.
282298
fn get_entrypoints(
283299
from: &PackageName,

0 commit comments

Comments
 (0)