mirror of
https://github.com/astral-sh/uv.git
synced 2025-12-09 03:08:53 +00:00
Child exit with signal n returns 128+n (#10781)
This commit is contained in:
parent
0008ec69c4
commit
f645499dbd
5 changed files with 95 additions and 111 deletions
|
|
@ -69,6 +69,7 @@ mod project;
|
||||||
mod publish;
|
mod publish;
|
||||||
mod python;
|
mod python;
|
||||||
pub(crate) mod reporters;
|
pub(crate) mod reporters;
|
||||||
|
mod run;
|
||||||
#[cfg(feature = "self-update")]
|
#[cfg(feature = "self-update")]
|
||||||
mod self_update;
|
mod self_update;
|
||||||
mod tool;
|
mod tool;
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ use crate::commands::project::{
|
||||||
EnvironmentSpecification, ProjectError, ScriptInterpreter, WorkspacePython,
|
EnvironmentSpecification, ProjectError, ScriptInterpreter, WorkspacePython,
|
||||||
};
|
};
|
||||||
use crate::commands::reporters::PythonDownloadReporter;
|
use crate::commands::reporters::PythonDownloadReporter;
|
||||||
|
use crate::commands::run::run_to_completion;
|
||||||
use crate::commands::{diagnostics, project, ExitStatus};
|
use crate::commands::{diagnostics, project, ExitStatus};
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
use crate::settings::ResolverInstallerSettings;
|
use crate::settings::ResolverInstallerSettings;
|
||||||
|
|
@ -1119,64 +1120,11 @@ pub(crate) async fn run(
|
||||||
// Spawn and wait for completion
|
// Spawn and wait for completion
|
||||||
// Standard input, output, and error streams are all inherited
|
// Standard input, output, and error streams are all inherited
|
||||||
// TODO(zanieb): Throw a nicer error message if the command is not found
|
// TODO(zanieb): Throw a nicer error message if the command is not found
|
||||||
let mut handle = process
|
let handle = process
|
||||||
.spawn()
|
.spawn()
|
||||||
.with_context(|| format!("Failed to spawn: `{}`", command.display_executable()))?;
|
.with_context(|| format!("Failed to spawn: `{}`", command.display_executable()))?;
|
||||||
|
|
||||||
// Ignore signals in the parent process, deferring them to the child. This is safe as long as
|
run_to_completion(handle).await
|
||||||
// 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() {} });
|
|
||||||
|
|
||||||
// Exit based on the result of the command.
|
|
||||||
#[cfg(unix)]
|
|
||||||
let status = {
|
|
||||||
use tokio::select;
|
|
||||||
use tokio::signal::unix::{signal, SignalKind};
|
|
||||||
|
|
||||||
let mut term_signal = signal(SignalKind::terminate())?;
|
|
||||||
loop {
|
|
||||||
select! {
|
|
||||||
result = handle.wait() => {
|
|
||||||
break result;
|
|
||||||
},
|
|
||||||
|
|
||||||
// `SIGTERM`
|
|
||||||
_ = term_signal.recv() => {
|
|
||||||
let _ = terminate_process(&mut handle);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}?;
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
let status = handle.wait().await?;
|
|
||||||
|
|
||||||
if let Some(code) = status.code() {
|
|
||||||
debug!("Command exited with code: {code}");
|
|
||||||
if let Ok(code) = u8::try_from(code) {
|
|
||||||
Ok(ExitStatus::External(code))
|
|
||||||
} else {
|
|
||||||
#[allow(clippy::exit)]
|
|
||||||
std::process::exit(code);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::process::ExitStatusExt;
|
|
||||||
debug!("Command exited with signal: {:?}", status.signal());
|
|
||||||
}
|
|
||||||
Ok(ExitStatus::Failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn terminate_process(child: &mut tokio::process::Child) -> anyhow::Result<()> {
|
|
||||||
use nix::sys::signal::{self, Signal};
|
|
||||||
use nix::unistd::Pid;
|
|
||||||
|
|
||||||
let pid = child.id().context("Failed to get child process ID")?;
|
|
||||||
signal::kill(Pid::from_raw(pid.try_into()?), Signal::SIGTERM).context("Failed to send SIGTERM")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if we can skip creating an additional ephemeral environment in `uv run`.
|
/// Returns `true` if we can skip creating an additional ephemeral environment in `uv run`.
|
||||||
|
|
|
||||||
71
crates/uv/src/commands/run.rs
Normal file
71
crates/uv/src/commands/run.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
use crate::commands::ExitStatus;
|
||||||
|
use tokio::process::Child;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
/// Wait for the child process to complete, handling signals and error codes.
|
||||||
|
pub(crate) async fn run_to_completion(mut handle: Child) -> anyhow::Result<ExitStatus> {
|
||||||
|
// 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() {} });
|
||||||
|
|
||||||
|
// Exit based on the result of the command.
|
||||||
|
#[cfg(unix)]
|
||||||
|
let status = {
|
||||||
|
use tokio::select;
|
||||||
|
use tokio::signal::unix::{signal, SignalKind};
|
||||||
|
|
||||||
|
let mut term_signal = signal(SignalKind::terminate())?;
|
||||||
|
loop {
|
||||||
|
select! {
|
||||||
|
result = handle.wait() => {
|
||||||
|
break result;
|
||||||
|
},
|
||||||
|
|
||||||
|
// `SIGTERM`
|
||||||
|
_ = term_signal.recv() => {
|
||||||
|
let _ = terminate_process(&mut handle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
let status = handle.wait().await?;
|
||||||
|
|
||||||
|
if let Some(code) = status.code() {
|
||||||
|
debug!("Command exited with code: {code}");
|
||||||
|
if let Ok(code) = u8::try_from(code) {
|
||||||
|
Ok(ExitStatus::External(code))
|
||||||
|
} else {
|
||||||
|
#[allow(clippy::exit)]
|
||||||
|
std::process::exit(code);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::process::ExitStatusExt;
|
||||||
|
debug!("Command exited with signal: {:?}", status.signal());
|
||||||
|
// Following https://tldp.org/LDP/abs/html/exitcodes.html, a fatal signal n gets the
|
||||||
|
// exit code 128+n
|
||||||
|
if let Some(mapped_code) = status
|
||||||
|
.signal()
|
||||||
|
.and_then(|signal| u8::try_from(signal).ok())
|
||||||
|
.and_then(|signal| 128u8.checked_add(signal))
|
||||||
|
{
|
||||||
|
return Ok(ExitStatus::External(mapped_code));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ExitStatus::Failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn terminate_process(child: &mut Child) -> anyhow::Result<()> {
|
||||||
|
use anyhow::Context;
|
||||||
|
use nix::sys::signal::{self, Signal};
|
||||||
|
use nix::unistd::Pid;
|
||||||
|
|
||||||
|
let pid = child.id().context("Failed to get child process ID")?;
|
||||||
|
signal::kill(Pid::from_raw(pid.try_into()?), Signal::SIGTERM).context("Failed to send SIGTERM")
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,7 @@ use crate::commands::pip::loggers::{
|
||||||
};
|
};
|
||||||
use crate::commands::project::{resolve_names, EnvironmentSpecification, ProjectError};
|
use crate::commands::project::{resolve_names, EnvironmentSpecification, ProjectError};
|
||||||
use crate::commands::reporters::PythonDownloadReporter;
|
use crate::commands::reporters::PythonDownloadReporter;
|
||||||
|
use crate::commands::run::run_to_completion;
|
||||||
use crate::commands::tool::common::{matching_packages, refine_interpreter};
|
use crate::commands::tool::common::{matching_packages, refine_interpreter};
|
||||||
use crate::commands::tool::Target;
|
use crate::commands::tool::Target;
|
||||||
use crate::commands::ExitStatus;
|
use crate::commands::ExitStatus;
|
||||||
|
|
@ -188,7 +189,7 @@ pub(crate) async fn run(
|
||||||
invocation_source,
|
invocation_source,
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut handle = match process.spawn() {
|
let handle = match process.spawn() {
|
||||||
Ok(handle) => Ok(handle),
|
Ok(handle) => Ok(handle),
|
||||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||||
match get_entrypoints(&from.name, &site_packages) {
|
match get_entrypoints(&from.name, &site_packages) {
|
||||||
|
|
@ -239,60 +240,7 @@ pub(crate) async fn run(
|
||||||
}
|
}
|
||||||
.with_context(|| format!("Failed to spawn: `{executable}`"))?;
|
.with_context(|| format!("Failed to spawn: `{executable}`"))?;
|
||||||
|
|
||||||
// Ignore signals in the parent process, deferring them to the child. This is safe as long as
|
run_to_completion(handle).await
|
||||||
// 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() {} });
|
|
||||||
|
|
||||||
// Exit based on the result of the command.
|
|
||||||
#[cfg(unix)]
|
|
||||||
let status = {
|
|
||||||
use tokio::select;
|
|
||||||
use tokio::signal::unix::{signal, SignalKind};
|
|
||||||
|
|
||||||
let mut term_signal = signal(SignalKind::terminate())?;
|
|
||||||
loop {
|
|
||||||
select! {
|
|
||||||
result = handle.wait() => {
|
|
||||||
break result;
|
|
||||||
},
|
|
||||||
|
|
||||||
// `SIGTERM`
|
|
||||||
_ = term_signal.recv() => {
|
|
||||||
let _ = terminate_process(&mut handle);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}?;
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
let status = handle.wait().await?;
|
|
||||||
|
|
||||||
if let Some(code) = status.code() {
|
|
||||||
debug!("Command exited with code: {code}");
|
|
||||||
if let Ok(code) = u8::try_from(code) {
|
|
||||||
Ok(ExitStatus::External(code))
|
|
||||||
} else {
|
|
||||||
#[allow(clippy::exit)]
|
|
||||||
std::process::exit(code);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::process::ExitStatusExt;
|
|
||||||
debug!("Command exited with signal: {:?}", status.signal());
|
|
||||||
}
|
|
||||||
Ok(ExitStatus::Failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn terminate_process(child: &mut tokio::process::Child) -> anyhow::Result<()> {
|
|
||||||
use nix::sys::signal::{self, Signal};
|
|
||||||
use nix::unistd::Pid;
|
|
||||||
|
|
||||||
let pid = child.id().context("Failed to get child process ID")?;
|
|
||||||
signal::kill(Pid::from_raw(pid.try_into()?), Signal::SIGTERM).context("Failed to send SIGTERM")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the entry points for the specified package.
|
/// Return the entry points for the specified package.
|
||||||
|
|
|
||||||
|
|
@ -3266,7 +3266,7 @@ fn run_gui_script_explicit_windows() -> Result<()> {
|
||||||
if not executable.startswith("pythonw"):
|
if not executable.startswith("pythonw"):
|
||||||
print(f"Error: Expected pythonw.exe but got: {executable}", file=sys.stderr)
|
print(f"Error: Expected pythonw.exe but got: {executable}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print(f"Using executable: {executable}", file=sys.stderr)
|
print(f"Using executable: {executable}", file=sys.stderr)
|
||||||
"#})?;
|
"#})?;
|
||||||
|
|
||||||
|
|
@ -3776,3 +3776,19 @@ fn run_with_group_conflict() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test that a signal n makes the process exit with code 128+n.
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn exit_status_signal() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let script = context.temp_dir.child("segfault.py");
|
||||||
|
script.write_str(indoc! {r"
|
||||||
|
import os
|
||||||
|
os.kill(os.getpid(), 11)
|
||||||
|
"})?;
|
||||||
|
let status = context.run().arg(script.path()).status()?;
|
||||||
|
assert_eq!(status.code().expect("a status code"), 139);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue