greggd 1.0.14

Lightweight Linux, macOS, and Windows metrics daemon that exposes a read-only JSON API for the gregg client.
Documentation
//! Bounded child-process execution for startup manager probes and commands.

use std::io::{self, Read};
use std::process::{Command, Output};
use std::thread;
use std::time::{Duration, Instant};

pub(crate) const MANAGER_COMMAND_TIMEOUT: Duration = Duration::from_secs(10);
#[cfg(unix)]
pub(crate) const DIRECT_RESTART_TIMEOUT: Duration = Duration::from_secs(10);
/// Poll interval for bounded child waits. A 10ms sleep keeps manager probes
/// responsive without a timeout thread; wakeups are bounded (~1000 per 10s
/// probe) and probes are infrequent operator commands, so blocking `wait()`
/// with `wait_timeout` is not worth the extra dependency.
pub(crate) const CHILD_POLL_INTERVAL: Duration = Duration::from_millis(10);
pub(crate) fn run_bounded_command(
    program: &str,
    args: &[&str],
    timeout: Duration,
) -> io::Result<Output> {
    let mut child = Command::new(program)
        .args(args)
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()?;
    let stdout = child.stdout.take().map(read_pipe);
    let stderr = child.stderr.take().map(read_pipe);
    let deadline = Instant::now() + timeout;
    let status = loop {
        match child.try_wait()? {
            Some(status) => break status,
            None if Instant::now() >= deadline => {
                let _ = child.kill();
                let _ = child.wait();
                let _ = join_pipe(stdout);
                let _ = join_pipe(stderr);
                return Err(io::Error::new(
                    io::ErrorKind::TimedOut,
                    format!("{program} timed out after {}s", timeout.as_secs()),
                ));
            }
            None => thread::sleep(CHILD_POLL_INTERVAL),
        }
    };
    Ok(Output {
        status,
        stdout: join_pipe(stdout)?,
        stderr: join_pipe(stderr)?,
    })
}
fn read_pipe<R: Read + Send + 'static>(mut reader: R) -> thread::JoinHandle<io::Result<Vec<u8>>> {
    thread::spawn(move || {
        let mut bytes = Vec::new();
        reader.read_to_end(&mut bytes)?;
        Ok(bytes)
    })
}
fn join_pipe(reader: Option<thread::JoinHandle<io::Result<Vec<u8>>>>) -> io::Result<Vec<u8>> {
    match reader {
        Some(reader) => reader
            .join()
            .map_err(|_| io::Error::other("child output reader panicked"))?,
        None => Ok(Vec::new()),
    }
}
#[cfg(test)]
mod tests {
    #[cfg(unix)]
    use super::*;

    #[cfg(unix)]
    #[test]
    fn bounded_manager_command_captures_stderr_and_kills_on_timeout() {
        let output = run_bounded_command(
            "sh",
            &["-c", "printf stdout; printf denied >&2; exit 7"],
            Duration::from_secs(1),
        )
        .unwrap();
        assert_eq!(output.status.code(), Some(7));
        assert_eq!(output.stdout, b"stdout");
        assert_eq!(output.stderr, b"denied");

        let error = run_bounded_command("sh", &["-c", "sleep 1"], Duration::from_millis(40))
            .expect_err("slow manager command must time out");
        assert_eq!(error.kind(), io::ErrorKind::TimedOut);
    }
}