Skip to content

Commit 228824e

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

File tree

5 files changed

+131
-25
lines changed

5 files changed

+131
-25
lines changed

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: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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,18 @@ 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+
let mut stderr = std::io::stderr();
239+
let _ = writeln!(
240+
stderr,
241+
"timeout: {}",
242+
translate!("timeout-verbose-sending-signal", "signal" => s, "command" => cmd.quote())
243+
);
244+
let _ = stderr.flush();
229245
}
230246
}
231247

@@ -361,6 +377,9 @@ fn timeout(
361377
}
362378
}
363379

380+
install_sigchld();
381+
install_signal_handlers(signal);
382+
364383
let process = &mut cmd_builder.spawn().map_err(|err| {
365384
let status_code = match err.kind() {
366385
ErrorKind::NotFound => ExitStatus::CommandNotFound.into(),
@@ -372,8 +391,6 @@ fn timeout(
372391
translate!("timeout-error-failed-to-execute-process", "error" => err),
373392
)
374393
})?;
375-
install_sigchld();
376-
install_signal_handlers(signal);
377394

378395
match process.wait_or_timeout(duration, Some(&SIGNALED)) {
379396
Ok(Some(status)) => Err(status
@@ -383,15 +400,29 @@ fn timeout(
383400
Ok(None) => {
384401
let received_sig = RECEIVED_SIGNAL.load(atomic::Ordering::Relaxed);
385402
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 };
403+
let signal_to_send = if is_external_signal {
404+
received_sig as usize
405+
} else {
406+
signal
407+
};
387408

388409
report_if_verbose(signal_to_send, &cmd[0], verbose);
389410
send_signal(process, signal_to_send, foreground);
390411

391412
if let Some(kill_after) = kill_after {
392-
return match wait_or_kill_process(process, &cmd[0], kill_after, preserve_status, foreground, verbose) {
413+
return match wait_or_kill_process(
414+
process,
415+
&cmd[0],
416+
kill_after,
417+
preserve_status,
418+
foreground,
419+
verbose,
420+
) {
393421
Ok(status) => Err(status.into()),
394-
Err(e) => Err(USimpleError::new(ExitStatus::TimeoutFailed.into(), e.to_string())),
422+
Err(e) => Err(USimpleError::new(
423+
ExitStatus::TimeoutFailed.into(),
424+
e.to_string(),
425+
)),
395426
};
396427
}
397428

@@ -401,8 +432,13 @@ fn timeout(
401432
} else if SIGNALED.load(atomic::Ordering::Relaxed) {
402433
Err(ExitStatus::CommandTimedOut.into())
403434
} else if preserve_status {
404-
Err(status.code()
405-
.or_else(|| status.signal().map(|s| ExitStatus::SignalSent(s as usize).into()))
435+
Err(status
436+
.code()
437+
.or_else(|| {
438+
status
439+
.signal()
440+
.map(|s| ExitStatus::SignalSent(s as usize).into())
441+
})
406442
.unwrap_or(ExitStatus::CommandTimedOut.into())
407443
.into())
408444
} 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)