diff --git a/libs/cua-driver-rs/crates/cua-driver/src/cli.rs b/libs/cua-driver-rs/crates/cua-driver/src/cli.rs index 699bcb5e18..5870a8320b 100644 --- a/libs/cua-driver-rs/crates/cua-driver/src/cli.rs +++ b/libs/cua-driver-rs/crates/cua-driver/src/cli.rs @@ -1084,9 +1084,10 @@ pub fn run_recording_cmd(subcommand: &str, args: &[String], socket: Option<&str> /// `cua-driver update [--apply]` — check for a newer release and optionally apply it. /// /// Shares the GitHub releases fetch with the startup banner via -/// [`crate::version_check::fetch_latest_version`] so both code paths -/// agree on tag filtering and HTTP semantics. Pass `--apply` to download -/// and install via the canonical install.sh. +/// [`crate::version_check::fetch_latest_version`] so both code paths agree on +/// tag filtering and HTTP semantics. `--apply` delegates to the canonical +/// installer script — see [`crate::updater`] for why we go through the script +/// instead of re-implementing the asset resolution + atomic swap + GC in Rust. pub fn run_update_cmd(apply: bool) { let current = env!("CARGO_PKG_VERSION"); println!("Current version: {current}"); @@ -1114,23 +1115,38 @@ pub fn run_update_cmd(apply: bool) { println!(" cua-driver update --apply"); println!(); println!("Or reinstall directly:"); - println!(" curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh | bash"); + println!(" {}", crate::updater::manual_install_one_liner()); return; } println!("Downloading and installing cua-driver {v}…"); - let status = std::process::Command::new("bash") - .arg("-c") - .arg("curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh | bash") - .status(); - match status { - Ok(s) if s.success() => {} + let daemon_was_running = crate::updater::daemon_is_running(); + match crate::updater::run_install_script(&v) { + Ok(s) if s.success() => { + println!("Installed cua-driver {v}."); + if daemon_was_running { + // The atomic swap (symlink retarget / junction flip) + // means the running daemon kept executing the old + // binary — restart picks up the new one. + println!(); + println!("A daemon was running before the install. Restart it to pick up the new binary:"); + println!(" cua-driver stop && cua-driver serve"); + } + } Ok(s) => { - println!("Installation failed (exit {}). Run the command above manually.", s.code().unwrap_or(1)); + eprintln!( + "Installation failed (exit {}). Re-run install manually:", + s.code().unwrap_or(1) + ); + eprintln!(" {}", crate::updater::manual_install_one_liner()); process::exit(s.code().unwrap_or(1)); } Err(e) => { - eprintln!("Failed to run installer: {e}"); + eprintln!("Failed to launch installer: {e}"); + #[cfg(windows)] + eprintln!(" (is powershell.exe on PATH?)"); + #[cfg(not(windows))] + eprintln!(" (is bash + curl on PATH?)"); process::exit(1); } } diff --git a/libs/cua-driver-rs/crates/cua-driver/src/main.rs b/libs/cua-driver-rs/crates/cua-driver/src/main.rs index 89d839dfb2..30ef24d395 100644 --- a/libs/cua-driver-rs/crates/cua-driver/src/main.rs +++ b/libs/cua-driver-rs/crates/cua-driver/src/main.rs @@ -32,6 +32,7 @@ mod proxy; mod serve; mod skills; mod telemetry; +mod updater; mod version_check; use std::sync::Arc; diff --git a/libs/cua-driver-rs/crates/cua-driver/src/updater.rs b/libs/cua-driver-rs/crates/cua-driver/src/updater.rs new file mode 100644 index 0000000000..eda7f021de --- /dev/null +++ b/libs/cua-driver-rs/crates/cua-driver/src/updater.rs @@ -0,0 +1,99 @@ +//! `cua-driver update --apply` implementation. +//! +//! Delegates the actual install work to the canonical installer scripts: +//! - Unix: `libs/cua-driver/scripts/install.sh` (delegates to +//! `_install-rust.sh` when `--backend=rust`) +//! - Windows: `libs/cua-driver/scripts/install.ps1` +//! +//! Why not reimplement the download / atomic-swap / GC in Rust? Those scripts +//! already solve the hard problems: +//! - target-triple → asset-name mapping (per-OS, per-arch) +//! - per-version dir layout (`packages/releases/-/`) +//! - atomic upgrade — symlink retarget on Unix, NTFS directory-junction +//! retarget on Windows. A running daemon survives the swap because the +//! kernel keeps the old inode alive (Unix) or the junction flip is a +//! reparse-point swap that doesn't touch the locked .exe (Windows). +//! - GC of stale per-version dirs (`CUA_DRIVER_RS_KEEP_VERSIONS`) +//! - PATH wiring +//! +//! Treating "update" as a pinned re-install with `CUA_DRIVER_RS_VERSION` set +//! keeps install + update reading from one source of truth. Improvements to +//! the on-disk layout ship in the scripts and benefit both code paths. + +use std::process::{Command, ExitStatus}; + +/// Canonical install-script URLs. Match what the docs print as the one-liner; +/// users who run `cua-driver update --apply` and re-run the printed manual +/// command land at the exact same script. Per-OS gating keeps the unused +/// constant from triggering `dead_code` on the platform that doesn't use it. +#[cfg(not(windows))] +const CANONICAL_INSTALL_SH: &str = + "https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.sh"; +#[cfg(windows)] +const CANONICAL_INSTALL_PS1: &str = + "https://raw.githubusercontent.com/trycua/cua/main/libs/cua-driver/scripts/install.ps1"; + +/// The env var both scripts honour to pin the target release tag. Set to a +/// bare version like `"0.2.18"` (no `cua-driver-rs-v` prefix). See +/// `libs/cua-driver/scripts/_install-rust.sh` + `install.ps1`. +const VERSION_PIN_ENV: &str = "CUA_DRIVER_RS_VERSION"; + +/// Invoke the canonical installer pinned to `version`. Returns the +/// installer's exit status so the caller can produce the right +/// "succeeded / failed — re-run manually" message. +pub fn run_install_script(version: &str) -> std::io::Result { + #[cfg(windows)] + { + // Match the documented Windows one-liner: `irm | iex`. + // -ExecutionPolicy Bypass lets the downloaded script run on + // machines with the default restricted policy without requiring + // the user to Set-ExecutionPolicy first. -NoProfile keeps any + // user profile script from racing the install. + let pwsh_cmd = format!("iwr -useb {CANONICAL_INSTALL_PS1} | iex"); + Command::new("powershell.exe") + .env(VERSION_PIN_ENV, version) + .args([ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + &pwsh_cmd, + ]) + .status() + } + + #[cfg(not(windows))] + { + // Match the canonical curl-piped-to-bash invocation. `--backend=rust` + // is the explicit selector — without it the canonical install.sh + // auto-detects on macOS and would install the Swift driver instead. + let bash_cmd = format!( + "curl -fsSL {CANONICAL_INSTALL_SH} | bash -s -- install --backend=rust" + ); + Command::new("bash") + .env(VERSION_PIN_ENV, version) + .args(["-c", &bash_cmd]) + .status() + } +} + +/// True if the local cua-driver daemon is currently accepting connections +/// on its default socket / named pipe. Used post-install to decide whether +/// to print the "restart the daemon to pick up the new binary" hint. +pub fn daemon_is_running() -> bool { + crate::serve::is_daemon_listening(&crate::serve::default_socket_path()) +} + +/// The platform-appropriate manual re-install command, used in both the +/// "available, run --apply" preview and the "apply failed, retry manually" +/// error message. Kept here so both messages stay in sync. +pub fn manual_install_one_liner() -> String { + #[cfg(windows)] + { + format!("irm {CANONICAL_INSTALL_PS1} | iex") + } + #[cfg(not(windows))] + { + format!("curl -fsSL {CANONICAL_INSTALL_SH} | bash -s -- install --backend=rust") + } +}