mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
## Why `codex app-server daemon update` previously required a latest-channel installation, leaving pinned and local managed packages unable to return to production updates through that command. ## What changed - Allow explicit updates to restore managed packages to the latest stable release while preserving the automatic-update preference. Extend legacy migration to local and pinned packages when the published installer and release support it. - Bind restoration to the selected release with a single-use updater authorization and installer guards. Scheduled updates and ordinary updater socket requests continue to respect pins. - Restart a running daemon when its selected package changes, even if the binary and version are identical; leave stopped daemons stopped. - Reject incompatible production packages before selecting them, resolve Windows junction targets for ownership checks, and show the restoration command after installing a pinned CLI package. ## Testing Extend daemon and installer tests to cover local-package migration, restoration with automatic updates enabled or disabled, same-binary package restarts, stopped-daemon preservation, selection races, incompatible releases, and socket requests that must not undo pins. GitOrigin-RevId: d64520c1190b5c7bac084fb4a1b6b24998b08dfa
194 lines
7.0 KiB
Rust
194 lines
7.0 KiB
Rust
//! Moves a legacy daemon to its dedicated package only during an explicit update.
|
|
//! Preparation leaves the legacy selection intact; publishing current is the cutover.
|
|
|
|
use std::path::Path;
|
|
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use futures::FutureExt;
|
|
|
|
use super::InstallerHttp;
|
|
use super::InstallerMode;
|
|
use super::fetch_installer_script;
|
|
use super::run_installer_script;
|
|
use super::selected_release;
|
|
use crate::Daemon;
|
|
use crate::UpdateOutput;
|
|
use crate::UpdateStatus;
|
|
use crate::backend;
|
|
use crate::client;
|
|
use crate::install_lock::acquire_install_lock;
|
|
use crate::managed_install;
|
|
|
|
pub(super) async fn run(http: &impl InstallerHttp, legacy: &Daemon) -> Result<UpdateOutput> {
|
|
let _operation_lock = legacy.acquire_operation_lock().await?;
|
|
let previous = selected_release(legacy)?;
|
|
anyhow::ensure!(
|
|
previous.0.ends_with("standalone") && super::manual_update::supported(legacy)?,
|
|
"daemon selection changed; retry the update"
|
|
);
|
|
let home = previous
|
|
.0
|
|
.parent()
|
|
.and_then(Path::parent)
|
|
.context("package root has no Codex home")?;
|
|
let root = home.join("packages/app-server-daemon");
|
|
let settings = legacy.load_settings().await?;
|
|
let running = legacy.running_backend_instance(&settings).await?;
|
|
if running.is_none() && client::probe(&legacy.socket_path).await.is_ok() {
|
|
return super::manual_update::unsupported(legacy).await;
|
|
}
|
|
#[cfg(unix)]
|
|
let mut terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
|
|
let cancellation = async {
|
|
#[cfg(unix)]
|
|
tokio::select! {
|
|
_ = terminate.recv() => {},
|
|
_ = tokio::signal::ctrl_c() => {},
|
|
}
|
|
#[cfg(windows)]
|
|
let _ = tokio::signal::ctrl_c().await;
|
|
Some(())
|
|
};
|
|
let mut cancellation = std::pin::pin!(cancellation);
|
|
|
|
let script = fetch_installer_script(http).await?;
|
|
anyhow::ensure!(
|
|
script
|
|
.windows(b"CODEX_INSTALL_DEFER_SELECTION".len())
|
|
.any(|window| window == b"CODEX_INSTALL_DEFER_SELECTION"),
|
|
"the published installer does not support daemon migration yet; the legacy installation was left unchanged"
|
|
);
|
|
eprintln!("Preparing the daemon update in {}...", root.display());
|
|
#[cfg(unix)]
|
|
{
|
|
if matches!(
|
|
run_installer_script(
|
|
&script,
|
|
InstallerMode::Migration,
|
|
&root,
|
|
cancellation.as_mut()
|
|
)
|
|
.await?,
|
|
super::UpdateLoopControl::Stop
|
|
) {
|
|
return Err(std::io::Error::from(std::io::ErrorKind::Interrupted).into());
|
|
}
|
|
}
|
|
#[cfg(windows)]
|
|
tokio::select! {
|
|
result = run_installer_script(&script, InstallerMode::Migration, &root) => { result?; },
|
|
_ = &mut cancellation => return Err(std::io::Error::from(std::io::ErrorKind::Interrupted).into()),
|
|
}
|
|
|
|
let _install_lock = acquire_install_lock(&root).await?;
|
|
let pending = root.join(".migration-current");
|
|
let release = pending
|
|
.canonicalize()
|
|
.context("installer did not prepare a daemon package")?;
|
|
let name = release
|
|
.file_name()
|
|
.context("prepared release has no name")?;
|
|
anyhow::ensure!(
|
|
release.parent() == Some(root.join("releases").canonicalize()?.as_path())
|
|
&& std::fs::read_to_string(root.join("auto-update-version"))? == name.to_string_lossy(),
|
|
"installer did not prepare a latest-channel daemon package"
|
|
);
|
|
let entrypoint = if cfg!(windows) {
|
|
"bin/codex.exe"
|
|
} else {
|
|
"bin/codex"
|
|
};
|
|
let binary = release.join(entrypoint);
|
|
let version = managed_install::managed_codex_version(&binary).await?;
|
|
anyhow::ensure!(
|
|
name.to_string_lossy().starts_with(&format!("{version}-")),
|
|
"prepared daemon version does not match its release"
|
|
);
|
|
// An older production updater understands only the legacy package/PID paths.
|
|
anyhow::ensure!(
|
|
managed_install::supports_daemon_command(
|
|
&binary,
|
|
&["pid-update-loop", "--check-package-ownership"]
|
|
)
|
|
.await,
|
|
"the latest production release does not support daemon migration yet; the legacy daemon was left running"
|
|
);
|
|
#[cfg(windows)]
|
|
backend::windows::ensure_detached_launch(&binary)?;
|
|
anyhow::ensure!(
|
|
selected_release(legacy)? == previous,
|
|
"daemon selection changed; retry the update"
|
|
);
|
|
if cancellation.as_mut().now_or_never().is_some() {
|
|
return Err(std::io::Error::from(std::io::ErrorKind::Interrupted).into());
|
|
}
|
|
let cutover = async {
|
|
backend::pid_update_loop_backend(legacy.backend_paths(&settings))
|
|
.stop()
|
|
.await?;
|
|
anyhow::ensure!(
|
|
selected_release(legacy)? == previous,
|
|
"daemon selection changed; retry the update"
|
|
);
|
|
if let Some(running) = &running {
|
|
crate::thread_recovery::discard_pending(legacy)?;
|
|
running
|
|
.stop_with_grace(settings.shutdown_grace_seconds)
|
|
.await?;
|
|
}
|
|
std::fs::rename(&pending, root.join("current"))
|
|
.context("failed to select the new daemon package; retry the update")
|
|
}
|
|
.await;
|
|
if let Err(error) = cutover {
|
|
if let Err(restore_error) = async {
|
|
legacy
|
|
.current_installation()?
|
|
.ensure_managed_updater(&settings)
|
|
.await
|
|
}
|
|
.await
|
|
{
|
|
eprintln!(
|
|
"warning: failed to restore the legacy updater after migration failed: {restore_error:#}"
|
|
);
|
|
}
|
|
return Err(error);
|
|
}
|
|
let selected = Daemon {
|
|
pid_file: legacy.pid_file.with_file_name(crate::DAEMON_PID_FILE_NAME),
|
|
update_pid_file: legacy
|
|
.update_pid_file
|
|
.with_file_name(crate::DAEMON_UPDATE_PID_FILE_NAME),
|
|
managed_codex_bin: root.join("current").join(entrypoint),
|
|
..legacy.clone()
|
|
};
|
|
let running_version = if running.is_some() {
|
|
selected.start_managed_backend(&settings).await.context(
|
|
"daemon migrated but could not start; retry with `codex app-server daemon start`",
|
|
)?;
|
|
Some(
|
|
selected
|
|
.wait_until_ready()
|
|
.await
|
|
.context(
|
|
"daemon migrated but is not ready; retry with `codex app-server daemon start`",
|
|
)?
|
|
.app_server_version,
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
if let Err(error) = selected.ensure_managed_updater(&settings).await {
|
|
eprintln!("warning: daemon migrated but its updater could not start: {error:#}");
|
|
}
|
|
Ok(UpdateOutput {
|
|
status: UpdateStatus::Updated,
|
|
installed_version: Some(version),
|
|
running_version,
|
|
managed_codex_path: selected.managed_codex_bin,
|
|
message: "The daemon was updated and moved to its dedicated package. The legacy CLI package was left unchanged.".to_string(),
|
|
})
|
|
}
|