mod status;
use crate::status::ExitStatus;
use clap::{Arg, ArgAction, Command};
use std::io::{ErrorKind, Write};
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::signals::install_signal_handler;
use uucore::translate;
use rustix::process::{Pid, Signal, getpid, kill_process, setpgid};
#[cfg(unix)]
use std::os::unix::process::CommandExt;
use uucore::{
format_usage,
signals::{signal_by_name_or_value, signal_list_name_by_value},
};
pub mod options {
pub static FOREGROUND: &str = "foreground";
pub static KILL_AFTER: &str = "kill-after";
pub static SIGNAL: &str = "signal";
pub static PRESERVE_STATUS: &str = "preserve-status";
pub static VERBOSE: &str = "verbose";
pub static DURATION: &str = "duration";
pub static COMMAND: &str = "command";
}
struct Config {
foreground: bool,
kill_after: Option<Duration>,
signal: usize,
duration: Duration,
preserve_status: bool,
verbose: bool,
command: Vec<String>,
}
impl Config {
fn from(options: &clap::ArgMatches) -> UResult<Self> {
let signal = match options.get_one::<String>(options::SIGNAL) {
Some(signal_) => {
let signal_result = signal_by_name_or_value(signal_);
match signal_result {
None => {
return Err(UUsageError::new(
ExitStatus::TimeoutFailed.into(),
translate!("timeout-error-invalid-signal", "signal" => signal_.quote()),
));
}
Some(signal_value) => signal_value,
}
}
_ => signal_by_name_or_value("TERM").unwrap(),
};
let kill_after = match options.get_one::<String>(options::KILL_AFTER) {
None => None,
Some(kill_after) => match parse_time::from_str(kill_after, true) {
Ok(k) => Some(k),
Err(err) => return Err(UUsageError::new(ExitStatus::TimeoutFailed.into(), err)),
},
};
let duration =
parse_time::from_str(options.get_one::<String>(options::DURATION).unwrap(), true)
.map_err(|err| UUsageError::new(ExitStatus::TimeoutFailed.into(), err))?;
let preserve_status: bool = options.get_flag(options::PRESERVE_STATUS);
let foreground = options.get_flag(options::FOREGROUND);
let verbose = options.get_flag(options::VERBOSE);
let command = options
.get_many::<String>(options::COMMAND)
.unwrap()
.map(String::from)
.collect::<Vec<_>>();
Ok(Self {
foreground,
kill_after,
signal,
duration,
preserve_status,
verbose,
command,
})
}
}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches =
uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), args, 125)?;
let config = Config::from(&matches)?;
timeout(
&config.command,
config.duration,
config.signal,
config.kill_after,
config.foreground,
config.preserve_status,
config.verbose,
)
}
pub fn uu_app() -> Command {
Command::new("timeout")
.version(uucore::crate_version!())
.help_template(uucore::localized_help_template("timeout"))
.about(translate!("timeout-about"))
.override_usage(format_usage(&translate!("timeout-usage")))
.arg(
Arg::new(options::FOREGROUND)
.long(options::FOREGROUND)
.short('f')
.help(translate!("timeout-help-foreground"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::KILL_AFTER)
.long(options::KILL_AFTER)
.short('k')
.help(translate!("timeout-help-kill-after")),
)
.arg(
Arg::new(options::PRESERVE_STATUS)
.long(options::PRESERVE_STATUS)
.short('p')
.help(translate!("timeout-help-preserve-status"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::SIGNAL)
.short('s')
.long(options::SIGNAL)
.help(translate!("timeout-help-signal"))
.value_name("SIGNAL"),
)
.arg(
Arg::new(options::VERBOSE)
.short('v')
.long(options::VERBOSE)
.help(translate!("timeout-help-verbose"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::DURATION)
.required(true)
.help(translate!("timeout-help-duration")),
)
.arg(
Arg::new(options::COMMAND)
.required(true)
.action(ArgAction::Append)
.help(translate!("timeout-help-command"))
.value_hint(clap::ValueHint::CommandName),
)
.trailing_var_arg(true)
.infer_long_args(true)
.after_help(translate!("timeout-after-help"))
}
fn install_sigchld() {
extern "C" fn chld(_: libc::c_int) {}
let _ = install_signal_handler(Signal::as_raw(Signal::CHILD), chld);
}
static SIGNALED: AtomicBool = AtomicBool::new(false);
static RECEIVED_SIGNAL: atomic::AtomicI32 = atomic::AtomicI32::new(0);
fn install_signal_handlers(term_signal: usize) {
extern "C" fn handle_signal(sig: libc::c_int) {
SIGNALED.store(true, atomic::Ordering::Relaxed);
RECEIVED_SIGNAL.store(sig, atomic::Ordering::Relaxed);
}
let sigpipe_ignored = uucore::signals::sigpipe_was_ignored();
for sig in [
Signal::ALARM,
Signal::INT,
Signal::QUIT,
Signal::HUP,
Signal::TERM,
Signal::PIPE,
Signal::USR1,
Signal::USR2,
] {
if sig == Signal::PIPE && sigpipe_ignored {
continue; }
let _ = install_signal_handler(Signal::as_raw(sig), handle_signal);
}
if let Some(sig) = signal_from_raw(term_signal as i32) {
let _ = install_signal_handler(Signal::as_raw(sig), handle_signal);
}
}
fn report_if_verbose(signal: usize, cmd: &str, verbose: bool) {
if verbose {
let s = if signal == 0 {
"0".to_string()
} else {
signal_list_name_by_value(signal).unwrap()
};
let mut stderr = std::io::stderr();
let _ = writeln!(
stderr,
"timeout: {}",
translate!("timeout-verbose-sending-signal", "signal" => s, "command" => cmd.quote())
);
let _ = stderr.flush();
}
}
fn signal_from_raw(sig: i32) -> Option<Signal> {
if sig <= 0 {
return None;
}
if let Some(s) = Signal::from_named_raw(sig) {
return Some(s);
}
#[cfg(target_os = "linux")]
{
let rtmin = libc::SIGRTMIN();
let rtmax = libc::SIGRTMAX();
if sig >= rtmin && sig <= rtmax {
return Some(unsafe { Signal::from_raw_unchecked(sig) });
}
}
None
}
fn send_signal(process: &mut Child, signal: usize, foreground: bool) {
let _ = process.send_signal(signal);
if signal == 0 || foreground {
return;
}
let _ = process.send_signal_group(signal);
let kill_signal = signal_by_name_or_value("KILL").unwrap();
let continued_signal = signal_by_name_or_value("CONT").unwrap();
if signal != kill_signal && signal != continued_signal {
let _ = process.send_signal(continued_signal);
let _ = process.send_signal_group(continued_signal);
}
}
fn wait_or_kill_process(
process: &mut Child,
cmd: &str,
duration: Duration,
preserve_status: bool,
foreground: bool,
verbose: bool,
) -> std::io::Result<i32> {
match process.wait_or_timeout(duration, None) {
Ok(Some(status)) => {
if preserve_status {
let exit_code = status.code().unwrap_or_else(|| {
status.signal().unwrap_or_else(|| {
ExitStatus::TimeoutFailed.into()
})
});
Ok(exit_code)
} else {
Ok(ExitStatus::CommandTimedOut.into())
}
}
Ok(None) => {
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()),
}
}
#[cfg(unix)]
fn preserve_signal_info(signal: libc::c_int) -> libc::c_int {
if let Some(sig) = signal_from_raw(signal) {
let _ = kill_process(getpid(), sig);
}
signal
}
#[cfg(not(unix))]
fn preserve_signal_info(signal: libc::c_int) -> libc::c_int {
signal
}
fn timeout(
cmd: &[String],
duration: Duration,
signal: usize,
kill_after: Option<Duration>,
foreground: bool,
preserve_status: bool,
verbose: bool,
) -> UResult<()> {
if !foreground {
let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0));
}
let mut cmd_builder = process::Command::new(&cmd[0]);
cmd_builder
.args(&cmd[1..])
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
#[cfg(unix)]
{
#[cfg(target_os = "linux")]
let death_sig = signal_from_raw(signal as i32);
let sigpipe_was_ignored = uucore::signals::sigpipe_was_ignored();
let stdin_was_closed = uucore::signals::stdin_was_closed();
unsafe {
cmd_builder.pre_exec(move || {
let _ = libc::signal(Signal::as_raw(Signal::TTIN), libc::SIG_DFL);
let _ = libc::signal(Signal::as_raw(Signal::TTOU), libc::SIG_DFL);
if sigpipe_was_ignored {
let _ = libc::signal(Signal::as_raw(Signal::PIPE), libc::SIG_IGN);
}
if stdin_was_closed {
libc::close(libc::STDIN_FILENO);
}
#[cfg(target_os = "linux")]
let _ = rustix::process::set_parent_process_death_signal(death_sig);
Ok(())
});
}
}
install_sigchld();
install_signal_handlers(signal);
let process = &mut cmd_builder.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),
)
})?;
match process.wait_or_timeout(duration, Some(&SIGNALED)) {
Ok(Some(status)) => {
let exit_code = status.code().unwrap_or_else(|| {
status
.signal()
.map_or_else(|| ExitStatus::TimeoutFailed.into(), preserve_signal_info)
});
Err(exit_code.into())
}
Ok(None) => {
let received_sig = RECEIVED_SIGNAL.load(atomic::Ordering::Relaxed);
let is_external_signal = received_sig > 0 && received_sig != libc::SIGALRM;
let signal_to_send = if is_external_signal {
received_sig as usize
} else {
signal
};
report_if_verbose(signal_to_send, &cmd[0], verbose);
send_signal(process, signal_to_send, foreground);
if let Some(kill_after) = kill_after {
return match wait_or_kill_process(
process,
&cmd[0],
kill_after,
preserve_status,
foreground,
verbose,
) {
Ok(status) => Err(status.into()),
Err(e) => Err(USimpleError::new(
ExitStatus::TimeoutFailed.into(),
e.to_string(),
)),
};
}
let status = process.wait()?;
if is_external_signal {
Err(ExitStatus::SignalSent(received_sig as usize).into())
} else if SIGNALED.load(atomic::Ordering::Relaxed) {
Err(ExitStatus::CommandTimedOut.into())
} else if preserve_status {
Err(status
.code()
.or_else(|| {
status
.signal()
.map(|s| ExitStatus::SignalSent(s as usize).into())
})
.unwrap_or(ExitStatus::CommandTimedOut.into())
.into())
} else {
Err(ExitStatus::CommandTimedOut.into())
}
}
Err(_) => {
send_signal(process, signal, foreground);
Err(ExitStatus::TimeoutFailed.into())
}
}
}