diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index 325b587..7906417 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -301,7 +301,7 @@ async fn main() -> Result<()> { return run_file_cat_command(&args); } if args.server.as_deref() == Some("update") { - return run_update(&config, parse_update_options(&args.command)?); + return run_update(&config, &args, parse_update_options(&args.command)?); } if args.server.as_deref() == Some("trace") { return run_trace_command(&args); @@ -4097,16 +4097,18 @@ impl UpdateRole { } } - fn installer_arg_for_os(self, os: &str) -> Result<&'static str> { + fn local_installer_arg_for_os(self, os: &str) -> Option<&'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")) - } + UpdateRole::Auto | UpdateRole::Client | UpdateRole::Both => Some("client"), + UpdateRole::Server => None, }; } - Ok(self.installer_arg()) + Some(self.installer_arg()) + } + + fn updates_remote_server_from_os(self, os: &str) -> bool { + os == "windows" && matches!(self, UpdateRole::Server | UpdateRole::Both) } } @@ -4423,7 +4425,11 @@ fn url_reachable(url: &str) -> Result { Ok(status.success()) } -fn run_update(config: &dosh::config::ClientConfig, options: UpdateOptions) -> Result<()> { +fn run_update( + config: &dosh::config::ClientConfig, + args: &Args, + options: UpdateOptions, +) -> Result<()> { let repo = config .update_repo .clone() @@ -4432,14 +4438,24 @@ fn run_update(config: &dosh::config::ClientConfig, options: UpdateOptions) -> Re 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_for_os(os)?; + let local_installer_role = options.role.local_installer_arg_for_os(os); + let update_remote_server = options.role.updates_remote_server_from_os(os); if options.check_only { let local_version = env!("CARGO_PKG_VERSION"); let latest_tag = latest_release_tag(&repo)?; println!("dosh {local_version}"); println!("repo: {repo}"); - println!("installer: {installer}"); - println!("role: {installer_role}"); + if let Some(local_installer_role) = local_installer_role { + println!("installer: {installer}"); + println!("role: {local_installer_role}"); + } + if update_remote_server { + println!( + "remote_installer: {}", + update_installer_url(&raw_base, "linux") + ); + println!("remote_server: {}", config.server); + } match latest_tag.as_deref() { Some(tag) => { let latest_version = release_version_from_tag(tag); @@ -4450,7 +4466,9 @@ fn run_update(config: &dosh::config::ClientConfig, options: UpdateOptions) -> Re } None => println!("latest: unknown"), } - if let Some(latest_url) = latest_release_download_url(&repo, &artifact) { + if let Some(latest_url) = + local_installer_role.and_then(|_| latest_release_download_url(&repo, &artifact)) + { let mut status = "missing"; let mut display_url = latest_url.clone(); if let Some(tag_url) = @@ -4481,15 +4499,27 @@ fn run_update(config: &dosh::config::ClientConfig, options: UpdateOptions) -> Re env!("CARGO_PKG_VERSION"), latest_release_tag(&repo)?.as_deref(), ); - run_update_installer( - config, - &repo, - &installer, - installer_role, - &update_cache.display().to_string(), - &use_prebuilt, - binary_version.as_deref(), - )?; + if update_remote_server { + run_remote_server_update( + config, + args, + &repo, + &raw_base, + &use_prebuilt, + binary_version.as_deref(), + )?; + } + if let Some(local_installer_role) = local_installer_role { + run_update_installer( + config, + &repo, + &installer, + local_installer_role, + &update_cache.display().to_string(), + &use_prebuilt, + binary_version.as_deref(), + )?; + } Ok(()) } @@ -4546,6 +4576,31 @@ fn run_update_installer( Ok(()) } +fn run_remote_server_update( + config: &dosh::config::ClientConfig, + args: &Args, + repo: &str, + raw_base: &str, + use_prebuilt: &str, + binary_version: Option<&str>, +) -> Result<()> { + anyhow::ensure!( + config.server != "user@example.com", + "set server in ~/.config/dosh/client.toml before running dosh update --server" + ); + let installer = update_installer_url(raw_base, "linux"); + let script = + remote_unix_server_update_script(config, repo, &installer, use_prebuilt, binary_version); + run_remote_script( + &config.server, + args.ssh_port.or(config.ssh_port), + args.ssh_key.as_deref(), + args.ssh_known_hosts.as_deref(), + &script, + "run remote Dosh server update", + ) +} + fn unix_update_script( config: &dosh::config::ClientConfig, repo: &str, @@ -4580,6 +4635,29 @@ fn unix_update_script( script } +fn remote_unix_server_update_script( + config: &dosh::config::ClientConfig, + repo: &str, + installer: &str, + use_prebuilt: &str, + binary_version: Option<&str>, +) -> String { + let mut env = format!( + "DOSH_REPO={} DOSH_PORT={} DOSH_UPDATE_QUIET=1 DOSH_USE_PREBUILT={}", + shell_word(repo), + config.update_port.unwrap_or(config.dosh_port), + shell_word(use_prebuilt) + ); + if let Some(version) = binary_version { + env.push_str(&format!(" DOSH_BINARY_VERSION={}", shell_word(version))); + } + format!( + "curl -fsSL {} | {} sh -s -- server", + shell_word(installer), + env + ) +} + fn windows_update_script( config: &dosh::config::ClientConfig, repo: &str, @@ -10257,25 +10335,25 @@ mod tests { queue_or_send_stream_data, queue_pending_user_input, queue_stale_pending_user_input, raw_contains_host_table, recv_response_until, refresh_live_addr, release_artifact_name_for, release_tag_download_url, release_tag_from_effective_url, release_version_from_tag, - render_frame_bytes, render_status_clear, render_status_overlay, requested_env, - resolve_forward_agent_endpoint, resolved_startup_command, retire_stream_state, - retransmit_stream_closes, retransmit_stream_eofs, retransmit_stream_opens, - retransmit_stream_window_adjusts, rewrite_forward_command, sanitize_trace_name, - selected_predict_mode, selected_udp_host, send_stream_eof, server_version_mismatch, - should_flush_terminal_input_after_contact, should_health_log_client_start, - should_hold_during_startup_gate, should_hold_post_submit_input, - should_reconnect_before_input_for_local_sleep, should_repaint_idle_terminal, - should_strip_unowned_terminal_reports, split_after_command_submit, split_trace_tokens, - ssh_command_target, ssh_config_uses_proxy, ssh_config_word_for_os, ssh_destination_host, - ssh_username, ssh_with_user, startup_command, status_ssh_target, strip_stale_mouse_reports, - strip_terminal_focus_reports, strip_unowned_terminal_reports, summarize_trace_file, - summarize_trace_file_with_mode, terminal_private_mode_transition, toml_bare_key_or_quoted, - top_trace_events, 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_deferred_update_script, windows_effective_url_script, - windows_mode_from_readonly, windows_readonly_from_mode, windows_update_script, - windows_url_reachable_script, + remote_unix_server_update_script, render_frame_bytes, render_status_clear, + render_status_overlay, requested_env, resolve_forward_agent_endpoint, + resolved_startup_command, retire_stream_state, retransmit_stream_closes, + retransmit_stream_eofs, retransmit_stream_opens, retransmit_stream_window_adjusts, + rewrite_forward_command, sanitize_trace_name, selected_predict_mode, selected_udp_host, + send_stream_eof, server_version_mismatch, should_flush_terminal_input_after_contact, + should_health_log_client_start, should_hold_during_startup_gate, + should_hold_post_submit_input, should_reconnect_before_input_for_local_sleep, + should_repaint_idle_terminal, should_strip_unowned_terminal_reports, + split_after_command_submit, split_trace_tokens, ssh_command_target, ssh_config_uses_proxy, + ssh_config_word_for_os, ssh_destination_host, ssh_username, ssh_with_user, startup_command, + status_ssh_target, strip_stale_mouse_reports, strip_terminal_focus_reports, + strip_unowned_terminal_reports, summarize_trace_file, summarize_trace_file_with_mode, + terminal_private_mode_transition, toml_bare_key_or_quoted, top_trace_events, + 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_deferred_update_script, windows_effective_url_script, windows_mode_from_readonly, + windows_readonly_from_mode, windows_update_script, windows_url_reachable_script, }; use dosh::config::{ClientConfig, CommandExtension, HostConfig}; use dosh::native::EnvVar; @@ -12461,21 +12539,31 @@ mod tests { } #[test] - fn windows_update_roles_are_client_only() { + fn windows_update_roles_split_local_client_and_remote_server() { assert_eq!( - UpdateRole::Auto.installer_arg_for_os("windows").unwrap(), - "client" + UpdateRole::Auto.local_installer_arg_for_os("windows"), + Some("client") ); assert_eq!( - UpdateRole::Client.installer_arg_for_os("windows").unwrap(), - "client" + UpdateRole::Client.local_installer_arg_for_os("windows"), + Some("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" + UpdateRole::Server.local_installer_arg_for_os("windows"), + None ); + assert_eq!( + UpdateRole::Both.local_installer_arg_for_os("windows"), + Some("client") + ); + assert!(UpdateRole::Server.updates_remote_server_from_os("windows")); + assert!(UpdateRole::Both.updates_remote_server_from_os("windows")); + assert!(!UpdateRole::Client.updates_remote_server_from_os("windows")); + assert_eq!( + UpdateRole::Both.local_installer_arg_for_os("linux"), + Some("both") + ); + assert!(!UpdateRole::Both.updates_remote_server_from_os("linux")); } #[test] @@ -12523,6 +12611,31 @@ mod tests { assert!(!windows.contains(" sh -s ")); } + #[test] + fn windows_server_update_uses_remote_unix_installer_script() { + let config = ClientConfig { + server: "palav".to_string(), + dosh_port: 50000, + update_port: Some(50001), + ..ClientConfig::default() + }; + let script = remote_unix_server_update_script( + &config, + "https://git.palav.dev/Palav/dosh.git", + "https://git.palav.dev/Palav/dosh/raw/branch/main/install.sh", + "1", + Some("v1.0.0-rc41"), + ); + assert!(script.contains("curl -fsSL")); + assert!(script.contains("install.sh")); + assert!(script.contains("DOSH_REPO='https://git.palav.dev/Palav/dosh.git'")); + assert!(script.contains("DOSH_PORT=50001")); + assert!(script.contains("DOSH_BINARY_VERSION='v1.0.0-rc41'")); + assert!(script.contains(" sh -s -- server")); + assert!(!script.contains("install.ps1")); + assert!(!script.contains("DOSH_ROLE='client'")); + } + #[test] fn windows_update_script_waits_for_parent_before_replacing_locked_exe() { let installer = windows_update_script(