From b87f277e2dd949f28c4819d07b3a5bbf8e2c88e6 Mon Sep 17 00:00:00 2001 From: DuProcess <273172371+DuProcess@users.noreply.github.com> Date: Sat, 11 Jul 2026 11:29:41 -0400 Subject: [PATCH] Use native installer for Windows updates --- README.md | 1 + src/bin/dosh-client.rs | 231 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 216 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 3a2fe93..0d61415 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ dosh setup HOST dosh HOST dosh HOST COMMAND dosh update +dosh update --client|--server|--both ``` Useful commands: diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index 4ed2a22..e51a768 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -2864,6 +2864,18 @@ impl UpdateRole { UpdateRole::Both => "both", } } + + fn installer_arg_for_os(self, os: &str) -> Result<&'static str> { + if os == "windows" { + return match self { + UpdateRole::Auto | UpdateRole::Client => Ok("client"), + UpdateRole::Server | UpdateRole::Both => { + Err(anyhow!("Windows installs support client updates only")) + } + }; + } + Ok(self.installer_arg()) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -2932,6 +2944,18 @@ fn release_artifact_name() -> &'static str { } } +fn update_installer_name(os: &str) -> &'static str { + if os == "windows" { + "install.ps1" + } else { + "install.sh" + } +} + +fn update_installer_url(raw_base: &str, os: &str) -> String { + format!("{raw_base}/raw/branch/main/{}", update_installer_name(os)) +} + fn latest_release_download_url(repo: &str, artifact: &str) -> Option { let web = repo .strip_suffix(".git") @@ -3044,9 +3068,10 @@ fn run_update(config: &dosh::config::ClientConfig, options: UpdateOptions) -> Re .clone() .unwrap_or_else(|| "https://git.palav.dev/Palav/dosh.git".to_string()); let raw_base = raw_base_from_repo(&repo); - let installer = format!("{raw_base}/raw/branch/main/install.sh"); + let os = std::env::consts::OS; + let installer = update_installer_url(&raw_base, os); let artifact = release_artifact_name(); - let installer_role = options.role.installer_arg(); + let installer_role = options.role.installer_arg_for_os(os)?; if options.check_only { let local_version = env!("CARGO_PKG_VERSION"); let latest_tag = latest_release_tag(&repo)?; @@ -3089,13 +3114,78 @@ fn run_update(config: &dosh::config::ClientConfig, options: UpdateOptions) -> Re .join("dosh") .join("source"); let use_prebuilt = std::env::var("DOSH_USE_PREBUILT").unwrap_or_else(|_| "1".to_string()); + let status = run_update_installer( + config, + &repo, + &installer, + installer_role, + &update_cache.display().to_string(), + &use_prebuilt, + )?; + if !status.success() { + return Err(anyhow!("dosh update failed with status {status}")); + } + Ok(()) +} + +fn run_update_installer( + config: &dosh::config::ClientConfig, + repo: &str, + installer: &str, + installer_role: &str, + update_cache: &str, + use_prebuilt: &str, +) -> Result { + if cfg!(windows) { + let script = windows_update_script( + config, + repo, + installer, + installer_role, + update_cache, + use_prebuilt, + ); + return Command::new("powershell.exe") + .arg("-NoProfile") + .arg("-ExecutionPolicy") + .arg("Bypass") + .arg("-Command") + .arg(script) + .stdin(Stdio::null()) + .status() + .context("run dosh update installer"); + } + let script = unix_update_script( + config, + repo, + installer, + installer_role, + update_cache, + use_prebuilt, + ); + Command::new("sh") + .arg("-c") + .arg(script) + .stdin(Stdio::null()) + .status() + .context("run dosh update installer") +} + +fn unix_update_script( + config: &dosh::config::ClientConfig, + repo: &str, + installer: &str, + installer_role: &str, + update_cache: &str, + use_prebuilt: &str, +) -> String { let mut script = format!( "curl -fsSL {} | DOSH_REPO={} DOSH_PORT={} DOSH_UPDATE_CACHE={} DOSH_UPDATE_QUIET=1 DOSH_USE_PREBUILT={} sh -s -- {}", - shell_word(&installer), - shell_word(&repo), + shell_word(installer), + shell_word(repo), config.update_port.unwrap_or(config.dosh_port), - shell_word(&update_cache.display().to_string()), - shell_word(&use_prebuilt), + shell_word(update_cache), + shell_word(use_prebuilt), shell_word(installer_role) ); if config.server != "user@example.com" { @@ -3104,16 +3194,47 @@ fn run_update(config: &dosh::config::ClientConfig, options: UpdateOptions) -> Re if let Some(host) = &config.dosh_host { script.push_str(&format!(" --dosh-host {}", shell_word(host))); } - let status = Command::new("sh") - .arg("-c") - .arg(script) - .stdin(Stdio::null()) - .status() - .context("run dosh update installer")?; - if !status.success() { - return Err(anyhow!("dosh update failed with status {status}")); + script +} + +fn windows_update_script( + config: &dosh::config::ClientConfig, + repo: &str, + installer: &str, + installer_role: &str, + update_cache: &str, + use_prebuilt: &str, +) -> String { + let mut script = String::from("$ErrorActionPreference='Stop';"); + script.push_str("$ProgressPreference='SilentlyContinue';"); + for (name, value) in [ + ("DOSH_REPO", repo), + ("DOSH_ROLE", installer_role), + ( + "DOSH_PORT", + &config.update_port.unwrap_or(config.dosh_port).to_string(), + ), + ("DOSH_UPDATE_CACHE", update_cache), + ("DOSH_UPDATE_QUIET", "1"), + ("DOSH_USE_PREBUILT", use_prebuilt), + ] { + script.push_str(&format!("$env:{name}={};", powershell_string(value))); } - Ok(()) + if config.server != "user@example.com" { + script.push_str(&format!( + "$env:DOSH_SERVER={};", + powershell_string(&config.server) + )); + } + if let Some(host) = &config.dosh_host { + script.push_str(&format!("$env:DOSH_HOST={};", powershell_string(host))); + } + script.push_str(&format!("irm {} | iex", powershell_string(installer))); + script +} + +fn powershell_string(value: &str) -> String { + format!("'{}'", value.replace('\'', "''")) } #[derive(Debug, Clone, PartialEq, Eq)] @@ -7654,7 +7775,8 @@ mod tests { should_repaint_idle_alternate_screen, split_after_command_submit, ssh_destination_host, ssh_username, ssh_with_user, startup_command, status_ssh_target, strip_stale_mouse_reports, strip_terminal_focus_reports, terminal_private_mode_transition, toml_bare_key_or_quoted, - update_version_status, upsert_managed_block, valid_forward_host, vscode_safe_alias, + unix_update_script, update_installer_url, update_version_status, upsert_managed_block, + valid_forward_host, vscode_safe_alias, windows_update_script, }; use dosh::config::{ClientConfig, CommandExtension, HostConfig}; use dosh::native::EnvVar; @@ -8801,6 +8923,83 @@ mod tests { assert!(parse_update_options(&["--client".to_string(), "--server".to_string()]).is_err()); } + #[test] + fn update_installer_uses_platform_native_script() { + assert_eq!( + update_installer_url("https://git.palav.dev/Palav/dosh", "linux"), + "https://git.palav.dev/Palav/dosh/raw/branch/main/install.sh" + ); + assert_eq!( + update_installer_url("https://git.palav.dev/Palav/dosh", "macos"), + "https://git.palav.dev/Palav/dosh/raw/branch/main/install.sh" + ); + assert_eq!( + update_installer_url("https://git.palav.dev/Palav/dosh", "windows"), + "https://git.palav.dev/Palav/dosh/raw/branch/main/install.ps1" + ); + } + + #[test] + fn windows_update_roles_are_client_only() { + assert_eq!( + UpdateRole::Auto.installer_arg_for_os("windows").unwrap(), + "client" + ); + assert_eq!( + UpdateRole::Client.installer_arg_for_os("windows").unwrap(), + "client" + ); + assert!(UpdateRole::Server.installer_arg_for_os("windows").is_err()); + assert!(UpdateRole::Both.installer_arg_for_os("windows").is_err()); + assert_eq!( + UpdateRole::Both.installer_arg_for_os("linux").unwrap(), + "both" + ); + } + + #[test] + fn update_scripts_are_native_to_the_platform() { + let config = ClientConfig { + server: "palav".to_string(), + dosh_host: Some("dosh.example.com".to_string()), + dosh_port: 50000, + update_port: Some(50001), + ..ClientConfig::default() + }; + let unix = unix_update_script( + &config, + "https://git.palav.dev/Palav/dosh.git", + "https://git.palav.dev/Palav/dosh/raw/branch/main/install.sh", + "both", + "/tmp/dosh-cache", + "1", + ); + assert!(unix.contains("curl -fsSL")); + assert!(unix.contains("install.sh")); + assert!(unix.contains("sh -s -- 'both'")); + assert!(unix.contains("--server 'palav'")); + assert!(unix.contains("--dosh-host 'dosh.example.com'")); + + let windows = windows_update_script( + &config, + "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", + ); + assert!( + windows.contains( + "irm 'https://git.palav.dev/Palav/dosh/raw/branch/main/install.ps1' | iex" + ) + ); + assert!(windows.contains("$env:DOSH_ROLE='client';")); + assert!(windows.contains("$env:DOSH_SERVER='palav';")); + assert!(windows.contains("$env:DOSH_HOST='dosh.example.com';")); + assert!(!windows.contains("install.sh")); + assert!(!windows.contains(" sh -s ")); + } + #[test] fn status_uses_host_alias_and_user() { let host = HostConfig {