Skip to content

Commit ee2e751

Browse files
committed
Handle platform differences and propagate sigterm
This commit handles sigterm explicitly,and implements the different sigterm propagation based on windows vs unix systems. This commit does not provide a windows implementation for handling sigterm. Affected paths are project/run and tool/run
1 parent 6da70d1 commit ee2e751

File tree

4 files changed

+105
-28
lines changed

4 files changed

+105
-28
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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +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::process::{Child, Command};
1415
use tracing::{debug, warn};
1516
use url::Url;
1617
use uv_cache::Cache;
@@ -996,9 +997,27 @@ pub(crate) async fn run(
996997
// signal handlers after the command completes.
997998
let _handler = tokio::spawn(async { while tokio::signal::ctrl_c().await.is_ok() {} });
998999

999-
let status = handle.wait().await.context("Child process disappeared")?;
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+
},
1010+
1011+
// `SIGTERM`
1012+
_ = term_signal.recv() => {
1013+
let _ = terminate_process(&mut handle);
1014+
}
1015+
};
1016+
}
1017+
}
10001018

10011019
// Exit based on the result of the command
1020+
let status = handle.wait().await?;
10021021
if let Some(code) = status.code() {
10031022
debug!("Command exited with code: {code}");
10041023
if let Ok(code) = u8::try_from(code) {
@@ -1017,6 +1036,22 @@ pub(crate) async fn run(
10171036
}
10181037
}
10191038

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+
10201055
/// Returns `true` if we can skip creating an additional ephemeral environment in `uv run`.
10211056
fn can_skip_ephemeral(
10221057
spec: Option<&RequirementsSpecification>,

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

Lines changed: 37 additions & 23 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};
@@ -238,30 +238,27 @@ pub(crate) async fn run(
238238
// signal handlers after the command completes.
239239
let _handler = tokio::spawn(async { while tokio::signal::ctrl_c().await.is_ok() {} });
240240

241-
let mut term_signal = signal(SignalKind::terminate())?;
242-
let mut int_signal = signal(SignalKind::interrupt())?;
243-
244-
let status = select! {
245-
status = handle.wait() => status,
246-
247-
// `SIGTERM`
248-
_ = term_signal.recv() => {
249-
handle.kill().await?;
250-
handle.wait().await.context("Child process disappeared")?;
251-
return Ok(ExitStatus::Failure);
252-
}
253-
254-
// `SIGINT`
255-
_ = int_signal.recv() => {
256-
handle.kill().await?;
257-
handle.wait().await.context("Child process disappeared")?;
258-
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+
};
259257
}
260-
};
261-
262-
let status = status.context("Child process disappeared")?;
258+
}
263259

264260
// Exit based on the result of the command
261+
let status = handle.wait().await?;
265262
if let Some(code) = status.code() {
266263
debug!("Command exited with code: {code}");
267264
if let Ok(code) = u8::try_from(code) {
@@ -280,6 +277,23 @@ pub(crate) async fn run(
280277
}
281278
}
282279

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+
283297
/// Return the entry points for the specified package.
284298
fn get_entrypoints(
285299
from: &PackageName,

0 commit comments

Comments
 (0)