Skip to content

Commit 93ea128

Browse files
committed
tests/timeout: add tests for signal handling and update verbose output
1 parent bf9f733 commit 93ea128

5 files changed

Lines changed: 130 additions & 27 deletions

File tree

src/uu/timeout/src/status.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ pub(crate) enum ExitStatus {
3333

3434
/// When a signal is sent to the child process or `timeout` itself.
3535
SignalSent(usize),
36-
37-
/// When `SIGTERM` signal received.
38-
Terminated,
3936
}
4037

4138
impl From<ExitStatus> for i32 {
@@ -46,7 +43,6 @@ impl From<ExitStatus> for i32 {
4643
ExitStatus::CannotInvoke => 126,
4744
ExitStatus::CommandNotFound => 127,
4845
ExitStatus::SignalSent(s) => 128 + s as Self,
49-
ExitStatus::Terminated => 143,
5046
}
5147
}
5248
}

src/uu/timeout/src/timeout.rs

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ mod status;
88

99
use crate::status::ExitStatus;
1010
use clap::{Arg, ArgAction, Command};
11-
use std::io::{ErrorKind, Write};
11+
use std::io::ErrorKind;
1212
use std::os::unix::process::ExitStatusExt;
1313
use std::process::{self, Child, Stdio};
1414
use std::sync::atomic::{self, AtomicBool};
@@ -20,7 +20,7 @@ use uucore::process::ChildExt;
2020
use uucore::translate;
2121

2222
use uucore::{
23-
format_usage,
23+
format_usage, show_error,
2424
signals::{signal_by_name_or_value, signal_name_by_value},
2525
};
2626

@@ -208,8 +208,14 @@ fn install_signal_handlers(term_signal: usize) {
208208
let sigpipe_ignored = uucore::signals::sigpipe_was_ignored();
209209

210210
for sig in [
211-
Signal::SIGALRM, Signal::SIGINT, Signal::SIGQUIT, Signal::SIGHUP,
212-
Signal::SIGTERM, Signal::SIGPIPE, Signal::SIGUSR1, Signal::SIGUSR2,
211+
Signal::SIGALRM,
212+
Signal::SIGINT,
213+
Signal::SIGQUIT,
214+
Signal::SIGHUP,
215+
Signal::SIGTERM,
216+
Signal::SIGPIPE,
217+
Signal::SIGUSR1,
218+
Signal::SIGUSR2,
213219
] {
214220
if sig != Signal::SIGPIPE || !sigpipe_ignored {
215221
let _ = unsafe { nix::sys::signal::signal(sig, handler) };
@@ -224,8 +230,15 @@ fn install_signal_handlers(term_signal: usize) {
224230
/// Report that a signal is being sent if the verbose flag is set.
225231
fn report_if_verbose(signal: usize, cmd: &str, verbose: bool) {
226232
if verbose {
227-
let s = if signal == 0 { "0".to_string() } else { signal_name_by_value(signal).unwrap().to_string() };
228-
let _ = writeln!(std::io::stderr(), "timeout: sending signal {} to command {}", s, cmd.quote());
233+
let s = if signal == 0 {
234+
"0".to_string()
235+
} else {
236+
signal_name_by_value(signal).unwrap().to_string()
237+
};
238+
show_error!(
239+
"{}",
240+
translate!("timeout-verbose-sending-signal", "signal" => s, "command" => cmd.quote())
241+
);
229242
}
230243
}
231244

@@ -361,6 +374,9 @@ fn timeout(
361374
}
362375
}
363376

377+
install_sigchld();
378+
install_signal_handlers(signal);
379+
364380
let process = &mut cmd_builder.spawn().map_err(|err| {
365381
let status_code = match err.kind() {
366382
ErrorKind::NotFound => ExitStatus::CommandNotFound.into(),
@@ -372,8 +388,6 @@ fn timeout(
372388
translate!("timeout-error-failed-to-execute-process", "error" => err),
373389
)
374390
})?;
375-
install_sigchld();
376-
install_signal_handlers(signal);
377391

378392
match process.wait_or_timeout(duration, Some(&SIGNALED)) {
379393
Ok(Some(status)) => Err(status
@@ -383,15 +397,29 @@ fn timeout(
383397
Ok(None) => {
384398
let received_sig = RECEIVED_SIGNAL.load(atomic::Ordering::Relaxed);
385399
let is_external_signal = received_sig > 0 && received_sig != libc::SIGALRM;
386-
let signal_to_send = if is_external_signal { received_sig as usize } else { signal };
400+
let signal_to_send = if is_external_signal {
401+
received_sig as usize
402+
} else {
403+
signal
404+
};
387405

388406
report_if_verbose(signal_to_send, &cmd[0], verbose);
389407
send_signal(process, signal_to_send, foreground);
390408

391409
if let Some(kill_after) = kill_after {
392-
return match wait_or_kill_process(process, &cmd[0], kill_after, preserve_status, foreground, verbose) {
410+
return match wait_or_kill_process(
411+
process,
412+
&cmd[0],
413+
kill_after,
414+
preserve_status,
415+
foreground,
416+
verbose,
417+
) {
393418
Ok(status) => Err(status.into()),
394-
Err(e) => Err(USimpleError::new(ExitStatus::TimeoutFailed.into(), e.to_string())),
419+
Err(e) => Err(USimpleError::new(
420+
ExitStatus::TimeoutFailed.into(),
421+
e.to_string(),
422+
)),
395423
};
396424
}
397425

@@ -401,8 +429,13 @@ fn timeout(
401429
} else if SIGNALED.load(atomic::Ordering::Relaxed) {
402430
Err(ExitStatus::CommandTimedOut.into())
403431
} else if preserve_status {
404-
Err(status.code()
405-
.or_else(|| status.signal().map(|s| ExitStatus::SignalSent(s as usize).into()))
432+
Err(status
433+
.code()
434+
.or_else(|| {
435+
status
436+
.signal()
437+
.map(|s| ExitStatus::SignalSent(s as usize).into())
438+
})
406439
.unwrap_or(ExitStatus::CommandTimedOut.into())
407440
.into())
408441
} else {

src/uu/yes/src/yes.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,22 @@ use std::ffi::OsString;
1111
use std::io::{self, Write};
1212
use uucore::error::{UResult, USimpleError};
1313
use uucore::format_usage;
14-
#[cfg(unix)]
15-
use uucore::signals::enable_pipe_errors;
1614
use uucore::translate;
1715

1816
// it's possible that using a smaller or larger buffer might provide better performance on some
1917
// systems, but honestly this is good enough
2018
const BUF_SIZE: usize = 16 * 1024;
2119

20+
#[cfg(unix)]
21+
uucore::init_sigpipe_capture!();
22+
2223
#[uucore::main]
2324
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
25+
#[cfg(unix)]
26+
if !uucore::signals::sigpipe_was_ignored() {
27+
let _ = uucore::signals::enable_pipe_errors();
28+
}
29+
2430
let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
2531

2632
let mut buffer = Vec::with_capacity(BUF_SIZE);
@@ -29,7 +35,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
2935

3036
match exec(&buffer) {
3137
Ok(()) => Ok(()),
32-
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => Ok(()),
38+
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => {
39+
#[cfg(unix)]
40+
if uucore::signals::sigpipe_was_ignored() {
41+
uucore::show_error!(
42+
"{}",
43+
translate!("yes-error-standard-output", "error" => err)
44+
);
45+
}
46+
Ok(())
47+
}
3348
Err(err) => Err(USimpleError::new(
3449
1,
3550
translate!("yes-error-standard-output", "error" => err),
@@ -113,8 +128,6 @@ fn prepare_buffer(buf: &mut Vec<u8>) {
113128
pub fn exec(bytes: &[u8]) -> io::Result<()> {
114129
let stdout = io::stdout();
115130
let mut stdout = stdout.lock();
116-
#[cfg(unix)]
117-
enable_pipe_errors()?;
118131

119132
loop {
120133
stdout.write_all(bytes)?;

src/uucore/src/lib/features/process.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,29 @@ impl ChildExt for Child {
107107
}
108108

109109
fn send_signal_group(&mut self, signal: usize) -> io::Result<()> {
110-
// Ignore the signal, so we don't go into a signal loop.
111-
if unsafe { libc::signal(signal as i32, libc::SIG_IGN) } == usize::MAX {
112-
return Err(io::Error::last_os_error());
110+
// Send signal to our process group (group 0 = caller's group).
111+
// This matches GNU coreutils behavior: if the child has remained in our
112+
// process group, it will receive this signal along with all other processes
113+
// in the group. If the child has created its own process group (via setpgid),
114+
// it won't receive this group signal, but will have received the direct signal.
115+
116+
// Signal 0 is special - it just checks if process exists, doesn't send anything.
117+
// No need to manipulate signal handlers for it.
118+
if signal == 0 {
119+
let result = unsafe { libc::kill(0, 0) };
120+
return if result == 0 {
121+
Ok(())
122+
} else {
123+
Err(io::Error::last_os_error())
124+
};
113125
}
114-
if unsafe { libc::kill(0, signal as i32) } == 0 {
126+
127+
// Ignore the signal temporarily so we don't receive it ourselves.
128+
let old_handler = unsafe { libc::signal(signal as i32, libc::SIG_IGN) };
129+
let result = unsafe { libc::kill(0, signal as i32) };
130+
// Restore the old handler
131+
unsafe { libc::signal(signal as i32, old_handler) };
132+
if result == 0 {
115133
Ok(())
116134
} else {
117135
Err(io::Error::last_os_error())

tests/by-util/test_timeout.rs

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ fn test_verbose() {
5858
new_ucmd!()
5959
.args(&[verbose_flag, "-s0", "-k.1", ".1", "sleep", "1"])
6060
.fails()
61-
.stderr_only("timeout: sending signal EXIT to command 'sleep'\ntimeout: sending signal KILL to command 'sleep'\n");
61+
.stderr_only("timeout: sending signal 0 to command 'sleep'\ntimeout: sending signal KILL to command 'sleep'\n");
6262
}
6363
}
6464

@@ -235,3 +235,46 @@ fn test_command_cannot_invoke() {
235235
// Try to execute a directory (should give permission denied or similar)
236236
new_ucmd!().args(&["1", "/"]).fails_with_code(126);
237237
}
238+
239+
#[test]
240+
#[cfg(unix)]
241+
fn test_sigchld_ignored_by_parent() {
242+
new_ucmd!()
243+
.args(&["10", "sh", "-c", "trap '' CHLD; exec timeout 1 true"])
244+
.succeeds();
245+
}
246+
247+
#[test]
248+
#[cfg(unix)]
249+
fn test_with_background_child() {
250+
new_ucmd!()
251+
.args(&[".5", "sh", "-c", "sleep .1 & sleep 2"])
252+
.fails_with_code(124)
253+
.no_stdout();
254+
}
255+
256+
#[test]
257+
#[cfg(unix)]
258+
fn test_forward_sigint_to_child() {
259+
let mut cmd = new_ucmd!()
260+
.args(&[
261+
"10",
262+
"sh",
263+
"-c",
264+
"trap 'echo got_int; exit 42' INT; sleep 5",
265+
])
266+
.run_no_wait();
267+
cmd.delay(100);
268+
cmd.kill_with_custom_signal(nix::sys::signal::Signal::SIGINT);
269+
cmd.make_assertion()
270+
.is_not_alive()
271+
.with_current_output()
272+
.stdout_contains("got_int");
273+
}
274+
275+
#[test]
276+
fn test_foreground_signal0_kill_after() {
277+
new_ucmd!()
278+
.args(&["--foreground", "-s0", "-k.1", ".1", "sleep", "10"])
279+
.fails_with_code(137);
280+
}

0 commit comments

Comments
 (0)