Skip to content
Open
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
17 changes: 17 additions & 0 deletions .vscode/cspell.dictionaries/jargon.wordlist.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
AFAICT
alrm
sigalrm
SIGALRM
asimd
ASIMD
alloc
Expand Down Expand Up @@ -31,6 +34,7 @@ denoland
deque
dequeue
dev
EAGAIN
EINTR
eintr
nextest
Expand Down Expand Up @@ -98,6 +102,7 @@ multicall
nmerge
noatime
nocache
NOCLDSTOP
nocreat
noctty
noerror
Expand All @@ -122,8 +127,10 @@ preload
prepend
prepended
primality
pselect
pseudoprime
pseudoprimes
pthread
quantiles
readonly
reparse
Expand All @@ -135,21 +142,31 @@ semiprimes
setcap
setfacl
setfattr
setmask
setlocale
shortcode
shortcodes
sigaction
sigaddset
siginfo
sigmask
sigusr
sigprocmask
SIGRTMAX
SIGRTMIN
sigset
strcasecmp
subcommand
subexpression
submodule
suseconds
sync
symlink
symlinks
syscall
syscalls
sysconf
timeval
tokenize
toolchain
totalram
Expand Down
9 changes: 7 additions & 2 deletions src/uu/timeout/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@ path = "src/timeout.rs"
[dependencies]
clap = { workspace = true }
libc = { workspace = true }
nix = { workspace = true, features = ["signal"] }
uucore = { workspace = true, features = ["parser", "process", "signals"] }
nix = { workspace = true, features = ["signal", "poll"] }
uucore = { workspace = true, features = [
"parser",
"process",
"signals",
"pipes",
] }
fluent = { workspace = true }

[[bin]]
Expand Down
148 changes: 84 additions & 64 deletions src/uu/timeout/src/timeout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ mod status;

use crate::status::ExitStatus;
use clap::{Arg, ArgAction, Command};
use nix::sys::signal::Signal;
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::{UResult, USimpleError, UUsageError};
use uucore::parser::parse_time;
use uucore::process::ChildExt;
use uucore::process::{ChildExt, CommandExt, SelfPipe, WaitOrTimeoutRet};
use uucore::translate;

#[cfg(unix)]
use uucore::signals::enable_pipe_errors;
use ::{
nix::sys::signal::{SigSet, SigmaskHow, pthread_sigmask},
uucore::signals::{enable_pipe_errors, is_ignored},
};

use uucore::{
format_usage, show_error,
Expand Down Expand Up @@ -176,34 +179,6 @@ pub fn uu_app() -> Command {
.after_help(translate!("timeout-after-help"))
}

/// Remove pre-existing SIGCHLD handlers that would make waiting for the child's exit code fail.
fn unblock_sigchld() {
unsafe {
nix::sys::signal::signal(
nix::sys::signal::Signal::SIGCHLD,
nix::sys::signal::SigHandler::SigDfl,
)
.unwrap();
}
}

/// 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 @@ -258,24 +233,28 @@ fn wait_or_kill_process(
preserve_status: bool,
foreground: bool,
verbose: bool,
self_pipe: &mut SelfPipe,
) -> std::io::Result<i32> {
// ignore `SIGTERM` here
match process.wait_or_timeout(duration, None) {
Ok(Some(status)) => {
self_pipe.unset_other(Signal::SIGTERM)?;

match process.wait_or_timeout(duration, self_pipe) {
Ok(WaitOrTimeoutRet::InTime(status)) => {
if preserve_status {
Ok(status.code().unwrap_or_else(|| status.signal().unwrap()))
} else {
Ok(ExitStatus::TimeoutFailed.into())
}
}
Ok(None) => {
Ok(WaitOrTimeoutRet::TimedOut) => {
let signal = signal_by_name_or_value("KILL").unwrap();
report_if_verbose(signal, cmd, verbose);
send_signal(process, signal, foreground);
process.wait()?;
Ok(ExitStatus::SignalSent(signal).into())
}
Err(_) => Ok(ExitStatus::CommandTimedOut.into()),
Ok(WaitOrTimeoutRet::CustomSignaled(n)) => Ok(ExitStatus::SignalSent(n as _).into()),
Err(_) => Ok(ExitStatus::TimeoutFailed.into()),
}
}

Expand Down Expand Up @@ -305,6 +284,37 @@ fn preserve_signal_info(signal: libc::c_int) -> libc::c_int {
signal
}

#[cfg(unix)]
fn block_ignored_signals() -> nix::Result<()> {
#[cfg(target_os = "linux")]
fn rt_signals() -> impl Iterator<Item = i32> {
libc::SIGRTMIN()..=libc::SIGRTMAX()
}
#[cfg(not(target_os = "linux"))]
fn rt_signals() -> impl Iterator<Item = i32> {
std::iter::empty()
}

let mut set = SigSet::empty();
for s in Signal::iterator()
.filter_map(|s| {
if matches!(s, Signal::SIGSTOP | Signal::SIGKILL | Signal::SIGTERM) {
None
} else {
Some(s as i32)
}
})
.chain(rt_signals())
{
if is_ignored(s)? {
// We use raw libc bindings because [`nix`] does not support RT signals.
// SAFETY: SigSet is repr(transparent) over sigset_t.
unsafe { libc::sigaddset((&raw mut set).cast(), s) };
}
}
pthread_sigmask(SigmaskHow::SIG_BLOCK, Some(&set), None)
}

fn timeout(
cmd: &[String],
duration: Duration,
Expand All @@ -318,52 +328,61 @@ fn timeout(
unsafe { libc::setpgid(0, 0) };
}
#[cfg(unix)]
enable_pipe_errors()?;
// We keep the inherited SIGPIPE disposition if ignored.
if !is_ignored(Signal::SIGPIPE as _)? {
enable_pipe_errors()?;
}

let process = &mut process::Command::new(&cmd[0])
let mut command = process::Command::new(&cmd[0]);
command
.args(&cmd[1..])
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.map_err(|err| {
let status_code = match err.kind() {
ErrorKind::NotFound => ExitStatus::CommandNotFound.into(),
ErrorKind::PermissionDenied => ExitStatus::CannotInvoke.into(),
_ => ExitStatus::CannotInvoke.into(),
};
USimpleError::new(
status_code,
translate!("timeout-error-failed-to-execute-process", "error" => err),
)
})?;
unblock_sigchld();
catch_sigterm();
.stderr(Stdio::inherit());
#[cfg(unix)]
block_ignored_signals()?;
let mut set = SigSet::empty();
set.add(Signal::SIGTERM);
set.add(Signal::SIGUSR1);
let mut self_pipe = command.set_up_timeout(set)?;
let process = &mut command.spawn().map_err(|err| {
let status_code = match err.kind() {
ErrorKind::NotFound => ExitStatus::CommandNotFound.into(),
ErrorKind::PermissionDenied => ExitStatus::CannotInvoke.into(),
_ => ExitStatus::CannotInvoke.into(),
};
USimpleError::new(
status_code,
translate!("timeout-error-failed-to-execute-process", "error" => err),
)
})?;
// Wait for the child process for the specified time period.
//
// If the process exits within the specified time period (the
// `Ok(Some(_))` arm), then return the appropriate status code.
//
// If the process does not exit within that time (the `Ok(None)`
// arm) and `kill_after` is specified, then try sending `SIGKILL`.
//
// 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, Some(&SIGNALED)) {
Ok(Some(status)) => Err(status
match process.wait_or_timeout(duration, &mut self_pipe) {
Ok(WaitOrTimeoutRet::InTime(status)) => Err(status
.code()
.unwrap_or_else(|| preserve_signal_info(status.signal().unwrap()))
.into()),
Ok(None) => {
Ok(WaitOrTimeoutRet::CustomSignaled(n)) => {
report_if_verbose(signal, &cmd[0], verbose);
send_signal(process, signal, foreground);
process.wait()?;
if n == Signal::SIGTERM as i32 {
Err(ExitStatus::Terminated.into())
} else {
Err(ExitStatus::SignalSent(n as _).into())
}
}
Ok(WaitOrTimeoutRet::TimedOut) => {
report_if_verbose(signal, &cmd[0], verbose);
send_signal(process, signal, foreground);
match kill_after {
None => {
let status = process.wait()?;
if SIGNALED.load(atomic::Ordering::Relaxed) {
Err(ExitStatus::Terminated.into())
} else if preserve_status {
if preserve_status {
if let Some(ec) = status.code() {
Err(ec.into())
} else if let Some(sc) = status.signal() {
Expand All @@ -383,6 +402,7 @@ fn timeout(
preserve_status,
foreground,
verbose,
&mut self_pipe,
) {
Ok(status) => Err(status.into()),
Err(e) => Err(USimpleError::new(
Expand Down
3 changes: 2 additions & 1 deletion src/uucore/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ nix = { workspace = true, features = [
"signal",
"dir",
"user",
"poll",
] }
xattr = { workspace = true, optional = true }

Expand Down Expand Up @@ -163,7 +164,7 @@ ringbuffer = []
safe-traversal = ["libc"]
selinux = ["dep:selinux"]
smack = ["xattr"]
signals = []
signals = ["libc"]
sum = [
"digest",
"hex",
Expand Down
Loading
Loading