mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
## Why Daemon installation rereads staged files to verify the executable and hash the normalized WinGet package, even though staging already computed their digests. ## What changed Cache file digests while copying the package and reuse them for the staged executable comparison and final WinGet tree hash. Update cached paths when moving WinGet executables and refresh digests when rewriting manifests. Continue hashing source packages and existing releases from disk so digest reuse does not bypass content validation. ## Testing Extend the installation test to corrupt an existing release's `codex-package.json` and verify that reinstalling rejects it while preserving the current selection. GitOrigin-RevId: 429da2ad8b2ee50f07ffb14b79281ab262a2d1a1
262 lines
8.6 KiB
Rust
262 lines
8.6 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;
|
|
|
|
/// New daemons own their packages, regardless of how the calling CLI was installed.
|
|
/// Preserve legacy launch state, including logs left after a daemon is stopped;
|
|
/// settings, installer selections, and lock files alone do not prove a prior launch.
|
|
pub(crate) fn package_root(codex_home: &Path) -> PathBuf {
|
|
let dedicated = codex_home.join("packages/app-server-daemon");
|
|
if !matches!(dedicated.join("current").symlink_metadata(),
|
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound)
|
|
{
|
|
return dedicated;
|
|
}
|
|
let state = codex_home.join("app-server-daemon");
|
|
for (package, artifacts) in [
|
|
(
|
|
"app-server-daemon",
|
|
[
|
|
crate::DAEMON_PID_FILE_NAME,
|
|
"daemon.stderr.log",
|
|
crate::DAEMON_UPDATE_PID_FILE_NAME,
|
|
"daemon-updater.stderr.log",
|
|
],
|
|
),
|
|
(
|
|
"standalone",
|
|
[
|
|
crate::LEGACY_PID_FILE_NAME,
|
|
"app-server.stderr.log",
|
|
crate::LEGACY_UPDATE_PID_FILE_NAME,
|
|
"app-server-updater.stderr.log",
|
|
],
|
|
),
|
|
] {
|
|
if artifacts.iter().any(|name| {
|
|
!matches!(state.join(name).symlink_metadata(),
|
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound)
|
|
}) {
|
|
return codex_home.join("packages").join(package);
|
|
}
|
|
}
|
|
dedicated
|
|
}
|
|
|
|
/// Resolve both packaged and legacy binaries without requiring a valid install.
|
|
pub(crate) fn managed_codex_bin(codex_home: &Path) -> PathBuf {
|
|
let root = package_root(codex_home);
|
|
let current = root.join("current");
|
|
let packaged = current.join("bin").join(managed_codex_file_name());
|
|
let legacy = current.join(managed_codex_file_name());
|
|
if packaged.is_file()
|
|
|| !legacy.is_file() && (cfg!(windows) || root.ends_with("app-server-daemon"))
|
|
{
|
|
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 = package_root(codex_home);
|
|
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;
|
|
};
|
|
// GNU packages can seed the new directory; retain legacy updater eligibility.
|
|
if standalone.ends_with("standalone") && release_name.ends_with("-gnu") {
|
|
return false;
|
|
}
|
|
let targets = [
|
|
"aarch64-apple-darwin",
|
|
"x86_64-apple-darwin",
|
|
"aarch64-unknown-linux-gnu",
|
|
"x86_64-unknown-linux-gnu",
|
|
"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 {
|
|
supports_daemon_command(codex_bin, &["pid-update-loop", "--help"]).await
|
|
}
|
|
|
|
/// Probe an internal daemon command without running a long-lived process.
|
|
pub(crate) async fn supports_daemon_command(codex_bin: &Path, args: &[&str]) -> 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"])
|
|
.args(args)
|
|
.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")
|
|
.kill_on_drop(true)
|
|
.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 {
|
|
pub(crate) digest: [u8; 32],
|
|
// Distinguish release generations whose executable bytes are identical.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub(crate) path_digest: Option<[u8; 32]>,
|
|
}
|
|
|
|
impl ExecutableIdentity {
|
|
pub(crate) fn same_contents(&self, other: &Self) -> bool {
|
|
self.digest == other.digest
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn executable_identity(executable: &Path) -> Result<ExecutableIdentity> {
|
|
let executable = executable.to_path_buf();
|
|
// Debug executables can be hundreds of MB. Stream the digest off the async
|
|
// runtime instead of allocating the whole file and blocking a runtime thread.
|
|
tokio::task::spawn_blocking(move || {
|
|
let executable = std::fs::canonicalize(&executable).unwrap_or(executable);
|
|
let mut identity = std::fs::File::open(&executable)
|
|
.and_then(executable_identity_from_reader)
|
|
.with_context(|| format!("failed to read executable {}", executable.display()))?;
|
|
identity.path_digest = Some(path_digest(&executable));
|
|
Ok(identity)
|
|
})
|
|
.await
|
|
.context("executable identity task failed")?
|
|
}
|
|
|
|
pub(crate) fn executable_identity_from_reader(
|
|
reader: impl std::io::Read,
|
|
) -> std::io::Result<ExecutableIdentity> {
|
|
let mut hasher = blake3::Hasher::new();
|
|
hasher.update_reader(reader)?;
|
|
Ok(ExecutableIdentity {
|
|
digest: *hasher.finalize().as_bytes(),
|
|
path_digest: None,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn path_digest(path: &Path) -> [u8; 32] {
|
|
let mut hasher = blake3::Hasher::new();
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::ffi::OsStrExt;
|
|
hasher.update(path.as_os_str().as_bytes());
|
|
}
|
|
#[cfg(windows)]
|
|
{
|
|
use std::os::windows::ffi::OsStrExt;
|
|
for unit in path.as_os_str().encode_wide() {
|
|
hasher.update(&unit.to_le_bytes());
|
|
}
|
|
}
|
|
*hasher.finalize().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;
|