use std::io::{self, Read, Write};
use std::os::unix::process::{CommandExt, ExitStatusExt};
use std::process::{Command, Stdio};
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::{Duration, Instant};
use thiserror::Error;
pub(crate) const CASCADE_DEADLINE: Duration = Duration::from_millis(2000);
const POLL_INTERVAL: Duration = Duration::from_millis(20);
pub(crate) struct Finished {
pub(crate) stdout: Vec<u8>,
pub(crate) stderr: Vec<u8>,
pub(crate) code: i32,
}
#[derive(Debug, Error)]
pub(crate) enum Error {
#[error("spawn the tool's child process: {0}")]
Spawn(#[source] io::Error),
#[error("write to the child process's stdin: {0}")]
Stdin(#[source] io::Error),
#[error("wait for the tool's child process: {0}")]
Wait(#[source] io::Error),
}
pub(crate) fn run(
cmd: &mut Command,
input: Option<&[u8]>,
stop: &AtomicBool,
deadline: Duration,
) -> Result<Finished, Error> {
cmd.stdin(if input.is_some() {
Stdio::piped()
} else {
Stdio::null()
})
.stdout(Stdio::piped())
.stderr(Stdio::piped());
unsafe {
cmd.pre_exec(enter_own_process_group);
}
let mut child = cmd.spawn().map_err(Error::Spawn)?;
let pgid = child.id() as i32;
let mut child_stdout = child.stdout.take().expect("piped");
let mut child_stderr = child.stderr.take().expect("piped");
let stdout_thread = thread::spawn(move || {
let mut buf = Vec::new();
let _ = child_stdout.read_to_end(&mut buf);
buf
});
let stderr_thread = thread::spawn(move || {
let mut buf = Vec::new();
let _ = child_stderr.read_to_end(&mut buf);
buf
});
if let Some(bytes) = input {
let mut pipe = child.stdin.take().expect("piped");
pipe.write_all(bytes).map_err(Error::Stdin)?;
}
let status = wait_with_cascade(&mut child, pgid, stop, deadline)?;
Ok(Finished {
stdout: stdout_thread.join().expect("stdout reader did not panic"),
stderr: stderr_thread.join().expect("stderr reader did not panic"),
code: status
.code()
.or_else(|| status.signal().map(|sig| 128 + sig))
.unwrap_or(1),
})
}
fn enter_own_process_group() -> io::Result<()> {
unsafe {
libc::setpgid(0, 0);
}
Ok(())
}
fn wait_with_cascade(
child: &mut std::process::Child,
pgid: i32,
stop: &AtomicBool,
deadline: Duration,
) -> Result<std::process::ExitStatus, Error> {
loop {
if let Some(status) = child.try_wait().map_err(Error::Wait)? {
return Ok(status);
}
thread::sleep(POLL_INTERVAL);
if stop.load(Ordering::SeqCst) {
return cascade_terminate(child, pgid, deadline);
}
}
}
fn cascade_terminate(
child: &mut std::process::Child,
pgid: i32,
deadline: Duration,
) -> Result<std::process::ExitStatus, Error> {
unsafe {
libc::kill(-pgid, libc::SIGTERM);
}
let term_until = Instant::now() + deadline;
while Instant::now() < term_until {
if let Some(status) = child.try_wait().map_err(Error::Wait)? {
return Ok(status);
}
thread::sleep(POLL_INTERVAL);
}
unsafe {
libc::kill(-pgid, libc::SIGKILL);
}
child.wait().map_err(Error::Wait)
}
static SIGTERM_FLAG: AtomicBool = AtomicBool::new(false);
static HANDLER_INSTALLED: OnceLock<()> = OnceLock::new();
extern "C" fn on_sigterm(_signo: libc::c_int) {
SIGTERM_FLAG.store(true, Ordering::SeqCst);
}
pub(crate) fn install_sigterm_handler() {
HANDLER_INSTALLED.get_or_init(|| {
unsafe {
libc::signal(libc::SIGTERM, on_sigterm as *const () as libc::sighandler_t);
}
});
}
pub(crate) fn sigterm_flag() -> &'static AtomicBool {
&SIGTERM_FLAG
}
#[cfg(test)]
mod tests;