use std::io::Read;
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
pub const LIMIT: Duration = Duration::from_secs(20);
#[derive(Debug, Clone, Default)]
pub struct Ran {
pub success: bool,
pub stdout: String,
pub stderr: String,
}
pub fn run(command: &mut Command, limit: Duration) -> Option<Ran> {
let mut child = command
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.ok()?;
let deadline = Instant::now() + limit;
let (tx, rx) = mpsc::channel();
read_detached(child.stdout.take(), 0, tx.clone());
read_detached(child.stderr.take(), 1, tx);
let status = loop {
match child.try_wait() {
Ok(Some(status)) => break status,
Ok(None) if Instant::now() < deadline => thread::sleep(Duration::from_millis(5)),
_ => {
let _ = child.kill();
let _ = child.wait();
return None;
}
}
};
let mut ran = Ran {
success: status.success(),
..Ran::default()
};
for _ in 0..2 {
let wait = deadline
.saturating_duration_since(Instant::now())
.max(Duration::from_millis(200));
match rx.recv_timeout(wait) {
Ok((0, bytes)) => ran.stdout = String::from_utf8_lossy(&bytes).into_owned(),
Ok((_, bytes)) => ran.stderr = String::from_utf8_lossy(&bytes).into_owned(),
Err(_) => break,
}
}
Some(ran)
}
fn read_detached<R: Read + Send + 'static>(
pipe: Option<R>,
stream: u8,
tx: mpsc::Sender<(u8, Vec<u8>)>,
) {
thread::spawn(move || {
let mut bytes = Vec::new();
if let Some(mut pipe) = pipe {
let _ = pipe.read_to_end(&mut bytes);
}
let _ = tx.send((stream, bytes));
});
}
pub fn stdout(command: &mut Command) -> Option<String> {
run(command, LIMIT).filter(|r| r.success).map(|r| r.stdout)
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
#[test]
fn captures_output_and_gives_up_on_slow_commands() {
let ran = run(
Command::new("sh").args(["-c", "echo out; echo err >&2; exit 3"]),
LIMIT,
)
.unwrap();
assert!(!ran.success);
assert_eq!(ran.stdout.trim(), "out");
assert_eq!(ran.stderr.trim(), "err");
let started = Instant::now();
assert!(run(Command::new("sleep").arg("5"), Duration::from_millis(100)).is_none());
assert!(started.elapsed() < Duration::from_secs(4));
assert!(stdout(&mut Command::new("/nonexistent/program")).is_none());
}
#[cfg(unix)]
#[test]
fn a_daemon_holding_the_pipes_doesnt_hold_up_the_result() {
let started = Instant::now();
let ran = run(
Command::new("sh").args(["-c", "sleep 3 & exit 0"]),
Duration::from_millis(500),
)
.unwrap();
assert!(ran.success);
assert!(ran.stdout.is_empty() && ran.stderr.is_empty());
assert!(started.elapsed() < Duration::from_secs(2));
}
}