Download codex-rs/app-server-daemon/src/update_loop_tests.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 34 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/app-server-daemon/src/update_loop_tests.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/app-server-daemon/src/update_loop_tests.rs
-
curl -L -o update_loop_tests.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/app-server-daemon/src/update_loop_tests.rs
34 kB
| use std::sync::Mutex; | |
| use std::time::Duration; | |
| use pretty_assertions::assert_eq; | |
| use tempfile::TempDir; | |
| use super::INSTALL_URL; | |
| use super::InstallerHttp; | |
| use super::InstallerResponse; | |
| use super::fetch_installer_script; | |
| use super::manual_update::run as manual_update_once; | |
| use crate::Daemon; | |
| use crate::UpdateOutput; | |
| use crate::UpdateStatus; | |
| use crate::managed_install::executable_identity; | |
| use crate::managed_install::executable_identity_from_bytes; | |
| async fn installer_fetch_uses_exact_url_and_preserves_bytes() { | |
| let script = b"#!/bin/sh\nprintf 'update bytes'\n".to_vec(); | |
| let http = FakeInstallerHttp::new(InstallerResponse::Success(script.clone())); | |
| assert_eq!( | |
| fetch_installer_script(&http) | |
| .await | |
| .expect("installer fetch should succeed"), | |
| script | |
| ); | |
| assert_eq!(http.requested_urls(), vec![INSTALL_URL.to_string()]); | |
| } | |
| async fn installer_fetch_rejects_non_success_status() { | |
| let http = FakeInstallerHttp::new(InstallerResponse::Unsuccessful { status: 503 }); | |
| let error = fetch_installer_script(&http) | |
| .await | |
| .expect_err("non-success response should fail"); | |
| assert!(error.to_string().contains("503")); | |
| assert_eq!(http.requested_urls(), vec![INSTALL_URL.to_string()]); | |
| } | |
| struct FakeInstallerHttp { | |
| response: InstallerResponse, | |
| requested_urls: Mutex<Vec<String>>, | |
| } | |
| async fn explicit_update_migrates_running_and_stopped_installations() { | |
| for (running, local) in [(false, false), (true, false), (false, true), (true, true)] { | |
| let home = TempDir::new().unwrap(); | |
| let (legacy, release) = manual_update_daemon(&home); | |
| let root = home.path().join("packages/standalone"); | |
| if local { | |
| let package = root.join("releases/local-development"); | |
| std::fs::create_dir(&package).unwrap(); | |
| std::fs::copy(&legacy.managed_codex_bin, package.join("codex")).unwrap(); | |
| std::fs::remove_file(root.join("current")).unwrap(); | |
| std::os::unix::fs::symlink(&package, root.join("current")).unwrap(); | |
| std::fs::remove_file(root.join("auto-update-version")).unwrap(); | |
| } | |
| let previous = root.join("current").canonicalize().unwrap(); | |
| let scheduled = FakeInstallerHttp::new(InstallerResponse::Success( | |
| b"# CODEX_INSTALL_IF_LATEST\ntest \"$CODEX_INSTALL_DEFER_SELECTION\" = 0 && test \"$CODEX_INSTALL_DAEMON_ONLY\" = 0\n".to_vec(), | |
| )); | |
| super::update_once( | |
| &scheduled, | |
| &legacy, | |
| &executable_identity(&legacy.managed_codex_bin) | |
| .await | |
| .unwrap(), | |
| &mut test_terminate(), | |
| super::UpdateTrigger::Scheduled, | |
| ) | |
| .await | |
| .unwrap(); | |
| assert_eq!( | |
| scheduled.requested_urls(), | |
| if local { | |
| vec![] | |
| } else { | |
| vec![INSTALL_URL.to_string()] | |
| } | |
| ); | |
| assert_eq!( | |
| crate::managed_install::package_root(home.path()), | |
| home.path().join("packages/standalone") | |
| ); | |
| let settings = format!( | |
| r#"{{"updater":{{"autoUpdateEnabled":{running}}},"remoteControlEnabled":true}}"# | |
| ); | |
| std::fs::write(&legacy.settings_file, &settings).unwrap(); | |
| let daemon_settings = legacy.load_settings().await.unwrap(); | |
| let old_backend = crate::backend::pid_backend(legacy.backend_paths(&daemon_settings)); | |
| let old_updater = | |
| crate::backend::pid_update_loop_backend(legacy.backend_paths(&daemon_settings)); | |
| let server = if running { | |
| old_backend.start().await.unwrap(); | |
| old_updater.start().await.unwrap(); | |
| Some(test_control_server(&legacy, home.path()).await) | |
| } else { | |
| None | |
| }; | |
| let legacy_bin = std::fs::read(&legacy.managed_codex_bin).unwrap(); | |
| let dedicated = home.path().join("packages/app-server-daemon"); | |
| let old_installer = FakeInstallerHttp::new(InstallerResponse::Success(b"exit 99".to_vec())); | |
| assert!( | |
| super::migration::run(&old_installer, &legacy) | |
| .await | |
| .unwrap_err() | |
| .to_string() | |
| .contains("does not support daemon migration") | |
| ); | |
| let script = format!( | |
| r#"# CODEX_INSTALL_DEFER_SELECTION | |
| set -eu | |
| test "$CODEX_INSTALL_DEFER_SELECTION" = 1 | |
| test "$CODEX_INSTALL_IF_CURRENT" = 0 | |
| test "$CODEX_INSTALL_DAEMON_ONLY" = 1 | |
| test "$CODEX_INSTALL_IF_LATEST" = 0 | |
| root="$CODEX_HOME/packages/app-server-daemon" | |
| mkdir -p "$root/releases/{release}/bin" | |
| printf '#!/bin/sh\nif [ "$1" = --version ]; then echo codex 1.0.0; else exit 2; fi\n' > "$root/releases/{release}/bin/codex" | |
| chmod +x "$root/releases/{release}/bin/codex" | |
| ln -sfn 'releases/{release}' "$root/.migration-current" | |
| printf '{release}' > "$root/auto-update-version" | |
| "# | |
| ); | |
| let http = FakeInstallerHttp::new(InstallerResponse::Success(script.as_bytes().to_vec())); | |
| let error = super::migration::run(&http, &legacy).await.unwrap_err(); | |
| assert!( | |
| error | |
| .to_string() | |
| .contains("production release does not support daemon migration") | |
| ); | |
| assert!(!dedicated.join("current").exists()); | |
| assert_eq!( | |
| crate::managed_install::package_root(home.path()), | |
| home.path().join("packages/standalone") | |
| ); | |
| assert_eq!( | |
| ( | |
| old_backend.is_starting_or_running().await.unwrap(), | |
| old_updater.is_starting_or_running().await.unwrap() | |
| ), | |
| (running, running) | |
| ); | |
| let compatible = FakeInstallerHttp::new(InstallerResponse::Success(script.replace("else exit 2", "elif [ \"$4\" = --check-package-ownership ] || [ \"$4\" = --help ]; then exit 0; else exec sleep 30").into_bytes())); | |
| let output = super::migration::run(&compatible, &legacy).await.unwrap(); | |
| assert_eq!(output, UpdateOutput { | |
| status: UpdateStatus::Updated, | |
| installed_version: Some("1.0.0".to_string()), | |
| running_version: (running).then(|| "1.0.0".to_string()), | |
| managed_codex_path: dedicated.join("current/bin/codex"), | |
| message: "The daemon was updated and moved to its dedicated package. The legacy CLI package was left unchanged.".to_string(), | |
| }); | |
| assert_eq!( | |
| crate::managed_install::package_root(home.path()), | |
| dedicated.clone() | |
| ); | |
| assert!(!dedicated.join(".migration-current").exists()); | |
| assert_eq!(root.join("current").canonicalize().unwrap(), previous); | |
| assert_eq!( | |
| std::fs::read(&legacy.managed_codex_bin).unwrap(), | |
| legacy_bin | |
| ); | |
| assert_eq!( | |
| std::fs::read_to_string(&legacy.settings_file).unwrap(), | |
| settings | |
| ); | |
| 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: dedicated.join("current/bin/codex"), | |
| ..legacy.clone() | |
| }; | |
| let new_backend = crate::backend::pid_backend(selected.backend_paths(&daemon_settings)); | |
| assert_eq!(new_backend.is_starting_or_running().await.unwrap(), running); | |
| assert!(!old_backend.is_starting_or_running().await.unwrap()); | |
| assert!(!old_updater.is_starting_or_running().await.unwrap()); | |
| let new_updater = | |
| crate::backend::pid_update_loop_backend(selected.backend_paths(&daemon_settings)); | |
| assert_eq!(new_updater.is_starting_or_running().await.unwrap(), running); | |
| new_updater.stop().await.unwrap(); | |
| new_backend.stop().await.unwrap(); | |
| if let Some(server) = server { | |
| server.abort(); | |
| } | |
| } | |
| } | |
| impl FakeInstallerHttp { | |
| fn new(response: InstallerResponse) -> Self { | |
| Self { | |
| response, | |
| requested_urls: Mutex::new(Vec::new()), | |
| } | |
| } | |
| fn requested_urls(&self) -> Vec<String> { | |
| self.requested_urls | |
| .lock() | |
| .unwrap_or_else(std::sync::PoisonError::into_inner) | |
| .clone() | |
| } | |
| } | |
| impl InstallerHttp for FakeInstallerHttp { | |
| async fn get(&self, url: &str) -> anyhow::Result<InstallerResponse> { | |
| self.requested_urls | |
| .lock() | |
| .unwrap_or_else(std::sync::PoisonError::into_inner) | |
| .push(url.to_string()); | |
| Ok(self.response.clone()) | |
| } | |
| } | |
| async fn cancelling_installer_stops_children_and_releases_fallback_lock() { | |
| let home = tempfile::TempDir::new().expect("home"); | |
| let ready = home.path().join("ready"); | |
| let delayed = home.path().join("delayed"); | |
| let lock = home.path().join("packages/standalone/install.lock.d"); | |
| let script = format!( | |
| "mkdir -p '{lock}'\necho $$ > '{lock}/pid'\n(trap '' TERM; echo ready > '{ready}'; sleep 4; echo late > '{delayed}') &\nwait\n", | |
| lock = lock.display(), | |
| ready = ready.display(), | |
| delayed = delayed.display(), | |
| ); | |
| let (cancel, cancelled) = tokio::sync::oneshot::channel(); | |
| let ready_for_signal = ready.clone(); | |
| let signal_sender = tokio::spawn(async move { | |
| let deadline = tokio::time::Instant::now() + Duration::from_secs(5); | |
| while !ready_for_signal.exists() { | |
| assert!( | |
| tokio::time::Instant::now() < deadline, | |
| "installer did not start" | |
| ); | |
| tokio::time::sleep(Duration::from_millis(20)).await; | |
| } | |
| cancel.send(()).expect("cancel installer"); | |
| }); | |
| let result = tokio::time::timeout( | |
| Duration::from_secs(10), | |
| super::run_installer_script( | |
| script.as_bytes(), | |
| super::InstallerMode::Update("0.150.0-test"), | |
| &home.path().join("packages/standalone"), | |
| async { cancelled.await.ok() }, | |
| ), | |
| ) | |
| .await | |
| .expect("installer cancellation timed out") | |
| .expect("installer cancellation failed"); | |
| signal_sender.await.expect("signal sender"); | |
| assert!(matches!(result, super::UpdateLoopControl::Stop)); | |
| assert!(!lock.exists()); | |
| tokio::time::sleep(Duration::from_secs(3)).await; | |
| assert!(!delayed.exists()); | |
| } | |
| fn manual_update_daemon(home: &TempDir) -> (Daemon, String) { | |
| use std::os::unix::fs::PermissionsExt; | |
| let target = if cfg!(target_os = "macos") { | |
| format!("{}-apple-darwin", std::env::consts::ARCH) | |
| } else { | |
| format!("{}-unknown-linux-musl", std::env::consts::ARCH) | |
| }; | |
| let release = format!("1.0.0-{target}"); | |
| let standalone = home.path().join("packages/standalone"); | |
| let bin = standalone.join("releases").join(&release).join("codex"); | |
| std::fs::create_dir_all(bin.parent().expect("binary parent")).expect("release directory"); | |
| std::fs::write( | |
| &bin, | |
| b"#!/bin/sh\nif [ \"$1\" = '--version' ]; then echo codex 1.0.0; else exec sleep 30; fi\n", | |
| ) | |
| .expect("managed binary"); | |
| std::fs::set_permissions(&bin, std::fs::Permissions::from_mode(0o755)) | |
| .expect("executable binary"); | |
| std::os::unix::fs::symlink(format!("releases/{release}"), standalone.join("current")) | |
| .expect("current release"); | |
| std::fs::write(standalone.join("auto-update-version"), &release).expect("latest marker"); | |
| let state = home.path().join("app-server-daemon"); | |
| std::fs::create_dir(&state).unwrap(); | |
| std::fs::write(state.join("app-server.stderr.log"), b"").unwrap(); | |
| ( | |
| Daemon { | |
| socket_path: home.path().join("app-server-control/server.sock"), | |
| pid_file: state.join("app-server.pid"), | |
| update_pid_file: state.join("app-server-updater.pid"), | |
| operation_lock_file: state.join("daemon.lock"), | |
| settings_file: state.join("settings.json"), | |
| managed_codex_bin: standalone.join("current/codex"), | |
| }, | |
| release, | |
| ) | |
| } | |
| fn test_terminate() -> tokio::signal::unix::Signal { | |
| tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) | |
| .expect("install test signal handler") | |
| } | |
| async fn manual_request_retries_after_updater_replacement() { | |
| use tokio::io::AsyncReadExt; | |
| use tokio::io::AsyncWriteExt; | |
| let home = TempDir::new().expect("home"); | |
| let (daemon, _) = manual_update_daemon(&home); | |
| let socket_path = daemon.manual_update_socket_path(); | |
| codex_uds::prepare_private_socket_directory(socket_path.parent().expect("socket parent")) | |
| .await | |
| .expect("socket directory"); | |
| let mut listener = codex_uds::UnixListener::bind(&socket_path) | |
| .await | |
| .expect("old updater socket"); | |
| let expected = UpdateOutput { | |
| status: UpdateStatus::NoUpdate, | |
| managed_codex_path: daemon.managed_codex_bin.clone(), | |
| installed_version: None, | |
| running_version: None, | |
| message: "already current".to_string(), | |
| }; | |
| let reply = expected.clone(); | |
| let server = tokio::spawn(async move { | |
| let mut old = listener.accept().await.expect("first connection"); | |
| let mut request = [0; 7]; | |
| old.read_exact(&mut request).await.expect("first request"); | |
| drop(old); | |
| drop(listener); | |
| tokio::fs::remove_file(&socket_path) | |
| .await | |
| .expect("remove old socket"); | |
| let mut successor = codex_uds::UnixListener::bind(&socket_path) | |
| .await | |
| .expect("successor socket"); | |
| let mut connection = successor.accept().await.expect("retried connection"); | |
| connection | |
| .read_exact(&mut request) | |
| .await | |
| .expect("retried request"); | |
| connection | |
| .write_all(&serde_json::to_vec(&Ok::<_, String>(reply)).expect("serialize response")) | |
| .await | |
| .expect("send response"); | |
| }); | |
| assert_eq!( | |
| super::manual_update::request(&daemon) | |
| .await | |
| .expect("request survives handoff"), | |
| expected | |
| ); | |
| server.await.expect("replacement task"); | |
| } | |
| async fn manual_request_recovers_when_one_shot_updater_exits() { | |
| use tokio::io::AsyncReadExt; | |
| let home = TempDir::new().expect("home"); | |
| let (daemon, _) = manual_update_daemon(&home); | |
| let socket_path = daemon.manual_update_socket_path(); | |
| codex_uds::prepare_private_socket_directory(socket_path.parent().expect("socket parent")) | |
| .await | |
| .expect("socket directory"); | |
| let mut listener = codex_uds::UnixListener::bind(&socket_path) | |
| .await | |
| .expect("one-shot updater socket"); | |
| let server = tokio::spawn(async move { | |
| let mut connection = listener.accept().await.expect("request connection"); | |
| let mut request = [0; 7]; | |
| connection.read_exact(&mut request).await.expect("request"); | |
| drop(connection); | |
| drop(listener); | |
| tokio::fs::remove_file(socket_path) | |
| .await | |
| .expect("remove exited updater socket"); | |
| }); | |
| // Without the selected executable, the startup path reports unsupported. A | |
| // retry that only waits for a successor would time out instead. | |
| std::fs::remove_file(&daemon.managed_codex_bin).expect("remove selected binary"); | |
| let result = tokio::time::timeout( | |
| Duration::from_secs(5), | |
| super::manual_update::request(&daemon), | |
| ) | |
| .await | |
| .expect("retry should return to normal startup") | |
| .expect("unsupported response"); | |
| assert_eq!(result.status, UpdateStatus::Unsupported); | |
| server.await.expect("updater task"); | |
| } | |
| async fn unsupported_request_preserves_updater_schedule() { | |
| use tokio::io::AsyncReadExt; | |
| use tokio::io::AsyncWriteExt; | |
| let home = TempDir::new().expect("home"); | |
| let (daemon, _) = manual_update_daemon(&home); | |
| let daemon = std::sync::Arc::new(daemon); | |
| let identity = executable_identity(&daemon.managed_codex_bin) | |
| .await | |
| .expect("updater identity"); | |
| let socket_path = daemon.manual_update_socket_path(); | |
| let http = FakeInstallerHttp::new(InstallerResponse::Success(Vec::new())); | |
| let updater_daemon = std::sync::Arc::clone(&daemon); | |
| let worker = tokio::spawn(async move { | |
| super::run_with_http( | |
| &http, | |
| &updater_daemon, | |
| &identity, | |
| /*restore_release*/ None, | |
| ) | |
| .await | |
| }); | |
| let deadline = tokio::time::Instant::now() + Duration::from_secs(5); | |
| while !socket_path.exists() { | |
| assert!( | |
| tokio::time::Instant::now() < deadline, | |
| "updater did not listen" | |
| ); | |
| tokio::time::sleep(Duration::from_millis(20)).await; | |
| } | |
| // An external installer can pin while this ordinary worker still listens. | |
| // Raw IPC must not gain the manual CLI's authority to restore production. | |
| let marker = super::selected_release(&daemon) | |
| .unwrap() | |
| .0 | |
| .join("auto-update-version"); | |
| let previous_marker = std::fs::read(&marker).unwrap(); | |
| std::fs::remove_file(&marker).unwrap(); | |
| let mut pinned = codex_uds::UnixStream::connect(&socket_path).await.unwrap(); | |
| pinned.write_all(b"update\n").await.unwrap(); | |
| let mut response = Vec::new(); | |
| pinned.read_to_end(&mut response).await.unwrap(); | |
| let response: Result<UpdateOutput, String> = serde_json::from_slice(&response).unwrap(); | |
| assert_eq!(response.unwrap().status, UpdateStatus::Unsupported); | |
| assert!(!marker.exists()); | |
| std::fs::write(&marker, previous_marker).unwrap(); | |
| std::fs::remove_file(&daemon.managed_codex_bin).expect("remove selected binary"); | |
| let mut malformed = codex_uds::UnixStream::connect(&socket_path) | |
| .await | |
| .expect("connect malformed request"); | |
| malformed | |
| .write_all(b"upd") | |
| .await | |
| .expect("send partial request"); | |
| malformed.shutdown().await.expect("disconnect request"); | |
| let mut discarded = Vec::new(); | |
| malformed | |
| .read_to_end(&mut discarded) | |
| .await | |
| .expect("rejected request"); | |
| tokio::time::sleep(Duration::from_millis(100)).await; | |
| assert!(!worker.is_finished(), "malformed request stopped updater"); | |
| assert_eq!( | |
| super::manual_update::request(&daemon) | |
| .await | |
| .expect("unsupported response") | |
| .status, | |
| UpdateStatus::Unsupported | |
| ); | |
| tokio::time::sleep(Duration::from_millis(100)).await; | |
| assert!(!worker.is_finished(), "unsupported request stopped updater"); | |
| worker.abort(); | |
| } | |
| async fn test_control_server( | |
| daemon: &Daemon, | |
| home: &std::path::Path, | |
| ) -> tokio::task::JoinHandle<()> { | |
| use futures::SinkExt; | |
| use futures::StreamExt; | |
| std::fs::create_dir_all(daemon.socket_path.parent().expect("socket parent")) | |
| .expect("socket directory"); | |
| let mut listener = codex_uds::UnixListener::bind(&daemon.socket_path) | |
| .await | |
| .expect("control listener"); | |
| let codex_home = home.to_path_buf(); | |
| tokio::spawn(async move { | |
| loop { | |
| let connection = listener.accept().await.expect("control connection"); | |
| let mut websocket = tokio_tungstenite::accept_async(connection) | |
| .await | |
| .expect("websocket handshake"); | |
| websocket | |
| .next() | |
| .await | |
| .expect("initialize request") | |
| .expect("frame"); | |
| let version = if std::fs::read_to_string( | |
| crate::managed_install::package_root(&codex_home).join("auto-update-version"), | |
| ) | |
| .unwrap_or_default() | |
| .starts_with("1.1.0") | |
| { | |
| "1.1.0" | |
| } else { | |
| "1.0.0" | |
| }; | |
| websocket.send(tokio_tungstenite::tungstenite::Message::Text( | |
| serde_json::json!({"id": 1, "result": { | |
| "userAgent": format!("codex_app_server_daemon/{version}"), | |
| "codexHome": codex_home, "platformFamily": "unix", "platformOs": std::env::consts::OS, | |
| }}).to_string().into(), | |
| )).await.expect("initialize response"); | |
| websocket | |
| .next() | |
| .await | |
| .expect("initialized notification") | |
| .expect("frame"); | |
| } | |
| }) | |
| } | |
| async fn manual_update_restarts_managed_daemon_with_automatic_updates_disabled() { | |
| check_manual_update_restart("standalone").await; | |
| } | |
| async fn manual_update_restarts_local_daemon_with_automatic_updates_disabled() { | |
| check_manual_update_restart("app-server-daemon").await; | |
| } | |
| async fn check_manual_update_restart(package_directory: &str) { | |
| use tokio::io::AsyncReadExt; | |
| use tokio::io::AsyncWriteExt; | |
| let local_package = package_directory == "app-server-daemon"; | |
| let home = TempDir::new().expect("home"); | |
| let (mut daemon, mut release) = manual_update_daemon(&home); | |
| let standalone = home.path().join("packages").join(package_directory); | |
| if local_package { | |
| std::fs::rename(home.path().join("packages/standalone"), &standalone).unwrap(); | |
| let local = format!("local-development-{release}"); | |
| std::fs::rename( | |
| standalone.join("releases").join(&release), | |
| standalone.join("releases").join(&local), | |
| ) | |
| .unwrap(); | |
| std::fs::remove_file(standalone.join("current")).unwrap(); | |
| std::os::unix::fs::symlink(format!("releases/{local}"), standalone.join("current")) | |
| .unwrap(); | |
| std::fs::remove_file(standalone.join("auto-update-version")).unwrap(); | |
| daemon.managed_codex_bin = standalone.join("current/codex"); | |
| release = local; | |
| } | |
| let daemon = std::sync::Arc::new(daemon); | |
| std::fs::create_dir_all(daemon.settings_file.parent().expect("state directory")) | |
| .expect("state directory"); | |
| std::fs::write( | |
| &daemon.settings_file, | |
| r#"{"updater":{"autoUpdateEnabled":false}}"#, | |
| ) | |
| .expect("disabled updater"); | |
| let server = test_control_server(&daemon, home.path()).await; | |
| let settings = crate::settings::DaemonSettings::default(); | |
| let backend = crate::backend::pid_backend(daemon.backend_paths(&settings)); | |
| backend.start().await.expect("start daemon"); | |
| let current_pid = || { | |
| serde_json::from_slice::<serde_json::Value>( | |
| &std::fs::read(&daemon.pid_file).expect("daemon PID record"), | |
| ) | |
| .expect("PID JSON")["pid"] | |
| .as_u64() | |
| .expect("PID") | |
| }; | |
| let before = current_pid(); | |
| let version = if local_package { "1.0.0" } else { "1.1.0" }; | |
| let next = release | |
| .trim_start_matches("local-development-") | |
| .replacen("1.0.0", version, 1); | |
| let guard = if local_package { | |
| "CODEX_INSTALL_IF_CURRENT" | |
| } else { | |
| "CODEX_INSTALL_IF_LATEST" | |
| }; | |
| let install_binary = if local_package { | |
| // Same binary and version, but a different package: it must still restart. | |
| format!( | |
| "cp '{root}/releases/{release}/codex' '{root}/releases/{next}/bin/codex'", | |
| root = standalone.display() | |
| ) | |
| } else { | |
| format!( | |
| r#"printf '#!/bin/sh\nif [ "$1" = --version ]; then echo codex 1.1.0; else exec sleep 30; fi\n' > '{root}/releases/{next}/bin/codex'"#, | |
| root = standalone.display() | |
| ) | |
| }; | |
| let ready = home.path().join("installer-ready"); | |
| let proceed = home.path().join("installer-proceed"); | |
| let script = format!( | |
| "#!/bin/sh\n# CODEX_INSTALL_IF_LATEST CODEX_INSTALL_IF_CURRENT CODEX_INSTALL_DAEMON_ONLY\nif [ \"$CODEX_UPDATE_FROM_RELEASE\" = '{next}' ]; then exit 0; fi\ntest \"${guard}\" = 1 || exit 4\ntest \"$CODEX_UPDATE_FROM_RELEASE\" = '{release}' || exit 5\ntouch '{ready}'\nwhile [ ! -e '{proceed}' ]; do sleep .05; done\nmkdir -p '{root}/releases/{next}/bin'\n{install_binary}\nchmod +x '{root}/releases/{next}/bin/codex'\nln -sfn 'releases/{next}' '{root}/current'\nprintf '{next}' > '{root}/auto-update-version'\n", | |
| root = standalone.display(), | |
| ready = ready.display(), | |
| proceed = proceed.display(), | |
| ); | |
| let http = FakeInstallerHttp::new(InstallerResponse::Success(script.into_bytes())); | |
| let updater_daemon = std::sync::Arc::clone(&daemon); | |
| let restore_release = local_package.then(|| release.clone()); | |
| let worker = tokio::spawn(async move { | |
| super::run_with_http( | |
| &http, | |
| &updater_daemon, | |
| &executable_identity_from_bytes(b"updater"), | |
| restore_release, | |
| ) | |
| .await | |
| }); | |
| let deadline = tokio::time::Instant::now() + Duration::from_secs(5); | |
| while !daemon.manual_update_socket_path().exists() { | |
| assert!( | |
| tokio::time::Instant::now() < deadline, | |
| "updater did not listen" | |
| ); | |
| tokio::time::sleep(Duration::from_millis(20)).await; | |
| } | |
| let request_daemon = std::sync::Arc::clone(&daemon); | |
| let request = tokio::spawn(async move { super::manual_update::request(&request_daemon).await }); | |
| while !ready.exists() { | |
| assert!( | |
| tokio::time::Instant::now() < deadline, | |
| "installer did not start" | |
| ); | |
| tokio::time::sleep(Duration::from_millis(20)).await; | |
| } | |
| let mut queued = codex_uds::UnixStream::connect(&daemon.manual_update_socket_path()) | |
| .await | |
| .expect("queue a second update"); | |
| queued | |
| .write_all(b"update\n") | |
| .await | |
| .expect("send second update"); | |
| std::fs::write(proceed, b"go").expect("release installer"); | |
| let output = request | |
| .await | |
| .expect("first request task") | |
| .expect("manual update"); | |
| assert_eq!(output.status, UpdateStatus::Updated); | |
| assert_eq!(output.installed_version.as_deref(), Some(version)); | |
| assert_eq!(output.running_version.as_deref(), Some(version)); | |
| assert_eq!( | |
| output.managed_codex_path, | |
| standalone.join("current/bin/codex") | |
| ); | |
| let restarted = current_pid(); | |
| assert_ne!(restarted, before); | |
| let mut response = Vec::new(); | |
| queued | |
| .read_to_end(&mut response) | |
| .await | |
| .expect("second response"); | |
| let second: Result<crate::UpdateOutput, String> = | |
| serde_json::from_slice(&response).expect("valid second response"); | |
| assert_eq!( | |
| second.expect("second update").status, | |
| UpdateStatus::NoUpdate | |
| ); | |
| assert_eq!(current_pid(), restarted); | |
| tokio::time::timeout(Duration::from_secs(5), worker) | |
| .await | |
| .expect("one-shot updater did not exit") | |
| .expect("updater task") | |
| .expect("updater loop"); | |
| let no_op = FakeInstallerHttp::new(InstallerResponse::Success( | |
| b"#!/bin/sh\n# CODEX_INSTALL_IF_LATEST CODEX_INSTALL_DAEMON_ONLY\nexit 0\n".to_vec(), | |
| )); | |
| use std::io::Write; | |
| std::fs::OpenOptions::new() | |
| .append(true) | |
| .open( | |
| daemon | |
| .current_managed_codex_bin() | |
| .expect("current executable"), | |
| ) | |
| .expect("managed executable") | |
| .write_all(b"\n# same-version replacement\n") | |
| .expect("replace binary bytes"); | |
| let output = manual_update_once( | |
| &no_op, | |
| &daemon, | |
| &executable_identity_from_bytes(b"updater"), | |
| &mut test_terminate(), | |
| super::UpdateTrigger::Manual, | |
| ) | |
| .await | |
| .expect("retry with stale running binary"); | |
| assert_eq!(output.status, UpdateStatus::NoUpdate); | |
| assert_ne!(current_pid(), restarted); | |
| backend.stop().await.expect("stop daemon"); | |
| server.abort(); | |
| } | |
| async fn powershell_installer_is_noninteractive_and_reports_script_failure() { | |
| let valid = FakeInstallerHttp::new(InstallerResponse::Success( | |
| br#" | |
| function Test-Installer { | |
| if ($env:CODEX_NON_INTERACTIVE -ne '1') { throw 'interactive installer' } | |
| if ($env:CODEX_INSTALL_DAEMON_ONLY -ne '1') { throw 'wrong package destination' } | |
| if ($env:CODEX_INSTALL_IF_CURRENT -ne '1' -or $env:CODEX_INSTALL_IF_LATEST -ne '0') { throw 'wrong update guard' } | |
| } | |
| Test-Installer | |
| "# | |
| .to_vec(), | |
| )); | |
| let script = super::fetch_installer_script(&valid) | |
| .await | |
| .expect("fetch installer"); | |
| super::run_installer_script( | |
| &script, | |
| super::InstallerMode::RestoreProduction("0.150.0-x86_64-pc-windows-msvc"), | |
| std::path::Path::new("packages/app-server-daemon"), | |
| ) | |
| .await | |
| .expect("installer succeeds"); | |
| let failing = FakeInstallerHttp::new(InstallerResponse::Success( | |
| b"throw 'installer failed'".to_vec(), | |
| )); | |
| let script = super::fetch_installer_script(&failing) | |
| .await | |
| .expect("fetch failing installer"); | |
| assert!( | |
| super::run_installer_script( | |
| &script, | |
| super::InstallerMode::RestoreProduction("0.150.0-x86_64-pc-windows-msvc"), | |
| std::path::Path::new("packages/app-server-daemon") | |
| ) | |
| .await | |
| .is_err() | |
| ); | |
| } | |
| async fn update_rejects_a_package_root_change_during_download() { | |
| struct ChangingRoot<'a>(&'a std::path::Path); | |
| impl InstallerHttp for ChangingRoot<'_> { | |
| async fn get(&self, _url: &str) -> anyhow::Result<InstallerResponse> { | |
| std::os::unix::fs::symlink( | |
| self.0.join("packages/standalone"), | |
| self.0.join("packages/app-server-daemon"), | |
| )?; | |
| Ok(InstallerResponse::Success( | |
| b"# CODEX_INSTALL_IF_LATEST\nexit 99\n".to_vec(), | |
| )) | |
| } | |
| } | |
| let home = TempDir::new().unwrap(); | |
| let (daemon, _) = manual_update_daemon(&home); | |
| let identity = executable_identity(&daemon.managed_codex_bin) | |
| .await | |
| .unwrap(); | |
| let error = manual_update_once( | |
| &ChangingRoot(home.path()), | |
| &daemon, | |
| &identity, | |
| &mut test_terminate(), | |
| super::UpdateTrigger::Manual, | |
| ) | |
| .await | |
| .unwrap_err(); | |
| assert!(error.to_string().contains("package root changed")); | |
| } | |
| async fn daemon_owned_updates_require_and_request_an_isolated_installer() { | |
| let home = TempDir::new().unwrap(); | |
| let (mut daemon, release) = manual_update_daemon(&home); | |
| let root = home.path().join("packages/app-server-daemon"); | |
| std::fs::rename(home.path().join("packages/standalone"), &root).unwrap(); | |
| daemon.managed_codex_bin = root.join("current/codex"); | |
| let identity = executable_identity(&daemon.managed_codex_bin) | |
| .await | |
| .unwrap(); | |
| let old = FakeInstallerHttp::new(InstallerResponse::Success( | |
| b"# CODEX_INSTALL_IF_LATEST\nexit 0\n".to_vec(), | |
| )); | |
| let error = manual_update_once( | |
| &old, | |
| &daemon, | |
| &identity, | |
| &mut test_terminate(), | |
| super::UpdateTrigger::Manual, | |
| ) | |
| .await | |
| .unwrap_err(); | |
| assert!( | |
| error | |
| .to_string() | |
| .contains("does not support daemon-owned packages") | |
| ); | |
| let current = FakeInstallerHttp::new(InstallerResponse::Success( | |
| b"# CODEX_INSTALL_IF_LATEST\ntest \"$CODEX_INSTALL_IF_LATEST\" = 1 && test \"$CODEX_INSTALL_DAEMON_ONLY\" = 1\n".to_vec(), | |
| )); | |
| let output = manual_update_once( | |
| ¤t, | |
| &daemon, | |
| &identity, | |
| &mut test_terminate(), | |
| super::UpdateTrigger::Manual, | |
| ) | |
| .await | |
| .unwrap(); | |
| assert_eq!(output.status, UpdateStatus::NoUpdate); | |
| assert!(!home.path().join("packages/standalone").exists()); | |
| // Reuse the stopped daemon to verify explicit unpinning with either preference. | |
| let marker = root.join("auto-update-version"); | |
| let no_op = FakeInstallerHttp::new(InstallerResponse::Success( | |
| b"# CODEX_INSTALL_IF_CURRENT CODEX_INSTALL_DAEMON_ONLY\nexit 0\n".to_vec(), | |
| )); | |
| let script = format!( | |
| r#"# CODEX_INSTALL_IF_CURRENT CODEX_INSTALL_DAEMON_ONLY | |
| set -eu | |
| test "$CODEX_INSTALL_IF_CURRENT" = 1 | |
| test "$CODEX_INSTALL_IF_LATEST" = 0 | |
| test "$CODEX_INSTALL_DAEMON_ONLY" = 1 | |
| test "$CODEX_RELEASE" = latest | |
| test "$CODEX_UPDATE_FROM_RELEASE" = '{release}' | |
| printf '%s' '{release}' > '{marker}' | |
| "#, | |
| marker = marker.display() | |
| ); | |
| let restore = FakeInstallerHttp::new(InstallerResponse::Success(script.into_bytes())); | |
| for auto_update_enabled in [false, true] { | |
| let settings = format!(r#"{{"updater":{{"autoUpdateEnabled":{auto_update_enabled}}}}}"#); | |
| std::fs::write(&daemon.settings_file, &settings).unwrap(); | |
| std::fs::remove_file(&marker).unwrap(); | |
| let error = manual_update_once( | |
| &no_op, | |
| &daemon, | |
| &identity, | |
| &mut test_terminate(), | |
| super::UpdateTrigger::RestoreProduction(&release), | |
| ) | |
| .await | |
| .unwrap_err(); | |
| assert!( | |
| error | |
| .to_string() | |
| .contains("did not select a stable latest release") | |
| ); | |
| assert!(!marker.exists()); | |
| let restored = manual_update_once( | |
| &restore, | |
| &daemon, | |
| &identity, | |
| &mut test_terminate(), | |
| super::UpdateTrigger::RestoreProduction(&release), | |
| ) | |
| .await | |
| .unwrap(); | |
| // Restoring eligibility for the same package still reports noUpdate. | |
| assert_eq!(restored, output); | |
| assert!(!daemon.pid_file.exists()); | |
| assert!(daemon.is_stable_standalone_release().unwrap()); | |
| assert_eq!( | |
| std::fs::read_to_string(&daemon.settings_file).unwrap(), | |
| settings | |
| ); | |
| } | |
| } | |