1mod 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 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
182fn install_sigchld() {
184 extern "C" fn chld(_: libc::c_int) {}
185 let _ = unsafe { nix::sys::signal::signal(Signal::SIGCHLD, SigHandler::Handler(chld)) };
186}
187
188static SIGNALED: AtomicBool = AtomicBool::new(false);
190static RECEIVED_SIGNAL: atomic::AtomicI32 = atomic::AtomicI32::new(0);
192
193fn 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; }
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
224fn 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 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
258fn 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 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 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 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 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 let _ = nix::sys::signal::signal(Signal::SIGTTIN, SigHandler::SigDfl);
370 let _ = nix::sys::signal::signal(Signal::SIGTTOU, SigHandler::SigDfl);
371 if sigpipe_was_ignored {
373 let _ = nix::sys::signal::signal(Signal::SIGPIPE, SigHandler::SigIgn);
374 }
375 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 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 send_signal(process, signal, foreground);
475 Err(ExitStatus::TimeoutFailed.into())
476 }
477 }
478}