mirror of
https://github.com/openai/codex.git
synced 2026-05-23 12:34:25 +00:00
## Why `bootstrap` starts a detached pid-backed updater loop, but before this change that updater could keep running an old executable image even after `install.sh` replaced the managed standalone binary under `CODEX_HOME`. That left the updater itself behind the binary it had just rolled out, especially when the app-server was stopped or when the managed binary changed without a version-string change. ## What changed - Track updater identity from the executable contents rather than only the reported CLI version. - Force the managed app-server restart path when the managed binary contents differ from the running updater image, then re-exec the updater from the managed binary once the rollout is in a safe state. - Distinguish a genuinely absent managed app-server from a managed process that exists but is not yet probeable, so self-refresh does not skip a required restart. - Keep the restart/re-exec decision under the daemon operation lock so `bootstrap` cannot race the handoff. - Update `app-server-daemon/README.md` to document the resulting standalone and out-of-band update behavior. ## Verification - `cargo test -p codex-app-server-daemon` - `just fix -p codex-app-server-daemon` Added focused unit coverage for: - content-based updater refresh decisions - safe updater re-exec outcomes across restart states
104 lines
2.6 KiB
Rust
104 lines
2.6 KiB
Rust
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
#[cfg(unix)]
|
|
use anyhow::Context;
|
|
#[cfg(unix)]
|
|
use anyhow::Result;
|
|
#[cfg(unix)]
|
|
use anyhow::anyhow;
|
|
#[cfg(unix)]
|
|
use sha2::Digest;
|
|
#[cfg(unix)]
|
|
use sha2::Sha256;
|
|
#[cfg(unix)]
|
|
use tokio::fs;
|
|
#[cfg(unix)]
|
|
use tokio::process::Command;
|
|
|
|
pub(crate) fn managed_codex_bin(codex_home: &Path) -> PathBuf {
|
|
codex_home
|
|
.join("packages")
|
|
.join("standalone")
|
|
.join("current")
|
|
.join(managed_codex_file_name())
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
pub(crate) async fn resolved_managed_codex_bin(codex_bin: &Path) -> Result<PathBuf> {
|
|
fs::canonicalize(codex_bin).await.with_context(|| {
|
|
format!(
|
|
"failed to resolve managed Codex binary {}",
|
|
codex_bin.display()
|
|
)
|
|
})
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
pub(crate) async fn managed_codex_version(codex_bin: &Path) -> Result<String> {
|
|
let output = Command::new(codex_bin)
|
|
.arg("--version")
|
|
.output()
|
|
.await
|
|
.with_context(|| {
|
|
format!(
|
|
"failed to invoke managed Codex binary {}",
|
|
codex_bin.display()
|
|
)
|
|
})?;
|
|
if !output.status.success() {
|
|
return Err(anyhow!(
|
|
"managed Codex binary {} exited with status {}",
|
|
codex_bin.display(),
|
|
output.status
|
|
));
|
|
}
|
|
|
|
let stdout = String::from_utf8(output.stdout).with_context(|| {
|
|
format!(
|
|
"managed Codex version was not utf-8: {}",
|
|
codex_bin.display()
|
|
)
|
|
})?;
|
|
parse_codex_version(&stdout)
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub(crate) struct ExecutableIdentity {
|
|
digest: [u8; 32],
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
pub(crate) async fn executable_identity(executable: &Path) -> Result<ExecutableIdentity> {
|
|
let bytes = fs::read(executable)
|
|
.await
|
|
.with_context(|| format!("failed to read executable {}", executable.display()))?;
|
|
Ok(executable_identity_from_bytes(&bytes))
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
pub(crate) fn executable_identity_from_bytes(bytes: &[u8]) -> ExecutableIdentity {
|
|
ExecutableIdentity {
|
|
digest: Sha256::digest(bytes).into(),
|
|
}
|
|
}
|
|
|
|
fn managed_codex_file_name() -> &'static str {
|
|
if cfg!(windows) { "codex.exe" } else { "codex" }
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn parse_codex_version(output: &str) -> Result<String> {
|
|
let version = output
|
|
.split_whitespace()
|
|
.nth(1)
|
|
.filter(|version| !version.is_empty())
|
|
.ok_or_else(|| anyhow!("managed Codex version output was malformed"))?;
|
|
Ok(version.to_string())
|
|
}
|
|
|
|
#[cfg(all(test, unix))]
|
|
#[path = "managed_install_tests.rs"]
|
|
mod tests;
|