use std::io::Read;
use std::process::{Child, Command, ExitStatus, Stdio};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use wait_timeout::ChildExt;
pub struct Captured {
pub status: Option<ExitStatus>,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
pub fn run_captured(
cmd: &mut Command,
timeout: Duration,
interactive_stdin: bool,
) -> std::io::Result<Captured> {
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
if interactive_stdin {
cmd.stdin(Stdio::inherit());
} else {
cmd.stdin(Stdio::null());
}
#[cfg(unix)]
let hand_over_tty = unix::prepare(cmd, interactive_stdin);
let mut child = cmd.spawn()?;
#[cfg(unix)]
if hand_over_tty {
unix::give_terminal_to(child.id());
}
let (out_buf, out_thread) = drain(child.stdout.take().expect("stdout piped"));
let (err_buf, err_thread) = drain(child.stderr.take().expect("stderr piped"));
let status = match child.wait_timeout(timeout)? {
Some(st) => Some(st),
None => {
kill_tree(&child);
let _ = child.wait();
None
}
};
#[cfg(unix)]
if hand_over_tty {
unix::take_terminal_back();
}
let deadline = Instant::now() + Duration::from_millis(500);
for t in [&out_thread, &err_thread] {
while !t.is_finished() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
}
let stdout = out_buf.lock().expect("reader buffer").clone();
let stderr = err_buf.lock().expect("reader buffer").clone();
Ok(Captured {
status,
stdout,
stderr,
})
}
#[allow(clippy::type_complexity)]
fn drain(mut r: impl Read + Send + 'static) -> (Arc<Mutex<Vec<u8>>>, JoinHandle<()>) {
let buf = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&buf);
let t = std::thread::spawn(move || {
let mut chunk = [0u8; 8192];
loop {
match r.read(&mut chunk) {
Ok(0) | Err(_) => break,
Ok(n) => sink
.lock()
.expect("reader buffer")
.extend_from_slice(&chunk[..n]),
}
}
});
(buf, t)
}
#[cfg(unix)]
fn kill_tree(child: &Child) {
unsafe {
libc::killpg(child.id() as i32, libc::SIGKILL);
}
}
#[cfg(windows)]
fn kill_tree(child: &Child) {
let _ = Command::new("taskkill")
.args(["/T", "/F", "/PID", &child.id().to_string()])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
}
#[cfg(unix)]
mod unix {
use std::io::IsTerminal;
use std::process::Command;
use std::sync::Once;
pub fn prepare(cmd: &mut Command, interactive: bool) -> bool {
use std::os::unix::process::CommandExt;
cmd.process_group(0);
let tty = interactive && std::io::stdin().is_terminal();
if tty {
ignore_sigttou();
unsafe {
cmd.pre_exec(|| {
libc::setpgid(0, 0);
libc::signal(libc::SIGTTOU, libc::SIG_IGN);
libc::tcsetpgrp(0, libc::getpid());
Ok(())
});
}
}
tty
}
pub fn give_terminal_to(pid: u32) {
unsafe {
libc::tcsetpgrp(0, pid as i32);
}
}
pub fn take_terminal_back() {
unsafe {
libc::tcsetpgrp(0, libc::getpgrp());
}
}
fn ignore_sigttou() {
static ONCE: Once = Once::new();
ONCE.call_once(|| unsafe {
libc::signal(libc::SIGTTOU, libc::SIG_IGN);
});
}
}