Merge pull request #8197 from Luv-Ray/timeout_catch_sigterm
Some checks are pending
Code Quality / Style/toml (push) Waiting to run
CICD / Style/cargo-deny (push) Waiting to run
CICD / Build/nightly (push) Blocked by required conditions
CICD / Style/deps (push) Waiting to run
CICD / Documentation/warnings (push) Waiting to run
CICD / MinRustV (push) Waiting to run
CICD / Test all features separately (push) Blocked by required conditions
CICD / Dependencies (push) Waiting to run
CICD / Build/Makefile (push) Blocked by required conditions
CICD / Build/stable (push) Blocked by required conditions
CICD / Binary sizes (push) Blocked by required conditions
CICD / Build (push) Blocked by required conditions
CICD / Tests/BusyBox test suite (push) Blocked by required conditions
CICD / Tests/Toybox test suite (push) Blocked by required conditions
CICD / Code Coverage (push) Waiting to run
CICD / Separate Builds (push) Waiting to run
CICD / Build/SELinux (push) Blocked by required conditions
GnuTests / Run GNU tests (push) Waiting to run
Android / Test builds (push) Waiting to run
Code Quality / Style/Python (push) Waiting to run
Code Quality / Style/format (push) Waiting to run
Code Quality / Style/lint (push) Waiting to run
Code Quality / Style/spelling (push) Waiting to run
Code Quality / Pre-commit hooks (push) Waiting to run
FreeBSD / Style and Lint (push) Waiting to run
FreeBSD / Tests (push) Waiting to run

timeout: catch TERM signal
This commit is contained in:
Daniel Hofstetter 2025-07-05 17:19:40 +02:00 committed by GitHub
commit f74cceabc7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 65 additions and 7 deletions

View file

@ -30,6 +30,9 @@ pub(crate) enum ExitStatus {
/// When there is a failure while waiting for the child process to terminate.
WaitingFailed,
/// When `SIGTERM` signal received.
Terminated,
}
impl From<ExitStatus> for i32 {
@ -39,6 +42,7 @@ impl From<ExitStatus> for i32 {
ExitStatus::TimeoutFailed => 125,
ExitStatus::SignalSent(s) => 128 + s as Self,
ExitStatus::WaitingFailed => 124,
ExitStatus::Terminated => 143,
}
}
}

View file

@ -12,6 +12,7 @@ use std::collections::HashMap;
use std::io::ErrorKind;
use std::os::unix::process::ExitStatusExt;
use std::process::{self, Child, Stdio};
use std::sync::atomic::{self, AtomicBool};
use std::time::Duration;
use uucore::display::Quotable;
use uucore::error::{UClapError, UResult, USimpleError, UUsageError};
@ -185,6 +186,23 @@ fn unblock_sigchld() {
}
}
/// We should terminate child process when receiving TERM signal.
static SIGNALED: AtomicBool = AtomicBool::new(false);
fn catch_sigterm() {
use nix::sys::signal;
extern "C" fn handle_sigterm(signal: libc::c_int) {
let signal = signal::Signal::try_from(signal).unwrap();
if signal == signal::Signal::SIGTERM {
SIGNALED.store(true, atomic::Ordering::Relaxed);
}
}
let handler = signal::SigHandler::Handler(handle_sigterm);
unsafe { signal::signal(signal::Signal::SIGTERM, handler) }.unwrap();
}
/// Report that a signal is being sent if the verbose flag is set.
fn report_if_verbose(signal: usize, cmd: &str, verbose: bool) {
if verbose {
@ -246,7 +264,8 @@ fn wait_or_kill_process(
foreground: bool,
verbose: bool,
) -> std::io::Result<i32> {
match process.wait_or_timeout(duration) {
// ignore `SIGTERM` here
match process.wait_or_timeout(duration, None) {
Ok(Some(status)) => {
if preserve_status {
Ok(status.code().unwrap_or_else(|| status.signal().unwrap()))
@ -330,6 +349,7 @@ fn timeout(
)
})?;
unblock_sigchld();
catch_sigterm();
// Wait for the child process for the specified time period.
//
// If the process exits within the specified time period (the
@ -341,7 +361,7 @@ fn timeout(
// TODO The structure of this block is extremely similar to the
// structure of `wait_or_kill_process()`. They can probably be
// refactored into some common function.
match process.wait_or_timeout(duration) {
match process.wait_or_timeout(duration, Some(&SIGNALED)) {
Ok(Some(status)) => Err(status
.code()
.unwrap_or_else(|| preserve_signal_info(status.signal().unwrap()))
@ -352,7 +372,9 @@ fn timeout(
match kill_after {
None => {
let status = process.wait()?;
if preserve_status {
if SIGNALED.load(atomic::Ordering::Relaxed) {
Err(ExitStatus::Terminated.into())
} else if preserve_status {
if let Some(ec) = status.code() {
Err(ec.into())
} else if let Some(sc) = status.signal() {

View file

@ -13,6 +13,8 @@ use nix::errno::Errno;
use std::io;
use std::process::Child;
use std::process::ExitStatus;
use std::sync::atomic;
use std::sync::atomic::AtomicBool;
use std::thread;
use std::time::{Duration, Instant};
@ -88,7 +90,11 @@ pub trait ChildExt {
/// Wait for a process to finish or return after the specified duration.
/// A `timeout` of zero disables the timeout.
fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>>;
fn wait_or_timeout(
&mut self,
timeout: Duration,
signaled: Option<&AtomicBool>,
) -> io::Result<Option<ExitStatus>>;
}
impl ChildExt for Child {
@ -102,7 +108,7 @@ impl ChildExt for Child {
fn send_signal_group(&mut self, signal: usize) -> io::Result<()> {
// Ignore the signal, so we don't go into a signal loop.
if unsafe { libc::signal(signal as i32, libc::SIG_IGN) } != 0 {
if unsafe { libc::signal(signal as i32, libc::SIG_IGN) } == usize::MAX {
return Err(io::Error::last_os_error());
}
if unsafe { libc::kill(0, signal as i32) } == 0 {
@ -112,7 +118,11 @@ impl ChildExt for Child {
}
}
fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
fn wait_or_timeout(
&mut self,
timeout: Duration,
signaled: Option<&AtomicBool>,
) -> io::Result<Option<ExitStatus>> {
if timeout == Duration::from_micros(0) {
return self.wait().map(Some);
}
@ -125,7 +135,9 @@ impl ChildExt for Child {
return Ok(Some(status));
}
if start.elapsed() >= timeout {
if start.elapsed() >= timeout
|| signaled.is_some_and(|signaled| signaled.load(atomic::Ordering::Relaxed))
{
break;
}

View file

@ -204,3 +204,23 @@ fn test_hex_timeout_ending_with_d() {
.fails_with_code(124)
.no_output();
}
#[test]
fn test_terminate_child_on_receiving_terminate() {
let mut timeout_cmd = new_ucmd!()
.args(&[
"10",
"sh",
"-c",
"trap 'echo child received TERM' TERM; sleep 5",
])
.run_no_wait();
timeout_cmd.delay(100);
timeout_cmd.kill_with_custom_signal(nix::sys::signal::Signal::SIGTERM);
timeout_cmd
.make_assertion()
.is_not_alive()
.with_current_output()
.code_is(143)
.stdout_contains("child received TERM");
}