8000 timeout: catch TERM signal by Luv-Ray · Pull Request #8197 · uutils/coreutils · GitHub
[go: up one dir, main page]

Skip to content

timeout: catch TERM signal #8197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 5, 2025
Merged
Show file tree
Hide file tree
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
4 changes: 4 additions & 0 deletions src/uu/timeout/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
}
}
}
Expand Down
28 changes: 25 additions & 3 deletions src/uu/timeout/src/timeout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()))
Expand Down Expand Up @@ -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
Expand All @@ -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()))
Expand All @@ -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() {
Expand Down
20 changes: 16 additions & 4 deletions src/uucore/src/lib/features/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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);
}
Expand All @@ -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;
}

Expand Down
20 changes: 20 additions & 0 deletions tests/by-util/test_timeout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,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");
}
Loading
0