mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
## Why Bootstrapping the app-server daemon should preserve an explicitly selected release, even when that version is currently `latest`. Older managed binaries should also be able to serve app-server without supporting the updater command. ## What changed - Record `latest` selections in `auto-update-version` in both standalone installers and clear the marker for explicit releases. - Start the daemon updater only for a marked stable release whose binary supports `pid-update-loop`. Existing installs without a marker require a new `latest` installation to enable automatic updates. - Recheck the selected release under the install lock so an in-flight update cannot overwrite a new pin, including installer calls from older updaters. Recheck selection before restarting app-server or replacing the updater. - Cancel Unix installer process groups and clean up their owned fallback locks when the updater stops. ## Testing Add coverage for channel markers, explicit pins of the current latest version, older updater guards, updater command support, and Unix installer cancellation with child-process and lock cleanup. GitOrigin-RevId: 4d275237bd77d896bf64dc1b85a5bca3608142c7
168 lines
5.5 KiB
Rust
168 lines
5.5 KiB
Rust
//! Resolves both package and legacy standalone layouts and compares installed executables.
|
|
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::process::Stdio;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use anyhow::anyhow;
|
|
use sha2::Digest;
|
|
use sha2::Sha256;
|
|
use tokio::fs;
|
|
use tokio::process::Command;
|
|
use tokio::time::timeout;
|
|
|
|
/// Returns the packaged executable when present, otherwise an existing legacy executable.
|
|
/// If neither exists, returns the expected packaged path on Windows and preserves the
|
|
/// historical legacy fallback on Unix. This is path selection, not existence validation:
|
|
/// launch operations reject a missing executable separately, while commands such as
|
|
/// stop can still run after the managed install has been removed.
|
|
pub(crate) fn managed_codex_bin(codex_home: &Path) -> PathBuf {
|
|
let current = codex_home
|
|
.join("packages")
|
|
.join("standalone")
|
|
.join("current");
|
|
let packaged = current.join("bin").join(managed_codex_file_name());
|
|
let legacy = current.join(managed_codex_file_name());
|
|
if packaged.is_file() || (cfg!(windows) && !legacy.is_file()) {
|
|
packaged
|
|
} else {
|
|
legacy
|
|
}
|
|
}
|
|
|
|
/// Only latest-channel stable releases may run the public latest-version updater.
|
|
pub(crate) fn is_stable_standalone_release(codex_home: &Path, codex_bin: &Path) -> bool {
|
|
let standalone = codex_home.join("packages/standalone");
|
|
let Ok(releases) = std::fs::canonicalize(standalone.join("releases")) else {
|
|
return false;
|
|
};
|
|
let Ok(release) = std::fs::canonicalize(standalone.join("current")) else {
|
|
return false;
|
|
};
|
|
if release.parent() != Some(releases.as_path()) {
|
|
return false;
|
|
}
|
|
let Some(release_name) = release.file_name().and_then(|name| name.to_str()) else {
|
|
return false;
|
|
};
|
|
let targets = [
|
|
"aarch64-apple-darwin",
|
|
"x86_64-apple-darwin",
|
|
"aarch64-unknown-linux-musl",
|
|
"x86_64-unknown-linux-musl",
|
|
"aarch64-pc-windows-msvc",
|
|
"x86_64-pc-windows-msvc",
|
|
];
|
|
let Some(version) = targets
|
|
.iter()
|
|
.find_map(|target| release_name.strip_suffix(&format!("-{target}")))
|
|
else {
|
|
return false;
|
|
};
|
|
let components: Vec<_> = version.split('.').collect();
|
|
components.len() == 3
|
|
&& components.iter().all(|component| {
|
|
!component.is_empty() && component.bytes().all(|byte| byte.is_ascii_digit())
|
|
})
|
|
&& std::fs::read_to_string(standalone.join("auto-update-version"))
|
|
.is_ok_and(|selected| selected == release_name)
|
|
&& std::fs::canonicalize(codex_bin).is_ok_and(|bin| bin.starts_with(&release))
|
|
}
|
|
|
|
/// Older managed binaries can serve app-server requests without owning an updater.
|
|
pub(crate) async fn supports_daemon_update_loop(codex_bin: &Path) -> bool {
|
|
let mut command = Command::new(codex_bin);
|
|
#[cfg(windows)]
|
|
command.creation_flags(windows_sys::Win32::System::Threading::CREATE_NO_WINDOW);
|
|
timeout(
|
|
Duration::from_secs(5),
|
|
command
|
|
.args(["app-server", "daemon", "pid-update-loop", "--help"])
|
|
.stdin(Stdio::null())
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.kill_on_drop(true)
|
|
.status(),
|
|
)
|
|
.await
|
|
.is_ok_and(|result| result.is_ok_and(|status| status.success()))
|
|
}
|
|
|
|
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()
|
|
)
|
|
})
|
|
}
|
|
|
|
pub(crate) async fn managed_codex_version(codex_bin: &Path) -> Result<String> {
|
|
let mut command = Command::new(codex_bin);
|
|
#[cfg(windows)]
|
|
command.creation_flags(windows_sys::Win32::System::Threading::CREATE_NO_WINDOW);
|
|
let output = command.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)
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub(crate) struct ExecutableIdentity {
|
|
digest: [u8; 32],
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
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" }
|
|
}
|
|
|
|
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(test)]
|
|
#[path = "managed_install_tests.rs"]
|
|
mod tests;
|
|
|
|
#[cfg(test)]
|
|
#[path = "managed_install_path_tests.rs"]
|
|
mod path_tests;
|