Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

timeout: fix subprocess is never terminated #4315

Merged
merged 2 commits into from
Mar 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions src/uu/timeout/src/timeout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,22 @@ fn report_if_verbose(signal: usize, cmd: &str, verbose: bool) {
}
}

fn send_signal(process: &mut Child, signal: usize, foreground: bool) {
// NOTE: GNU timeout doesn't check for errors of signal.
// The subprocess might have exited just after the timeout.
// Sending a signal now would return "No such process", but we should still try to kill the children.
_ = process.send_signal(signal);
if !foreground {
_ = 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 {
_ = process.send_signal(continued_signal);
_ = process.send_signal_group(continued_signal);
}
}
}

/// Wait for a child process and send a kill signal if it does not terminate.
///
/// This function waits for the child `process` for the time period
Expand All @@ -217,10 +233,11 @@ fn report_if_verbose(signal: usize, cmd: &str, verbose: bool) {
/// If there is a problem sending the `SIGKILL` signal or waiting for
/// the process after that signal is sent.
fn wait_or_kill_process(
mut process: Child,
process: &mut Child,
cmd: &str,
duration: Duration,
preserve_status: bool,
foreground: bool,
verbose: bool,
) -> std::io::Result<i32> {
match process.wait_or_timeout(duration) {
Expand All @@ -234,7 +251,7 @@ fn wait_or_kill_process(
Ok(None) => {
let signal = signal_by_name_or_value("KILL").unwrap();
report_if_verbose(signal, cmd, verbose);
process.send_signal(signal)?;
send_signal(process, signal, foreground);
process.wait()?;
Ok(ExitStatus::SignalSent(signal).into())
}
Expand Down Expand Up @@ -300,7 +317,7 @@ fn timeout(

enable_pipe_errors()?;

let mut process = process::Command::new(&cmd[0])
let process = &mut process::Command::new(&cmd[0])
.args(&cmd[1..])
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
Expand Down Expand Up @@ -335,7 +352,7 @@ fn timeout(
.into()),
Ok(None) => {
report_if_verbose(signal, &cmd[0], verbose);
process.send_signal(signal)?;
send_signal(process, signal, foreground);
match kill_after {
None => {
if preserve_status {
Expand All @@ -350,6 +367,7 @@ fn timeout(
&cmd[0],
kill_after,
preserve_status,
foreground,
verbose,
) {
Ok(status) => Err(status.into()),
Expand All @@ -363,11 +381,8 @@ fn timeout(
}
Err(_) => {
// We're going to return ERR_EXIT_STATUS regardless of
// whether `send_signal()` succeeds or fails, so just
// ignore the return value.
process
.send_signal(signal)
.map_err(|e| USimpleError::new(ExitStatus::TimeoutFailed.into(), format!("{e}")))?;
// whether `send_signal()` succeeds or fails
send_signal(process, signal, foreground);
Err(ExitStatus::TimeoutFailed.into())
}
}
Expand Down
15 changes: 15 additions & 0 deletions src/uucore/src/lib/features/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ pub trait ChildExt {
/// send the signal to an unrelated process that recycled the PID.
fn send_signal(&mut self, signal: usize) -> io::Result<()>;

/// Send a signal to a process group.
fn send_signal_group(&mut self, signal: usize) -> io::Result<()>;

/// Wait for a process to finish or return after the specified duration.
/// A `timeout` of zero disables the timeout.
fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>>;
Expand All @@ -62,6 +65,18 @@ impl ChildExt for Child {
}
}

fn send_signal_group(&mut self, signal: usize) -> io::Result<()> {
// Ignore the signal, so we don't go into a signal loop.
if unsafe { libc::signal(signal as i32, libc::SIG_IGN) } != 0 {
return Err(io::Error::last_os_error());
}
if unsafe { libc::kill(0, signal as i32) } != 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}

fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
if timeout == Duration::from_micros(0) {
return self.wait().map(Some);
Expand Down
16 changes: 16 additions & 0 deletions tests/by-util/test_timeout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,19 @@ fn test_kill_after_long() {
.no_stdout()
.no_stderr();
}

#[test]
fn test_kill_subprocess() {
new_ucmd!()
.args(&[
// Make sure the CI can spawn the subprocess.
"10",
"sh",
"-c",
"sh -c \"trap 'echo xyz' TERM; sleep 30\"",
])
.fails()
.code_is(124)
.stdout_contains("xyz")
.stderr_contains("Terminated");
}