This commit is contained in:
Ryan Ragona
2025-04-26 06:53:03 -07:00
parent 314d2216cb
commit abf0198a49
4 changed files with 97 additions and 80 deletions
+1
View File
@@ -586,6 +586,7 @@ dependencies = [
"chrono",
"clap",
"codex-core",
"codex-exec",
"directories",
"libc",
"nix 0.27.1",
+3
View File
@@ -32,6 +32,9 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1"
directories = "5"
# Re-use the codex-exec library for its CLI definition
codex_exec = { package = "codex-exec", path = "../exec" }
# unix-only process helpers
nix = { version = "0.27", optional = true, default-features = false, features = ["process"] }
libc = { version = "0.2", optional = true }
+39 -21
View File
@@ -47,17 +47,13 @@ enum Commands {
#[derive(Args)]
pub struct CreateCmd {
/// A custom session identifier. When omitted, a random UUIDv4 is used.
/// Session identifier. Generates a random UUIDv4 when omitted.
#[arg(long)]
id: Option<String>,
/// Path to the `codex-exec` binary. Defaults to relying on $PATH.
#[arg(long, default_value = "codex-exec")]
exec: String,
/// If set, terminate the agent when the CLI process exits ("attached" mode).
#[arg(long)]
kill_on_drop: bool,
/// All flags following `create` are forwarded to `codex-exec`.
#[clap(flatten)]
exec_cli: codex_exec::Cli,
}
impl CreateCmd {
@@ -75,24 +71,46 @@ impl CreateCmd {
let paths = store::paths_for(&id)?;
store::materialise(&paths, &meta)?;
// Spawn the background agent and immediately detach.
let mut child = spawn::spawn_agent(&self.exec, &id, &paths, self.kill_on_drop)?;
// Convert exec_cli back into a Vec<String> so we can forward them verbatim.
let exec_args = build_exec_args(&self.exec_cli);
if self.kill_on_drop {
// Hold the handle for the lifetime of the CLI; when we drop at the end of
// `run()` the agent will be terminated by the `kill_on_drop` setting.
tokio::spawn(async move {
let _ = child.wait().await;
});
}
// When not in kill_on_drop mode we *immediately* drop the handle so the agent can
// outlive us.
// Spawn the background agent and immediately detach – we never hold on to the
// Child handle.
let _child = spawn::spawn_agent(&paths, &exec_args)?;
println!("{id}");
Ok(())
}
}
/// Re-serialize a `codex_exec::Cli` struct back into the exact CLI args.
fn build_exec_args(cli: &codex_exec::Cli) -> Vec<String> {
let mut args = Vec::new();
for path in &cli.images {
args.push("--image".to_string());
args.push(path.to_string_lossy().into_owned());
}
if let Some(model) = &cli.model {
args.push("--model".to_string());
args.push(model.clone());
}
if cli.skip_git_repo_check {
args.push("--skip-git-repo-check".to_string());
}
if cli.disable_response_storage {
args.push("--disable-response-storage".to_string());
}
if let Some(prompt) = &cli.prompt {
args.push(prompt.clone());
}
args
}
// -----------------------------------------------------------------------------
// delete
@@ -141,7 +159,7 @@ impl LogsCmd {
let file = tokio::fs::File::open(target).await?;
if self.follow {
let mut reader = tokio::io::BufReader::new(file);
let reader = tokio::io::BufReader::new(file);
let mut lines = reader.lines();
while let Some(line) = lines.next_line().await? {
println!("{line}");
+54 -59
View File
@@ -1,80 +1,75 @@
//! Cross-platform helper to spawn a detached `codex-exec` agent.
//! Cross-platform helper to spawn a fully-detached `codex-exec` process.
use crate::store::Paths;
use anyhow::{Context, Result};
use std::fs::OpenOptions;
use tokio::process::{Child, Command};
#[cfg(unix)]
pub fn spawn_agent(exec: &str, id: &str, paths: &Paths, kill_on_drop: bool) -> Result<Child> {
use std::io;
/// Spawn `codex-exec` with `exec_args`, redirecting stdio to the per-session log files and
/// detaching the process group so it survives the parent CLI.
pub fn spawn_agent(paths: &Paths, exec_args: &[String]) -> Result<Child> {
#[cfg(unix)]
{
use std::io;
// Prepare stdio handles first.
let stdin = OpenOptions::new().read(true).open("/dev/null")?;
let stdout = OpenOptions::new()
.create(true)
.append(true)
.open(&paths.stdout)?;
let stderr = OpenOptions::new()
.create(true)
.append(true)
.open(&paths.stderr)?;
// Prepare stdio handles first.
let stdin = OpenOptions::new().read(true).open("/dev/null")?;
let stdout = OpenOptions::new()
.create(true)
.append(true)
.open(&paths.stdout)?;
let stderr = OpenOptions::new()
.create(true)
.append(true)
.open(&paths.stderr)?;
let mut cmd = Command::new(exec);
cmd.arg("--job").arg(id)
.stdin(stdin)
.stdout(stdout)
.stderr(stderr);
let mut cmd = Command::new("codex-exec");
cmd.args(exec_args)
.stdin(stdin)
.stdout(stdout)
.stderr(stderr);
if kill_on_drop {
cmd.kill_on_drop(true);
}
// Detach: make a new session and ignore SIGHUP.
unsafe {
cmd.pre_exec(|| {
unsafe {
// setsid(2)
// Detach from the controlling terminal: setsid + ignore SIGHUP.
// SAFETY: calling an `unsafe` method (`pre_exec`). Runs in the parent process right
// before fork; the closure then executes in the child.
unsafe {
cmd.pre_exec(|| {
if libc::setsid() == -1 {
return Err(io::Error::last_os_error());
}
libc::signal(libc::SIGHUP, libc::SIG_IGN);
}
Ok(())
});
Ok(())
});
}
let child = cmd.spawn().context("failed to spawn codex-exec")?;
return Ok(child);
}
let child = cmd.spawn().context("failed to spawn agent")?;
Ok(child)
}
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
#[cfg(windows)]
pub fn spawn_agent(exec: &str, id: &str, paths: &Paths, kill_on_drop: bool) -> Result<Child> {
use std::os::windows::process::CommandExt;
const DETACHED_PROCESS: u32 = 0x00000008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
const DETACHED_PROCESS: u32 = 0x00000008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
let stdout = OpenOptions::new()
.create(true)
.append(true)
.open(&paths.stdout)?;
let stderr = OpenOptions::new()
.create(true)
.append(true)
.open(&paths.stderr)?;
let stdout = OpenOptions::new()
.create(true)
.append(true)
.open(&paths.stdout)?;
let stderr = OpenOptions::new()
.create(true)
.append(true)
.open(&paths.stderr)?;
let mut cmd = Command::new("codex-exec");
cmd.args(exec_args)
.stdin(std::process::Stdio::null())
.stdout(stdout)
.stderr(stderr)
.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP);
let mut cmd = Command::new(exec);
cmd.arg("--job").arg(id)
.stdin(std::process::Stdio::null())
.stdout(stdout)
.stderr(stderr)
.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP);
if kill_on_drop {
cmd.kill_on_drop(true);
let child = cmd.spawn().context("failed to spawn codex-exec")?;
return Ok(child);
}
let child = cmd.spawn().context("failed to spawn agent")?;
Ok(child)
}