//! Spawn detached Codex agent processes for exec and repl sessions. use crate::store::Paths; use anyhow::Context; use anyhow::Result; use std::fs::OpenOptions; use tokio::process::Child; use tokio::process::Command; // ------------------------------------------------------------------------- // Additional (Unix-only) imports to replace the former unsafe `libc` calls. // These are guarded by `cfg(unix)` so Windows builds are completely unaffected. // ------------------------------------------------------------------------- #[cfg(unix)] use command_group::AsyncCommandGroup; // provides `group_spawn` for tokio::process::Command #[cfg(unix)] use nix::{ errno::Errno, sys::{ stat::Mode, }, unistd::mkfifo, }; /// Open (and create if necessary) the log files that stdout / stderr of the /// spawned agent will be redirected to. fn open_log_files(paths: &Paths) -> Result<(std::fs::File, std::fs::File)> { let stdout = OpenOptions::new() .create(true) .append(true) .open(&paths.stdout)?; let stderr = OpenOptions::new() .create(true) .append(true) .open(&paths.stderr)?; Ok((stdout, stderr)) } /// Configure a `tokio::process::Command` with the common options that are the /// same for both `codex-exec` and `codex-repl` sessions. fn base_command(bin: &str, paths: &Paths) -> Result { let (stdout, stderr) = open_log_files(paths)?; let mut cmd = Command::new(bin); cmd.stdin(std::process::Stdio::null()) .stdout(stdout) .stderr(stderr); Ok(cmd) } pub fn spawn_exec(paths: &Paths, exec_args: &[String]) -> Result { #[cfg(unix)] { // ----------------------------------------------------------------- // UNIX IMPLEMENTATION (now 100 % safe) // ----------------------------------------------------------------- // Build the base command and add the user-supplied arguments. let mut cmd = base_command("codex-exec", paths)?; cmd.args(exec_args); // Replace the `stdin` that `base_command` configured (null) with // `/dev/null` opened for reading – keeps the previous behaviour while // still leveraging the common helper. let stdin = OpenOptions::new().read(true).open("/dev/null")?; cmd.stdin(stdin); // Spawn the child as a *process group* / new session leader. // `group_spawn()` internally performs the traditional // 1. `fork()` // 2. `setsid()` // 3. `execvp()` // sequence that we previously had to code manually via an unsafe // `pre_exec` closure. let child = cmd .group_spawn() // <- safe wrapper from the `command-group` crate .context("failed to spawn codex-exec")? .into_inner(); // convert AsyncGroupChild -> tokio::process::Child // Ignore SIGHUP in the parent, mirroring the behaviour of the previous // unsafe `libc::signal` call. crate::sig::ignore_sighup()?; Ok(child) } #[cfg(windows)] { use std::os::windows::process::CommandExt; const DETACHED_PROCESS: u32 = 0x00000008; const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; let mut cmd = base_command("codex-exec", paths)?; cmd.args(exec_args) .creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP); let child = cmd.spawn().context("failed to spawn codex-exec")?; Ok(child) } } pub fn spawn_repl(paths: &Paths, repl_args: &[String]) -> Result { #[cfg(unix)] { // ----------------------------------------------------------------- // UNIX IMPLEMENTATION (now 100 % safe) // ----------------------------------------------------------------- // Ensure a FIFO exists at `paths.stdin` with permissions rw------- if !paths.stdin.exists() { if let Err(e) = mkfifo(&paths.stdin, Mode::from_bits_truncate(0o600)) { // If the FIFO already exists we silently accept, just as the // previous implementation did. if e != Errno::EEXIST { return Err(std::io::Error::from(e)).context("mkfifo failed"); } } } // Open the FIFO for *both* reading and writing so we don't deadlock // when there is no writer yet (mimics the previous behaviour). let stdin = OpenOptions::new() .read(true) .write(true) .open(&paths.stdin)?; // Build the command. let mut cmd = base_command("codex-repl", paths)?; cmd.args(repl_args).stdin(stdin); // Detached spawn. let child = cmd .group_spawn() .context("failed to spawn codex-repl")? .into_inner(); // Ignore SIGHUP as before. crate::sig::ignore_sighup()?; Ok(child) } #[cfg(windows)] { anyhow::bail!("codex-repl sessions are not supported on Windows yet"); } }