bctx 0.1.40

bctx CLI — intercept CLI commands and compress output for LLM coding agents
//! Background processes that outlive the command which started them.

use std::process::{Child, Command};

/// Spawns the command `make` builds outside the terminal's process group (Unix) or console
/// and job (Windows), so Ctrl-C or closing the window cannot stop it. `make` may be called
/// twice: Windows refuses to break away from a job that forbids it, and then we retry without.
pub fn spawn(make: impl Fn() -> Command) -> std::io::Result<Child> {
    #[cfg(unix)]
    {
        use std::os::unix::process::CommandExt;
        make().process_group(0).spawn()
    }
    #[cfg(windows)]
    {
        use std::os::windows::process::CommandExt;
        const DETACHED_PROCESS: u32 = 0x0000_0008;
        const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
        const CREATE_BREAKAWAY_FROM_JOB: u32 = 0x0100_0000;
        let flags = DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP;
        make()
            .creation_flags(flags | CREATE_BREAKAWAY_FROM_JOB)
            .spawn()
            .or_else(|_| make().creation_flags(flags).spawn())
    }
    #[cfg(not(any(unix, windows)))]
    {
        make().spawn()
    }
}

#[cfg(test)]
mod tests {
    #[cfg(unix)]
    #[test]
    fn a_detached_process_is_out_of_reach_of_the_terminals_ctrl_c() {
        // Ctrl-C signals the terminal's foreground process group; a detached process is not in it.
        let mut child = super::spawn(|| {
            let mut c = std::process::Command::new("sleep");
            c.arg("5");
            c
        })
        .unwrap();
        let pgid_of = |pid: u32| {
            let out = std::process::Command::new("ps")
                .args(["-o", "pgid=", "-p", &pid.to_string()])
                .output()
                .unwrap();
            String::from_utf8_lossy(&out.stdout).trim().to_string()
        };
        let child_group = pgid_of(child.id());
        let _ = child.kill();
        let _ = child.wait();
        assert_eq!(child_group, child.id().to_string(), "leads its own group");
        assert_ne!(child_group, pgid_of(std::process::id()));
    }
}