Add an explicit app-server daemon update command (#43562)

## What changed

Add `codex app-server daemon update` to check the latest stable release once, even when automatic updates are disabled. The command requires an installer-owned latest-channel standalone installation and reports `updated`, `noUpdate`, or `unsupported` as JSON, including installed and running versions.

Route manual requests through the updater so scheduled and manual installs share one owner and accepted updates continue if the CLI exits. Restart a running managed daemon when its binary or version differs from the selected installation; this may interrupt active or queued work. Return installer failures as command errors and retry requests across updater replacement.

## Testing

Add coverage for manual updates with automatic updates disabled, queued requests, same-version binary replacement, updater handoff retries, and unsupported installations or unmanaged servers.

GitOrigin-RevId: 240b63c79bae3a64a80768131a30da70900a4d62
This commit is contained in:
Eric Traut
2026-09-07 19:11:41 +00:00
committed by copyberry
parent 333c41eef6
commit daca1fab84
8 changed files with 1005 additions and 185 deletions
+8
View File
@@ -787,6 +787,9 @@ enum AppServerDaemonSubcommand {
/// Restart the local app server daemon.
Restart,
/// Update the standalone installation and restart the managed daemon (may interrupt work).
Update,
/// Enable remote control for future starts and a currently running managed daemon.
EnableRemoteControl,
@@ -1378,6 +1381,10 @@ async fn cli_main(
AppServerDaemonSubcommand::Restart => {
print_app_server_daemon_output(AppServerLifecycleCommand::Restart).await?;
}
AppServerDaemonSubcommand::Update => {
let output = codex_app_server_daemon::update().await?;
println!("{}", serde_json::to_string(&output)?);
}
AppServerDaemonSubcommand::EnableRemoteControl => {
print_app_server_remote_control_output(AppServerRemoteControlMode::Enabled)
.await?;
@@ -2616,6 +2623,7 @@ fn app_server_subcommand_name(subcommand: Option<&AppServerSubcommand>) -> &'sta
AppServerDaemonSubcommand::Bootstrap(_) => "app-server daemon bootstrap",
AppServerDaemonSubcommand::Start => "app-server daemon start",
AppServerDaemonSubcommand::Restart => "app-server daemon restart",
AppServerDaemonSubcommand::Update => "app-server daemon update",
AppServerDaemonSubcommand::EnableRemoteControl => {
"app-server daemon enable-remote-control"
}
+29
View File
@@ -168,6 +168,18 @@ fn managed_start_keeps_updater_on_marker_mismatch_but_stops_it_for_pin() -> Resu
std::fs::write(&marker, "0.1.0-other-target")?;
assert_eq!(daemon.lifecycle("start")?["status"], "alreadyRunning");
assert_eq!(daemon.pid("app-server-updater.pid")?, updater_pid);
assert_eq!(daemon.lifecycle("update")?["status"], "unsupported");
std::thread::sleep(Duration::from_millis(250));
let updater_state = Command::new("/bin/ps")
.args(["-p", &updater_pid.to_string(), "-o", "stat="])
.output()?;
ensure!(
updater_state.status.success()
&& !String::from_utf8_lossy(&updater_state.stdout)
.trim()
.starts_with('Z'),
"updater exited during the latest marker transition"
);
std::fs::remove_file(marker)?;
@@ -242,6 +254,7 @@ fn unmanaged_app_server_does_not_launch_updater() -> Result<()> {
let output = daemon.lifecycle("start")?;
assert_eq!(output["status"], "alreadyRunning");
assert_eq!(output["backend"], Value::Null);
assert_eq!(daemon.lifecycle("update")?["status"], "unsupported");
assert!(
!daemon
.home
@@ -251,3 +264,19 @@ fn unmanaged_app_server_does_not_launch_updater() -> Result<()> {
);
Ok(())
}
#[test]
fn manual_update_rejects_an_unowned_installation() -> Result<()> {
let daemon = TestDaemon::new()?;
std::fs::remove_file(
daemon
.home
.path()
.join("packages/standalone/auto-update-version"),
)?;
assert_eq!(daemon.lifecycle("update")?["status"], "unsupported");
assert!(daemon.pid("app-server.pid").is_err());
assert!(daemon.pid("app-server-updater.pid").is_err());
Ok(())
}