Files
codex/codex-rs/app-server-daemon/src/managed_install.rs
T
Eric Traut adee0b04fa Preserve standalone release pins during daemon updates (#43521)
## 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
2026-09-07 17:18:02 +00:00

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;