car-registry 0.50.0

File-based agent registry + lifecycle supervisor for Common Agent Runtime.
Documentation
//! Process-tree-safe command execution.
//!
//! `tokio::process::Command::kill_on_drop(true)` reaps only the *direct* child
//! when its future is dropped (e.g. a `tokio::time::timeout` fires). On Windows
//! there are no process groups, so any grandchildren the child spawned are
//! orphaned and leak. [`output_with_tree_kill`] closes that gap: it assigns the
//! child (and everything it spawns) to a `KILL_ON_JOB_CLOSE` Job Object, so
//! dropping the future closes the job handle and the kernel cascades the kill
//! to the whole tree. This is the same guard the coder/assistant shell tool in
//! `car-server-core` uses; sharing it here lets the scheduler's command runner
//! and the foreman's verify-command gate get the same guarantee.

use std::process::{Output, Stdio};

/// Run `cmd` to completion and capture its output, with a Windows process-tree
/// kill guard on drop. Forces `stdin` closed and `stdout`/`stderr` piped (like
/// [`tokio::process::Command::output`]) plus `kill_on_drop`, then — on Windows
/// — assigns the child to a tree-kill Job Object. If the returned future is
/// dropped (a timeout, a cancelled task), the whole tree is terminated, not
/// just the direct child.
///
/// Best-effort on the Job Object: if it can't be created/assigned the call
/// degrades to plain `kill_on_drop` (direct child only), never failing the
/// command for it. On non-Windows this is exactly `Command::output` semantics
/// (Unix callers that need tree-kill use `process_group` + `killpg`).
pub async fn output_with_tree_kill(mut cmd: tokio::process::Command) -> std::io::Result<Output> {
    cmd.stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .kill_on_drop(true);
    let child = cmd.spawn()?;

    // Assign to a tree-kill Job Object and hold the guard across the await so
    // that dropping this future closes the handle → KILL_ON_JOB_CLOSE cascades
    // to every process in the tree. `_job` must be a named binding (not `_`)
    // so it lives to the end of the scope rather than dropping immediately.
    #[cfg(target_os = "windows")]
    let _job = crate::supervisor::JobObject::new().ok().inspect(|j| {
        if let Some(pid) = child.id() {
            let _ = j.assign(pid);
        }
    });

    child.wait_with_output().await
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn runs_and_captures_output() {
        let cmd = if cfg!(windows) {
            let mut c = tokio::process::Command::new("cmd");
            c.args(["/C", "echo hi"]);
            c
        } else {
            let mut c = tokio::process::Command::new("sh");
            c.args(["-c", "echo hi"]);
            c
        };
        let out = output_with_tree_kill(cmd).await.expect("command runs");
        assert!(out.status.success());
        assert!(String::from_utf8_lossy(&out.stdout).contains("hi"));
    }
}