use std::io::Read;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
pub const PERIODIC_TIMEOUT: Duration = Duration::from_millis(1500);
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
pub const DISCOVERY_TIMEOUT: Duration = Duration::from_secs(5);
pub fn run_with_timeout(program: &str, args: &[&str], timeout: Duration) -> Option<String> {
let mut child = Command::new(program)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.ok()?;
let mut stdout = child.stdout.take()?;
let reader = std::thread::spawn(move || {
let mut text = String::new();
let _ = stdout.read_to_string(&mut text);
text
});
let started = Instant::now();
loop {
match child.try_wait() {
Ok(Some(_)) => {
let _ = child.wait();
return reader.join().ok();
}
Ok(None) if started.elapsed() >= timeout => {
let _ = child.kill();
let _ = child.wait();
drop(reader);
return None;
}
Ok(None) => std::thread::sleep(Duration::from_millis(20)),
Err(_) => {
let _ = child.kill();
let _ = child.wait();
drop(reader);
return None;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn captures_stdout_of_a_fast_command() {
let out = run_with_timeout("echo", &["hello"], Duration::from_secs(5));
assert_eq!(out.as_deref().map(str::trim), Some("hello"));
}
#[test]
fn missing_program_is_none() {
assert!(run_with_timeout("syswatch-no-such-binary", &[], Duration::from_secs(1)).is_none());
}
#[test]
fn grandchild_holding_the_pipe_does_not_extend_the_deadline() {
let started = Instant::now();
let out = run_with_timeout(
"sh",
&["-c", "sleep 30; echo never"],
Duration::from_millis(300),
);
assert!(out.is_none());
assert!(
started.elapsed() < Duration::from_secs(5),
"took {:?}, waited on the grandchild's pipe",
started.elapsed()
);
}
#[test]
fn hung_command_is_killed_at_the_deadline() {
let started = Instant::now();
let out = run_with_timeout("sleep", &["30"], Duration::from_millis(300));
assert!(out.is_none());
assert!(
started.elapsed() < Duration::from_secs(5),
"took {:?}, the child was not killed",
started.elapsed()
);
}
}