diff --git a/codex-rs/app-server-daemon/src/backend/pid.rs b/codex-rs/app-server-daemon/src/backend/pid.rs index aa05e1db8e..77de413a3b 100644 --- a/codex-rs/app-server-daemon/src/backend/pid.rs +++ b/codex-rs/app-server-daemon/src/backend/pid.rs @@ -1,8 +1,8 @@ +//! PID reservations serialize launches and protect stale-record cleanup. + use std::io::SeekFrom; use std::path::Path; use std::path::PathBuf; -#[cfg(unix)] -use std::process::Stdio; use std::time::Duration; use anyhow::Context; @@ -115,123 +115,6 @@ impl PidBackend { } } - #[cfg(unix)] - pub(crate) async fn start(&self) -> Result> { - if let Some(parent) = self.pid_file.parent() { - fs::create_dir_all(parent) - .await - .with_context(|| format!("failed to create pid directory {}", parent.display()))?; - } - let reservation_lock = self.acquire_reservation_lock().await?; - let _pid_file = loop { - match fs::OpenOptions::new() - .create_new(true) - .write(true) - .open(&self.pid_file) - .await - { - Ok(pid_file) => break pid_file, - Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { - match self.read_pid_file_state_with_lock_held().await? { - PidFileState::Missing => continue, - PidFileState::Running(record) => { - if self.record_is_active(&record).await? { - return Ok(None); - } - let _ = fs::remove_file(&self.pid_file).await; - continue; - } - PidFileState::Starting => { - unreachable!("lock holder cannot observe starting") - } - } - } - Err(err) => { - return Err(err).with_context(|| { - format!("failed to reserve pid file {}", self.pid_file.display()) - }); - } - } - }; - let mut command = Command::new(&self.codex_bin); - let stderr_log = match self.open_stderr_log().await { - Ok(stderr_log) => stderr_log, - Err(err) => { - let _ = fs::remove_file(&self.pid_file).await; - return Err(err); - } - }; - command - .args(self.command_args()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::from(stderr_log.into_std().await)); - if let Some((key, value)) = self.command_env() { - command.env(key, value); - } - - #[cfg(unix)] - { - unsafe { - command.pre_exec(|| { - if libc::setsid() == -1 { - return Err(std::io::Error::last_os_error()); - } - Ok(()) - }); - } - } - - let child = match command.spawn() { - Ok(child) => child, - Err(err) => { - let _ = fs::remove_file(&self.pid_file).await; - return Err(err).with_context(|| { - format!( - "failed to spawn detached app-server process using {}", - self.codex_bin.display() - ) - }); - } - }; - let pid = child - .id() - .context("spawned app-server process has no pid")?; - let record = match read_process_start_time(pid).await { - Ok(process_start_time) => PidRecord { - pid, - process_start_time, - }, - Err(err) => { - let _ = self.terminate_process(pid); - let mut context = - format!("failed to record pid-managed app-server process {pid} startup"); - super::append_stderr_log_tail_context(&self.pid_file, &mut context).await; - let _ = fs::remove_file(&self.pid_file).await; - return Err(err).context(context); - } - }; - let contents = serde_json::to_vec(&record).context("failed to serialize pid record")?; - let temp_pid_file = self.pid_file.with_extension("pid.tmp"); - if let Err(err) = fs::write(&temp_pid_file, &contents).await { - let _ = self.terminate_process(pid); - let _ = fs::remove_file(&self.pid_file).await; - return Err(err).with_context(|| { - format!("failed to write pid temp file {}", temp_pid_file.display()) - }); - } - if let Err(err) = fs::rename(&temp_pid_file, &self.pid_file).await { - let _ = self.terminate_process(pid); - let _ = fs::remove_file(&temp_pid_file).await; - let _ = fs::remove_file(&self.pid_file).await; - return Err(err).with_context(|| { - format!("failed to publish pid file {}", self.pid_file.display()) - }); - } - drop(reservation_lock); - Ok(Some(pid)) - } - #[cfg(not(unix))] pub(crate) async fn start(&self) -> Result> { bail!("pid-managed app-server startup is unsupported on this platform") @@ -720,3 +603,7 @@ async fn read_process_start_time(pid: u32) -> Result { #[cfg(all(test, unix))] #[path = "pid_tests.rs"] mod tests; + +#[cfg(unix)] +#[path = "pid_start.rs"] +mod start; diff --git a/codex-rs/app-server-daemon/src/backend/pid_start.rs b/codex-rs/app-server-daemon/src/backend/pid_start.rs new file mode 100644 index 0000000000..d0df052e7c --- /dev/null +++ b/codex-rs/app-server-daemon/src/backend/pid_start.rs @@ -0,0 +1,129 @@ +//! Detached Unix process launch and PID publication under the reservation lock. + +use super::PidBackend; +use super::PidFileState; +use super::PidRecord; +use super::read_process_start_time; +use anyhow::Context; +use anyhow::Result; +use std::process::Stdio; +use tokio::fs; +use tokio::process::Command; + +impl PidBackend { + pub(crate) async fn start(&self) -> Result> { + if let Some(parent) = self.pid_file.parent() { + fs::create_dir_all(parent) + .await + .with_context(|| format!("failed to create pid directory {}", parent.display()))?; + } + let reservation_lock = self.acquire_reservation_lock().await?; + let _pid_file = loop { + match fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(&self.pid_file) + .await + { + Ok(pid_file) => break pid_file, + Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => { + match self.read_pid_file_state_with_lock_held().await? { + PidFileState::Missing => continue, + PidFileState::Running(record) => { + if self.record_is_active(&record).await? { + return Ok(None); + } + let _ = fs::remove_file(&self.pid_file).await; + continue; + } + PidFileState::Starting => { + unreachable!("lock holder cannot observe starting") + } + } + } + Err(err) => { + return Err(err).with_context(|| { + format!("failed to reserve pid file {}", self.pid_file.display()) + }); + } + } + }; + let mut command = Command::new(&self.codex_bin); + let stderr_log = match self.open_stderr_log().await { + Ok(stderr_log) => stderr_log, + Err(err) => { + let _ = fs::remove_file(&self.pid_file).await; + return Err(err); + } + }; + command + .args(self.command_args()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::from(stderr_log.into_std().await)); + if let Some((key, value)) = self.command_env() { + command.env(key, value); + } + + #[cfg(unix)] + { + unsafe { + command.pre_exec(|| { + if libc::setsid() == -1 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }); + } + } + + let child = match command.spawn() { + Ok(child) => child, + Err(err) => { + let _ = fs::remove_file(&self.pid_file).await; + return Err(err).with_context(|| { + format!( + "failed to spawn detached app-server process using {}", + self.codex_bin.display() + ) + }); + } + }; + let pid = child + .id() + .context("spawned app-server process has no pid")?; + let record = match read_process_start_time(pid).await { + Ok(process_start_time) => PidRecord { + pid, + process_start_time, + }, + Err(err) => { + let _ = self.terminate_process(pid); + let mut context = + format!("failed to record pid-managed app-server process {pid} startup"); + super::super::append_stderr_log_tail_context(&self.pid_file, &mut context).await; + let _ = fs::remove_file(&self.pid_file).await; + return Err(err).context(context); + } + }; + let contents = serde_json::to_vec(&record).context("failed to serialize pid record")?; + let temp_pid_file = self.pid_file.with_extension("pid.tmp"); + if let Err(err) = fs::write(&temp_pid_file, &contents).await { + let _ = self.terminate_process(pid); + let _ = fs::remove_file(&self.pid_file).await; + return Err(err).with_context(|| { + format!("failed to write pid temp file {}", temp_pid_file.display()) + }); + } + if let Err(err) = fs::rename(&temp_pid_file, &self.pid_file).await { + let _ = self.terminate_process(pid); + let _ = fs::remove_file(&temp_pid_file).await; + let _ = fs::remove_file(&self.pid_file).await; + return Err(err).with_context(|| { + format!("failed to publish pid file {}", self.pid_file.display()) + }); + } + drop(reservation_lock); + Ok(Some(pid)) + } +}