Skip to main content

uu_timeout/
timeout.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5
6// spell-checker:ignore (ToDO) tstr sigstr cmdname setpgid sigchld getpid
7
8mod status;
9
10use crate::status::ExitStatus;
11use clap::{Arg, ArgAction, Command};
12use std::io::{ErrorKind, Write};
13use std::os::unix::process::ExitStatusExt;
14use std::process::{self, Child, Stdio};
15use std::sync::atomic::{self, AtomicBool};
16use std::time::Duration;
17use uucore::display::Quotable;
18use uucore::error::{UResult, USimpleError, UUsageError};
19use uucore::parser::parse_time;
20use uucore::process::ChildExt;
21use uucore::translate;
22
23use uucore::{
24    format_usage,
25    signals::{signal_by_name_or_value, signal_name_by_value},
26};
27
28use nix::sys::signal::{SigHandler, Signal, kill};
29use nix::unistd::{Pid, getpid, setpgid};
30#[cfg(unix)]
31use std::os::unix::process::CommandExt;
32
33pub mod options {
34    pub static FOREGROUND: &str = "foreground";
35    pub static KILL_AFTER: &str = "kill-after";
36    pub static SIGNAL: &str = "signal";
37    pub static PRESERVE_STATUS: &str = "preserve-status";
38    pub static VERBOSE: &str = "verbose";
39
40    // Positional args.
41    pub static DURATION: &str = "duration";
42    pub static COMMAND: &str = "command";
43}
44
45struct Config {
46    foreground: bool,
47    kill_after: Option<Duration>,
48    signal: usize,
49    duration: Duration,
50    preserve_status: bool,
51    verbose: bool,
52
53    command: Vec<String>,
54}
55
56impl Config {
57    fn from(options: &clap::ArgMatches) -> UResult<Self> {
58        let signal = match options.get_one::<String>(options::SIGNAL) {
59            Some(signal_) => {
60                let signal_result = signal_by_name_or_value(signal_);
61                match signal_result {
62                    None => {
63                        return Err(UUsageError::new(
64                            ExitStatus::TimeoutFailed.into(),
65                            translate!("timeout-error-invalid-signal", "signal" => signal_.quote()),
66                        ));
67                    }
68                    Some(signal_value) => signal_value,
69                }
70            }
71            _ => signal_by_name_or_value("TERM").unwrap(),
72        };
73
74        let kill_after = match options.get_one::<String>(options::KILL_AFTER) {
75            None => None,
76            Some(kill_after) => match parse_time::from_str(kill_after, true) {
77                Ok(k) => Some(k),
78                Err(err) => return Err(UUsageError::new(ExitStatus::TimeoutFailed.into(), err)),
79            },
80        };
81
82        let duration =
83            parse_time::from_str(options.get_one::<String>(options::DURATION).unwrap(), true)
84                .map_err(|err| UUsageError::new(ExitStatus::TimeoutFailed.into(), err))?;
85
86        let preserve_status: bool = options.get_flag(options::PRESERVE_STATUS);
87        let foreground = options.get_flag(options::FOREGROUND);
88        let verbose = options.get_flag(options::VERBOSE);
89
90        let command = options
91            .get_many::<String>(options::COMMAND)
92            .unwrap()
93            .map(String::from)
94            .collect::<Vec<_>>();
95
96        Ok(Self {
97            foreground,
98            kill_after,
99            signal,
100            duration,
101            preserve_status,
102            verbose,
103            command,
104        })
105    }
106}
107
108#[uucore::main]
109pub fn uumain(args: impl uucore::Args) -> UResult<()> {
110    let matches =
111        uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), args, 125)?;
112
113    let config = Config::from(&matches)?;
114    timeout(
115        &config.command,
116        config.duration,
117        config.signal,
118        config.kill_after,
119        config.foreground,
120        config.preserve_status,
121        config.verbose,
122    )
123}
124
125pub fn uu_app() -> Command {
126    Command::new("timeout")
127        .version(uucore::crate_version!())
128        .help_template(uucore::localized_help_template(uucore::util_name()))
129        .about(translate!("timeout-about"))
130        .override_usage(format_usage(&translate!("timeout-usage")))
131        .arg(
132            Arg::new(options::FOREGROUND)
133                .long(options::FOREGROUND)
134                .short('f')
135                .help(translate!("timeout-help-foreground"))
136                .action(ArgAction::SetTrue),
137        )
138        .arg(
139            Arg::new(options::KILL_AFTER)
140                .long(options::KILL_AFTER)
141                .short('k')
142                .help(translate!("timeout-help-kill-after")),
143        )
144        .arg(
145            Arg::new(options::PRESERVE_STATUS)
146                .long(options::PRESERVE_STATUS)
147                .short('p')
148                .help(translate!("timeout-help-preserve-status"))
149                .action(ArgAction::SetTrue),
150        )
151        .arg(
152            Arg::new(options::SIGNAL)
153                .short('s')
154                .long(options::SIGNAL)
155                .help(translate!("timeout-help-signal"))
156                .value_name("SIGNAL"),
157        )
158        .arg(
159            Arg::new(options::VERBOSE)
160                .short('v')
161                .long(options::VERBOSE)
162                .help(translate!("timeout-help-verbose"))
163                .action(ArgAction::SetTrue),
164        )
165        .arg(
166            Arg::new(options::DURATION)
167                .required(true)
168                .help(translate!("timeout-help-duration")),
169        )
170        .arg(
171            Arg::new(options::COMMAND)
172                .required(true)
173                .action(ArgAction::Append)
174                .help(translate!("timeout-help-command"))
175                .value_hint(clap::ValueHint::CommandName),
176        )
177        .trailing_var_arg(true)
178        .infer_long_args(true)
179        .after_help(translate!("timeout-after-help"))
180}
181
182/// Install SIGCHLD handler to ensure waiting for child works even if parent ignored SIGCHLD.
183fn install_sigchld() {
184    extern "C" fn chld(_: libc::c_int) {}
185    let _ = unsafe { nix::sys::signal::signal(Signal::SIGCHLD, SigHandler::Handler(chld)) };
186}
187
188/// We should terminate child process when receiving termination signals.
189static SIGNALED: AtomicBool = AtomicBool::new(false);
190/// Track which signal was received (0 = none/timeout expired naturally).
191static RECEIVED_SIGNAL: atomic::AtomicI32 = atomic::AtomicI32::new(0);
192
193/// Install signal handlers for termination signals.
194fn install_signal_handlers(term_signal: usize) {
195    extern "C" fn handle_signal(sig: libc::c_int) {
196        SIGNALED.store(true, atomic::Ordering::Relaxed);
197        RECEIVED_SIGNAL.store(sig, atomic::Ordering::Relaxed);
198    }
199
200    let handler = SigHandler::Handler(handle_signal);
201    let sigpipe_ignored = uucore::signals::sigpipe_was_ignored();
202
203    for sig in [
204        Signal::SIGALRM,
205        Signal::SIGINT,
206        Signal::SIGQUIT,
207        Signal::SIGHUP,
208        Signal::SIGTERM,
209        Signal::SIGPIPE,
210        Signal::SIGUSR1,
211        Signal::SIGUSR2,
212    ] {
213        if sig == Signal::SIGPIPE && sigpipe_ignored {
214            continue; // Skip SIGPIPE if it was ignored by parent
215        }
216        let _ = unsafe { nix::sys::signal::signal(sig, handler) };
217    }
218
219    if let Ok(sig) = Signal::try_from(term_signal as i32) {
220        let _ = unsafe { nix::sys::signal::signal(sig, handler) };
221    }
222}
223
224/// Report that a signal is being sent if the verbose flag is set.
225fn report_if_verbose(signal: usize, cmd: &str, verbose: bool) {
226    if verbose {
227        let s = if signal == 0 {
228            "0".to_string()
229        } else {
230            signal_name_by_value(signal).unwrap().to_string()
231        };
232        let mut stderr = std::io::stderr();
233        let _ = writeln!(
234            stderr,
235            "timeout: {}",
236            translate!("timeout-verbose-sending-signal", "signal" => s, "command" => cmd.quote())
237        );
238        let _ = stderr.flush();
239    }
240}
241
242fn send_signal(process: &mut Child, signal: usize, foreground: bool) {
243    // NOTE: GNU timeout doesn't check for errors of signal.
244    // The subprocess might have exited just after the timeout.
245    let _ = process.send_signal(signal);
246    if signal == 0 || foreground {
247        return;
248    }
249    let _ = process.send_signal_group(signal);
250    let kill_signal = signal_by_name_or_value("KILL").unwrap();
251    let continued_signal = signal_by_name_or_value("CONT").unwrap();
252    if signal != kill_signal && signal != continued_signal {
253        let _ = process.send_signal(continued_signal);
254        let _ = process.send_signal_group(continued_signal);
255    }
256}
257
258/// Wait for a child process and send a kill signal if it does not terminate.
259///
260/// This function waits for the child `process` for the time period
261/// given by `duration`. If the child process does not terminate
262/// within that time, we send the `SIGKILL` signal to it. If `verbose`
263/// is `true`, then a message is printed to `stderr` when that
264/// happens.
265///
266/// If the child process terminates within the given time period and
267/// `preserve_status` is `true`, then the status code of the child
268/// process is returned. If the child process terminates within the
269/// given time period and `preserve_status` is `false`, then 124 is
270/// returned. If the child does not terminate within the time period,
271/// then 137 is returned. Finally, if there is an error while waiting
272/// for the child process to terminate, then 124 is returned.
273///
274/// # Errors
275///
276/// If there is a problem sending the `SIGKILL` signal or waiting for
277/// the process after that signal is sent.
278fn wait_or_kill_process(
279    process: &mut Child,
280    cmd: &str,
281    duration: Duration,
282    preserve_status: bool,
283    foreground: bool,
284    verbose: bool,
285) -> std::io::Result<i32> {
286    // ignore `SIGTERM` here
287    match process.wait_or_timeout(duration, None) {
288        Ok(Some(status)) => {
289            if preserve_status {
290                let exit_code = status.code().unwrap_or_else(|| {
291                    status.signal().unwrap_or_else(|| {
292                        // Extremely rare: process exited but we have neither exit code nor signal.
293                        // This can happen on some platforms or in unusual termination scenarios.
294                        ExitStatus::TimeoutFailed.into()
295                    })
296                });
297                Ok(exit_code)
298            } else {
299                Ok(ExitStatus::TimeoutFailed.into())
300            }
301        }
302        Ok(None) => {
303            let signal = signal_by_name_or_value("KILL").unwrap();
304            report_if_verbose(signal, cmd, verbose);
305            send_signal(process, signal, foreground);
306            process.wait()?;
307            Ok(ExitStatus::SignalSent(signal).into())
308        }
309        Err(_) => Ok(ExitStatus::CommandTimedOut.into()),
310    }
311}
312
313#[cfg(unix)]
314fn preserve_signal_info(signal: libc::c_int) -> libc::c_int {
315    // This is needed because timeout is expected to preserve the exit
316    // status of its child. It is not the case that utilities have a
317    // single simple exit code, that's an illusion some shells
318    // provide.  Instead exit status is really two numbers:
319    //
320    //  - An exit code if the program ran to completion
321    //
322    //  - A signal number if the program was terminated by a signal
323    //
324    // The easiest way to preserve the latter seems to be to kill
325    // ourselves with whatever signal our child exited with, which is
326    // what the following is intended to accomplish.
327    if let Ok(sig) = Signal::try_from(signal) {
328        let _ = kill(getpid(), Some(sig));
329    }
330    signal
331}
332
333#[cfg(not(unix))]
334fn preserve_signal_info(signal: libc::c_int) -> libc::c_int {
335    // Do nothing
336    signal
337}
338
339fn timeout(
340    cmd: &[String],
341    duration: Duration,
342    signal: usize,
343    kill_after: Option<Duration>,
344    foreground: bool,
345    preserve_status: bool,
346    verbose: bool,
347) -> UResult<()> {
348    if !foreground {
349        let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0));
350    }
351
352    let mut cmd_builder = process::Command::new(&cmd[0]);
353    cmd_builder
354        .args(&cmd[1..])
355        .stdin(Stdio::inherit())
356        .stdout(Stdio::inherit())
357        .stderr(Stdio::inherit());
358
359    #[cfg(unix)]
360    {
361        #[cfg(target_os = "linux")]
362        let death_sig = Signal::try_from(signal as i32).ok();
363        let sigpipe_was_ignored = uucore::signals::sigpipe_was_ignored();
364        let stdin_was_closed = uucore::signals::stdin_was_closed();
365
366        unsafe {
367            cmd_builder.pre_exec(move || {
368                // Reset terminal signals to default
369                let _ = nix::sys::signal::signal(Signal::SIGTTIN, SigHandler::SigDfl);
370                let _ = nix::sys::signal::signal(Signal::SIGTTOU, SigHandler::SigDfl);
371                // Preserve SIGPIPE ignore status if parent had it ignored
372                if sigpipe_was_ignored {
373                    let _ = nix::sys::signal::signal(Signal::SIGPIPE, SigHandler::SigIgn);
374                }
375                // If stdin was closed before Rust reopened it as /dev/null, close it in child
376                if stdin_was_closed {
377                    libc::close(libc::STDIN_FILENO);
378                }
379                #[cfg(target_os = "linux")]
380                if let Some(sig) = death_sig {
381                    let _ = nix::sys::prctl::set_pdeathsig(sig);
382                }
383                Ok(())
384            });
385        }
386    }
387
388    install_sigchld();
389    install_signal_handlers(signal);
390
391    let process = &mut cmd_builder.spawn().map_err(|err| {
392        let status_code = match err.kind() {
393            ErrorKind::NotFound => ExitStatus::CommandNotFound.into(),
394            ErrorKind::PermissionDenied => ExitStatus::CannotInvoke.into(),
395            _ => ExitStatus::CannotInvoke.into(),
396        };
397        USimpleError::new(
398            status_code,
399            translate!("timeout-error-failed-to-execute-process", "error" => err),
400        )
401    })?;
402
403    // Wait for the child process for the specified time period.
404    //
405    // If the process exits within the specified time period (the
406    // `Ok(Some(_))` arm), then return the appropriate status code.
407    //
408    // If the process does not exit within that time (the `Ok(None)`
409    // arm) and `kill_after` is specified, then try sending `SIGKILL`.
410    //
411    // TODO The structure of this block is extremely similar to the
412    // structure of `wait_or_kill_process()`. They can probably be
413    // refactored into some common function.
414    match process.wait_or_timeout(duration, Some(&SIGNALED)) {
415        Ok(Some(status)) => {
416            let exit_code = status.code().unwrap_or_else(|| {
417                status
418                    .signal()
419                    .map_or_else(|| ExitStatus::TimeoutFailed.into(), preserve_signal_info)
420            });
421            Err(exit_code.into())
422        }
423        Ok(None) => {
424            let received_sig = RECEIVED_SIGNAL.load(atomic::Ordering::Relaxed);
425            let is_external_signal = received_sig > 0 && received_sig != libc::SIGALRM;
426            let signal_to_send = if is_external_signal {
427                received_sig as usize
428            } else {
429                signal
430            };
431
432            report_if_verbose(signal_to_send, &cmd[0], verbose);
433            send_signal(process, signal_to_send, foreground);
434
435            if let Some(kill_after) = kill_after {
436                return match wait_or_kill_process(
437                    process,
438                    &cmd[0],
439                    kill_after,
440                    preserve_status,
441                    foreground,
442                    verbose,
443                ) {
444                    Ok(status) => Err(status.into()),
445                    Err(e) => Err(USimpleError::new(
446                        ExitStatus::TimeoutFailed.into(),
447                        e.to_string(),
448                    )),
449                };
450            }
451
452            let status = process.wait()?;
453            if is_external_signal {
454                Err(ExitStatus::SignalSent(received_sig as usize).into())
455            } else if SIGNALED.load(atomic::Ordering::Relaxed) {
456                Err(ExitStatus::CommandTimedOut.into())
457            } else if preserve_status {
458                Err(status
459                    .code()
460                    .or_else(|| {
461                        status
462                            .signal()
463                            .map(|s| ExitStatus::SignalSent(s as usize).into())
464                    })
465                    .unwrap_or(ExitStatus::CommandTimedOut.into())
466                    .into())
467            } else {
468                Err(ExitStatus::CommandTimedOut.into())
469            }
470        }
471        Err(_) => {
472            // We're going to return ERR_EXIT_STATUS regardless of
473            // whether `send_signal()` succeeds or fails
474            send_signal(process, signal, foreground);
475            Err(ExitStatus::TimeoutFailed.into())
476        }
477    }
478}