Files
codex/codex-rs/app-server-daemon/src/managed_install.rs
T
Eric Traut 769a6a5bcd Record the launched app-server executable identity in PID files (#43552)
## Why

An installer can retarget the selected executable symlink or junction while an app server is running. Recording the launch-time identity preserves which binary was started after that path changes.

## What changed

- Resolve the executable path before launching on all platforms and store an optional executable identity in app-server PID records. Continue accepting older records without this field and expose the identity only for an active process.
- Use BLAKE3 for executable identity digests and make `ExecutableIdentity` serializable.
- Schedule updater checks with an explicit deadline in `tokio::select!`, preserving persisted update intervals, settings-error retries, and termination handling.

## Testing

Add a Unix test that verifies the recorded identity matches the resolved launch binary, differs from a retargeted symlink's binary, and remains optional when reading legacy PID records.

GitOrigin-RevId: 4821130b9a5e75b417a8e39c583b30427a872ff8
2026-09-07 18:52:37 +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 serde::Deserialize;
use serde::Serialize;
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, Serialize, Deserialize)]
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: *blake3::hash(bytes).as_bytes(),
}
}
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;