From 25e361dea8ffb8d7893125712c65f81c14a966d8 Mon Sep 17 00:00:00 2001 From: DuProcess <273172371+DuProcess@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:14:38 -0400 Subject: [PATCH] Improve Windows update parity --- install.ps1 | 20 ++++++++++++- src/bin/dosh-client.rs | 63 +++++++++++++++++++++++++++++++++------- tests/release_scripts.rs | 14 +++++++++ 3 files changed, 85 insertions(+), 12 deletions(-) diff --git a/install.ps1 b/install.ps1 index 19f13cc..59985f0 100644 --- a/install.ps1 +++ b/install.ps1 @@ -24,6 +24,24 @@ function Require-Command($Name) { } } +function Ensure-Cargo { + if (Get-Command cargo -ErrorAction SilentlyContinue) { + return + } + if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { + throw "cargo not found and winget is unavailable; install Rust from https://rustup.rs or set DOSH_USE_PREBUILT=1" + } + Write-Host "cargo not found; installing Rust toolchain with winget/rustup" + winget install --id Rustlang.Rustup -e --accept-package-agreements --accept-source-agreements + $cargoBin = Join-Path $HOME ".cargo\bin" + if (Test-Path $cargoBin) { + $env:Path = "$cargoBin;$env:Path" + } + if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) { + throw "cargo was installed but is not available in this terminal yet; open a new terminal and rerun dosh update" + } +} + function Normalize-Arch { $arch = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE } switch ($arch) { @@ -367,7 +385,7 @@ function Install-Prebuilt { } function Install-FromSource { - Require-Command cargo + Ensure-Cargo if (Test-Path "Cargo.toml") { $src = (Get-Location).Path } else { diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index 58000d5..d7cb274 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -4428,7 +4428,7 @@ fn run_update(config: &dosh::config::ClientConfig, options: UpdateOptions) -> Re env!("CARGO_PKG_VERSION"), latest_release_tag(&repo)?.as_deref(), ); - let status = run_update_installer( + run_update_installer( config, &repo, &installer, @@ -4437,9 +4437,6 @@ fn run_update(config: &dosh::config::ClientConfig, options: UpdateOptions) -> Re &use_prebuilt, binary_version.as_deref(), )?; - if !status.success() { - return Err(anyhow!("dosh update failed with status {status}")); - } Ok(()) } @@ -4451,7 +4448,7 @@ fn run_update_installer( update_cache: &str, use_prebuilt: &str, binary_version: Option<&str>, -) -> Result { +) -> Result<()> { if cfg!(windows) { let script = windows_update_script( config, @@ -4462,15 +4459,18 @@ fn run_update_installer( use_prebuilt, binary_version, ); - return Command::new("powershell.exe") + let script = windows_deferred_update_script(std::process::id(), &script); + Command::new("powershell.exe") .arg("-NoProfile") .arg("-ExecutionPolicy") .arg("Bypass") .arg("-Command") .arg(script) .stdin(Stdio::null()) - .status() - .context("run dosh update installer"); + .spawn() + .context("start deferred dosh update installer")?; + println!("dosh update started; the installer will run after this process exits"); + return Ok(()); } let script = unix_update_script( config, @@ -4481,12 +4481,16 @@ fn run_update_installer( use_prebuilt, binary_version, ); - Command::new("sh") + let status = Command::new("sh") .arg("-c") .arg(script) .stdin(Stdio::null()) .status() - .context("run dosh update installer") + .context("run dosh update installer")?; + if !status.success() { + return Err(anyhow!("dosh update failed with status {status}")); + } + Ok(()) } fn unix_update_script( @@ -4566,6 +4570,17 @@ fn windows_update_script( script } +fn windows_deferred_update_script(parent_pid: u32, installer_script: &str) -> String { + format!( + "$ErrorActionPreference='Stop';\ + $ProgressPreference='SilentlyContinue';\ + while (Get-Process -Id {parent_pid} -ErrorAction SilentlyContinue) {{ \ + Start-Sleep -Milliseconds 100 \ + }};\ + {installer_script}" + ) +} + fn powershell_string(value: &str) -> String { format!("'{}'", value.replace('\'', "''")) } @@ -10175,7 +10190,8 @@ mod tests { trace_report_warnings, unix_update_script, update_binary_version_for_installer, update_installer_url, update_version_status, upsert_managed_block, valid_forward_host, vscode_safe_alias, wake_repaint_retry_deadline, windows_command_word, - windows_mode_from_readonly, windows_readonly_from_mode, windows_update_script, + windows_deferred_update_script, windows_mode_from_readonly, windows_readonly_from_mode, + windows_update_script, }; use dosh::config::{ClientConfig, CommandExtension, HostConfig}; use dosh::native::EnvVar; @@ -12423,6 +12439,31 @@ mod tests { assert!(!windows.contains(" sh -s ")); } + #[test] + fn windows_update_script_waits_for_parent_before_replacing_locked_exe() { + let installer = windows_update_script( + &ClientConfig::default(), + "https://git.palav.dev/Palav/dosh.git", + "https://git.palav.dev/Palav/dosh/raw/branch/main/install.ps1", + "client", + "C:\\Users\\palav\\AppData\\Local\\dosh\\source", + "1", + None, + ); + let deferred = windows_deferred_update_script(4242, &installer); + assert!(deferred.contains("Get-Process -Id 4242 -ErrorAction SilentlyContinue")); + assert!(deferred.contains("Start-Sleep -Milliseconds 100")); + assert!( + deferred.contains( + "irm 'https://git.palav.dev/Palav/dosh/raw/branch/main/install.ps1' | iex" + ) + ); + assert!( + deferred.find("Get-Process -Id 4242").unwrap() + < deferred.find("irm 'https://git.palav.dev").unwrap() + ); + } + #[test] fn update_uses_local_tag_when_release_latest_is_older() { assert_eq!( diff --git a/tests/release_scripts.rs b/tests/release_scripts.rs index 1988a3e..dd8eb5c 100644 --- a/tests/release_scripts.rs +++ b/tests/release_scripts.rs @@ -145,6 +145,20 @@ fn windows_installer_reuses_persistent_source_update_cache() { ); } +#[test] +fn windows_installer_bootstraps_rust_for_source_fallback_like_unix() { + let install = include_str!("../install.sh"); + let ps1 = include_str!("../install.ps1"); + assert!(install.contains("ensure_cargo()")); + assert!(install.contains("cargo not found; installing Rust toolchain with rustup")); + assert!(ps1.contains("function Ensure-Cargo")); + assert!(ps1.contains("cargo not found; installing Rust toolchain with winget/rustup")); + assert!(ps1.contains("winget install --id Rustlang.Rustup")); + assert!(ps1.contains("--accept-package-agreements --accept-source-agreements")); + assert!(ps1.contains("Ensure-Cargo")); + assert!(!ps1.contains("Require-Command cargo")); +} + #[test] fn windows_installer_adds_user_path_idempotently() { let ps1 = include_str!("../install.ps1");