mirror of
https://github.com/openai/codex.git
synced 2026-05-19 18:52:57 +00:00
## Why Desktop and mobile Codex clients need a machine-readable way to bootstrap and manage `codex app-server` on remote machines reached over SSH. The same flow is also useful for bringing up app-server with `remote_control` enabled on a fresh developer machine and keeping that managed install current without requiring a human session. ## What changed - add the new experimental `codex-app-server-daemon` crate and wire it into `codex app-server daemon` lifecycle commands: `start`, `restart`, `stop`, `version`, and `bootstrap` - add explicit `enable-remote-control` and `disable-remote-control` commands that persist the launch setting and restart a running managed daemon so the change takes effect immediately - emit JSON success responses for daemon commands so remote callers can consume them directly - support a Unix-only pidfile-backed detached backend for lifecycle management - assume the standalone `install.sh` layout for daemon-managed binaries and always launch `CODEX_HOME/packages/standalone/current/codex` - add bootstrap support for the standalone managed install plus a detached hourly updater loop - harden lifecycle management around concurrent operations, pidfile ownership, stale state cleanup, updater ownership, managed-binary preflight, Unix-only rejection, forced shutdown after the graceful window, and updater process-group tracking/cleanup - document the experimental Unix-only support boundary plus the standalone bootstrap/update flow in `codex-rs/app-server-daemon/README.md` ## Verification - `cargo test -p codex-app-server-daemon -p codex-cli` - live pid validation on `cb4`: `bootstrap --remote-control`, `restart`, `version`, `stop` ## Follow-up - Add updater self-refresh so the long-lived `pid-update-loop` can replace its own executable image after installing a newer managed Codex binary.
133 lines
3.5 KiB
Rust
133 lines
3.5 KiB
Rust
#[cfg(unix)]
|
|
use std::process::Stdio;
|
|
#[cfg(unix)]
|
|
use std::time::Duration;
|
|
|
|
#[cfg(unix)]
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
#[cfg(not(unix))]
|
|
use anyhow::bail;
|
|
#[cfg(unix)]
|
|
use futures::FutureExt;
|
|
#[cfg(unix)]
|
|
use tokio::io::AsyncWriteExt;
|
|
#[cfg(unix)]
|
|
use tokio::process::Command;
|
|
#[cfg(unix)]
|
|
use tokio::signal::unix::Signal;
|
|
#[cfg(unix)]
|
|
use tokio::signal::unix::SignalKind;
|
|
#[cfg(unix)]
|
|
use tokio::signal::unix::signal;
|
|
#[cfg(unix)]
|
|
use tokio::time::sleep;
|
|
|
|
#[cfg(unix)]
|
|
use crate::Daemon;
|
|
#[cfg(unix)]
|
|
use crate::RestartIfRunningOutcome;
|
|
|
|
#[cfg(unix)]
|
|
const INITIAL_UPDATE_DELAY: Duration = Duration::from_secs(5 * 60);
|
|
#[cfg(unix)]
|
|
const RESTART_RETRY_INTERVAL: Duration = Duration::from_millis(50);
|
|
#[cfg(unix)]
|
|
const UPDATE_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
|
|
|
#[cfg(unix)]
|
|
pub(crate) async fn run() -> Result<()> {
|
|
let mut terminate =
|
|
signal(SignalKind::terminate()).context("failed to install updater shutdown handler")?;
|
|
if sleep_or_terminate(INITIAL_UPDATE_DELAY, &mut terminate).await {
|
|
return Ok(());
|
|
}
|
|
loop {
|
|
match update_once(&mut terminate).await {
|
|
Ok(UpdateLoopControl::Continue) | Err(_) => {}
|
|
Ok(UpdateLoopControl::Stop) => return Ok(()),
|
|
}
|
|
if sleep_or_terminate(UPDATE_INTERVAL, &mut terminate).await {
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
pub(crate) async fn run() -> Result<()> {
|
|
bail!("pid-managed updater loop is unsupported on this platform")
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
async fn sleep_or_terminate(duration: Duration, terminate: &mut Signal) -> bool {
|
|
tokio::select! {
|
|
_ = sleep(duration) => false,
|
|
_ = terminate.recv() => true,
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
enum UpdateLoopControl {
|
|
Continue,
|
|
Stop,
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
async fn update_once(terminate: &mut Signal) -> Result<UpdateLoopControl> {
|
|
install_latest_standalone().await?;
|
|
|
|
let daemon = Daemon::from_environment()?;
|
|
loop {
|
|
if terminate.recv().now_or_never().flatten().is_some() {
|
|
return Ok(UpdateLoopControl::Stop);
|
|
}
|
|
match daemon.try_restart_if_running().await? {
|
|
RestartIfRunningOutcome::Completed => return Ok(UpdateLoopControl::Continue),
|
|
RestartIfRunningOutcome::Busy => {
|
|
if sleep_or_terminate(RESTART_RETRY_INTERVAL, terminate).await {
|
|
return Ok(UpdateLoopControl::Stop);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
async fn install_latest_standalone() -> Result<()> {
|
|
let script = reqwest::get("https://chatgpt.com/codex/install.sh")
|
|
.await
|
|
.context("failed to fetch standalone Codex updater")?
|
|
.error_for_status()
|
|
.context("standalone Codex updater request failed")?
|
|
.bytes()
|
|
.await
|
|
.context("failed to read standalone Codex updater")?;
|
|
|
|
let mut child = Command::new("/bin/sh")
|
|
.arg("-s")
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.spawn()
|
|
.context("failed to invoke standalone Codex updater")?;
|
|
let mut stdin = child
|
|
.stdin
|
|
.take()
|
|
.context("standalone Codex updater stdin was unavailable")?;
|
|
stdin
|
|
.write_all(&script)
|
|
.await
|
|
.context("failed to pass standalone Codex updater to shell")?;
|
|
drop(stdin);
|
|
let status = child
|
|
.wait()
|
|
.await
|
|
.context("failed to wait for standalone Codex updater")?;
|
|
|
|
if status.success() {
|
|
Ok(())
|
|
} else {
|
|
anyhow::bail!("standalone Codex updater exited with status {status}")
|
|
}
|
|
}
|