diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-07 21:53:59 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-07 21:54:29 +0300 |
| commit | 6f459691056b68c2c99d8341b5f8dd4b811a53c1 (patch) | |
| tree | 24c780f3813945feef4c0b48be1645ac22067691 /src | |
| parent | 0a635a3fc1e971030079945a447c73e2b24489f8 (diff) | |
fix(update): keep windows installs on preview
Diffstat (limited to 'src')
| -rw-r--r-- | src/cli.rs | 46 | ||||
| -rw-r--r-- | src/config/model.rs | 15 | ||||
| -rw-r--r-- | src/update.rs | 94 |
3 files changed, 119 insertions, 36 deletions
@@ -79,7 +79,7 @@ fn channel_set(args: &[String]) -> std::io::Result<i32> { return Ok(2); }; - if let Some(reason) = channel_set_preview_rejection( + if let Some(reason) = channel_set_rejection( channel, crate::update::preview_channel_rejection_for_current_install(), ) { @@ -151,13 +151,21 @@ fn parse_channel_set_arg(args: &[String]) -> Option<&str> { } } -fn channel_set_preview_rejection( +fn channel_set_rejection( channel: &str, install_rejection: Option<&'static str>, ) -> Option<&'static str> { - (channel == "preview") - .then_some(install_rejection) - .flatten() + if cfg!(windows) && channel == "stable" { + return Some( + "stable channel is not available on Windows yet; Windows builds are preview-only", + ); + } + + if channel == "preview" { + return install_rejection; + } + + None } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -898,14 +906,34 @@ mod tests { #[test] fn channel_set_rejects_package_managed_preview_before_config_write() { assert_eq!( - super::channel_set_preview_rejection("preview", Some("no preview")), + super::channel_set_rejection("preview", Some("no preview")), Some("no preview") ); assert_eq!( - super::channel_set_preview_rejection("stable", Some("no preview")), - None + super::channel_set_rejection("stable", Some("no preview")), + if cfg!(windows) { + Some( + "stable channel is not available on Windows yet; Windows builds are preview-only", + ) + } else { + None + } + ); + assert_eq!(super::channel_set_rejection("preview", None), None); + } + + #[test] + fn channel_set_rejects_stable_only_on_windows() { + assert_eq!( + super::channel_set_rejection("stable", None), + if cfg!(windows) { + Some( + "stable channel is not available on Windows yet; Windows builds are preview-only", + ) + } else { + None + } ); - assert_eq!(super::channel_set_preview_rejection("preview", None), None); } #[test] diff --git a/src/config/model.rs b/src/config/model.rs index c36150f6..528a940c 100644 --- a/src/config/model.rs +++ b/src/config/model.rs @@ -30,17 +30,26 @@ impl UpdateChannelConfig { #[derive(Debug, Clone, Copy, Deserialize)] #[serde(default)] pub struct UpdateConfig { + #[serde(default = "default_update_channel")] pub channel: UpdateChannelConfig, } impl Default for UpdateConfig { fn default() -> Self { Self { - channel: UpdateChannelConfig::Stable, + channel: default_update_channel(), } } } +fn default_update_channel() -> UpdateChannelConfig { + if cfg!(windows) { + UpdateChannelConfig::Preview + } else { + UpdateChannelConfig::Stable + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)] #[serde(rename_all = "lowercase")] pub enum ToastDelivery { @@ -714,9 +723,9 @@ mod tests { use super::*; #[test] - fn update_channel_defaults_stable_and_parses() { + fn update_channel_defaults_for_platform_and_parses() { let default_config = Config::default(); - assert_eq!(default_config.update.channel, UpdateChannelConfig::Stable); + assert_eq!(default_config.update.channel, default_update_channel()); let toml = r#" [update] diff --git a/src/update.rs b/src/update.rs index 3337d6e9..bedcc1b0 100644 --- a/src/update.rs +++ b/src/update.rs @@ -10,11 +10,16 @@ use std::collections::BTreeMap; use std::env; #[cfg(not(windows))] use std::fs; -use std::io::{self, BufRead, BufReader, IsTerminal, Write}; +#[cfg(not(windows))] +use std::io; +#[cfg(not(windows))] +use std::io::{BufRead, BufReader, IsTerminal, Write}; use std::path::{Path, PathBuf}; use std::process::Command; +#[cfg(not(windows))] use std::time::{Duration, Instant}; +#[cfg(not(windows))] use interprocess::local_socket::traits::Stream as _; use serde::{Deserialize, Deserializer}; @@ -29,9 +34,13 @@ const MISE_INSTALLS_DIR_ENV: &str = "MISE_INSTALLS_DIR"; const FAKE_UPDATE_VERSION_ENV: &str = "HERDR_FAKE_UPDATE_VERSION"; const FAKE_UPDATE_NOTES_VERSION_ENV: &str = "HERDR_FAKE_UPDATE_NOTES_VERSION"; const DEFAULT_FAKE_UPDATE_NOTES_VERSION: &str = "0.3.0"; +#[cfg(not(windows))] const SERVER_STOP_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5); +#[cfg(not(windows))] const SERVER_HANDOFF_REQUEST_TIMEOUT: Duration = Duration::from_secs(240); +#[cfg(not(windows))] const SERVER_HANDOFF_CONFIRM_TIMEOUT: Duration = Duration::from_secs(30); +#[cfg(not(windows))] const SERVER_SHUTDOWN_POLL_INTERVAL: Duration = Duration::from_millis(100); fn fake_release_notes_body(version: &str) -> String { let notes_version = env::var(FAKE_UPDATE_NOTES_VERSION_ENV) @@ -156,6 +165,7 @@ impl<'de> Deserialize<'de> for AssetRef { struct UpdateManifest { version: String, /// Thin-client protocol spoken by this release, when advertised by the manifest. + #[cfg(not(windows))] protocol: Option<u32>, notes: String, assets: BTreeMap<String, AssetRef>, @@ -259,6 +269,7 @@ struct ReleaseInfo { channel: UpdateChannel, build_id: Option<String>, commit: Option<String>, + #[cfg(not(windows))] target_protocol: Option<u32>, download_url: String, sha256: Option<String>, @@ -358,6 +369,7 @@ fn release_info_from_manifest(manifest: &UpdateManifest) -> Result<Option<Releas channel: UpdateChannel::Stable, build_id: None, commit: None, + #[cfg(not(windows))] target_protocol: manifest.protocol, download_url, sha256: asset.sha256.clone(), @@ -442,6 +454,7 @@ fn release_info_from_preview_manifest( channel: UpdateChannel::Preview, build_id: Some(build_id.to_string()), commit: Some(manifest.commit.clone()), + #[cfg(not(windows))] target_protocol: Some(manifest.protocol), download_url, sha256: asset.sha256.clone(), @@ -662,6 +675,7 @@ fn running_inside_herdr() -> bool { running_inside_herdr_env(env::var(crate::HERDR_ENV_VAR).ok().as_deref()) } +#[cfg(not(windows))] fn client_protocol_server_is_running_at(socket_path: &Path) -> bool { if !socket_path.exists() { return false; @@ -670,14 +684,17 @@ fn client_protocol_server_is_running_at(socket_path: &Path) -> bool { crate::ipc::connect_local_stream(socket_path).is_ok() } +#[cfg(not(windows))] fn client_protocol_server_is_running() -> bool { client_protocol_server_is_running_at(&crate::server::socket_paths::client_socket_path()) } +#[cfg(not(windows))] fn version_label(version: Option<&str>) -> &str { version.unwrap_or("unknown") } +#[cfg(not(windows))] fn update_requires_server_restart( server: &crate::api::RuntimeStatus, release: &ReleaseInfo, @@ -688,6 +705,7 @@ fn update_requires_server_restart( } } +#[cfg(not(windows))] fn server_supports_live_handoff(server: &crate::api::RuntimeStatus) -> bool { server .capabilities @@ -695,6 +713,7 @@ fn server_supports_live_handoff(server: &crate::api::RuntimeStatus) -> bool { .is_some_and(|capabilities| capabilities.live_handoff) } +#[cfg(not(windows))] fn parse_stop_old_servers_after_update_response(input: &str, default_yes: bool) -> Option<bool> { let trimmed = input.trim().to_ascii_lowercase(); match trimmed.as_str() { @@ -706,12 +725,14 @@ fn parse_stop_old_servers_after_update_response(input: &str, default_yes: bool) } #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg(not(windows))] struct RunningServerUpdatePlan { target: RunningUpdateTarget, server: crate::api::RuntimeStatus, requires_server_restart: bool, } +#[cfg(not(windows))] impl RunningServerUpdatePlan { fn label(&self) -> &str { &self.target.label @@ -739,12 +760,14 @@ impl RunningServerUpdatePlan { } #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg(not(windows))] struct RunningServerUpdateDecision { plan: RunningServerUpdatePlan, action: RunningServerUpdateAction, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg(not(windows))] enum RunningServerUpdateAction { None, LiveHandoff, @@ -752,6 +775,7 @@ enum RunningServerUpdateAction { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg(not(windows))] enum RunningServerUpdateOutcome { RestartDeferred, Stopped, @@ -763,6 +787,7 @@ enum RunningServerUpdateOutcome { } #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg(not(windows))] struct RunningSessionUpdateOutcome { session_label: String, target_noun: &'static str, @@ -773,6 +798,7 @@ struct RunningSessionUpdateOutcome { } #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg(not(windows))] enum FailedHandoffServerState { UpdatedServerRunning, OldServerRunning(crate::api::RuntimeStatus), @@ -780,6 +806,7 @@ enum FailedHandoffServerState { Unknown(String), } +#[cfg(not(windows))] fn plan_running_server_updates( release: &ReleaseInfo, ) -> Result<Vec<RunningServerUpdatePlan>, String> { @@ -837,6 +864,7 @@ fn plan_running_server_updates( } #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg(not(windows))] struct RunningUpdateTarget { name: Option<String>, label: String, @@ -847,6 +875,7 @@ struct RunningUpdateTarget { must_be_running: bool, } +#[cfg(not(windows))] fn running_update_targets() -> Result<Vec<RunningUpdateTarget>, String> { if crate::session::explicit_session_requested() { return Ok(vec![RunningUpdateTarget { @@ -913,6 +942,7 @@ fn running_update_targets() -> Result<Vec<RunningUpdateTarget>, String> { .collect()) } +#[cfg(not(windows))] fn target_client_protocol_server_is_running() -> Result<bool, String> { if crate::session::explicit_session_requested() || std::env::var_os(crate::api::SOCKET_PATH_ENV_VAR).is_some() @@ -951,6 +981,7 @@ pub(crate) fn parse_self_update_args(args: &[String]) -> Result<SelfUpdateOption Ok(options) } +#[cfg(not(windows))] fn prompt_to_stop_old_servers_before_update( plans: &[RunningServerUpdatePlan], release: &ReleaseInfo, @@ -999,6 +1030,7 @@ fn prompt_to_stop_old_servers_before_update( } } +#[cfg(not(windows))] fn confirm_running_server_update_action( plans: Vec<RunningServerUpdatePlan>, release: &ReleaseInfo, @@ -1053,6 +1085,7 @@ fn confirm_running_server_update_action( Ok(decisions) } +#[cfg(not(windows))] fn target_group_nouns(plans: &[&RunningServerUpdatePlan]) -> (&'static str, &'static str) { let all_sessions = plans.iter().all(|plan| plan.target_noun() == "session"); let all_servers = plans.iter().all(|plan| plan.target_noun() == "server"); @@ -1065,6 +1098,7 @@ fn target_group_nouns(plans: &[&RunningServerUpdatePlan]) -> (&'static str, &'st } } +#[cfg(not(windows))] fn prompt_to_complete_plain_update( decisions: &[RunningServerUpdateDecision], release: &ReleaseInfo, @@ -1121,6 +1155,7 @@ fn prompt_to_complete_plain_update( } } +#[cfg(not(windows))] fn mark_plain_update_stop_decisions( decisions: Vec<RunningServerUpdateDecision>, ) -> Vec<RunningServerUpdateDecision> { @@ -1133,6 +1168,7 @@ fn mark_plain_update_stop_decisions( .collect() } +#[cfg(not(windows))] fn print_running_session_update_summary( plans: &[RunningServerUpdatePlan], release: &ReleaseInfo, @@ -1164,6 +1200,7 @@ fn print_running_session_update_summary( eprintln!(); } +#[cfg(not(windows))] fn live_handoff_running_server_for_update( plan: &RunningServerUpdatePlan, release: &ReleaseInfo, @@ -1184,6 +1221,7 @@ fn live_handoff_running_server_for_update( Ok(()) } +#[cfg(not(windows))] fn runtime_matches_release(status: &crate::api::RuntimeStatus, release: &ReleaseInfo) -> bool { let protocol_matches = release .target_protocol @@ -1192,6 +1230,7 @@ fn runtime_matches_release(status: &crate::api::RuntimeStatus, release: &Release protocol_matches && version_matches } +#[cfg(not(windows))] fn classify_failed_live_handoff_state_at( socket_path: &Path, release: &ReleaseInfo, @@ -1206,6 +1245,7 @@ fn classify_failed_live_handoff_state_at( } } +#[cfg(not(windows))] fn prompt_to_stop_old_server_after_failed_handoff( plan: &RunningServerUpdatePlan, release: &ReleaseInfo, @@ -1254,6 +1294,7 @@ fn prompt_to_stop_old_server_after_failed_handoff( } } +#[cfg(not(windows))] fn recover_failed_live_handoff_for_update( plan: &RunningServerUpdatePlan, release: &ReleaseInfo, @@ -1310,6 +1351,7 @@ fn recover_failed_live_handoff_for_update( } } +#[cfg(not(windows))] fn reconnect_or_stop_guidance(plan: &RunningServerUpdatePlan) -> String { if let Some(command) = plan.attach_command() { format!( @@ -1324,6 +1366,7 @@ fn reconnect_or_stop_guidance(plan: &RunningServerUpdatePlan) -> String { } } +#[cfg(not(windows))] fn stop_server_via_api_at(socket_path: &Path, timeout: Duration) -> Result<(), String> { use crate::api::schema::{EmptyParams, Method}; @@ -1336,6 +1379,7 @@ fn stop_server_via_api_at(socket_path: &Path, timeout: Duration) -> Result<(), S ) } +#[cfg(not(windows))] fn send_server_update_method_at( socket_path: &Path, timeout: Duration, @@ -1404,6 +1448,7 @@ fn live_handoff_server_via_api_at(socket_path: &Path, timeout: Duration) -> Resu ) } +#[cfg(not(windows))] fn live_handoff_server_via_api_for_release_at( socket_path: &Path, timeout: Duration, @@ -1427,6 +1472,7 @@ fn live_handoff_server_via_api_for_release_at( ) } +#[cfg(not(windows))] fn live_handoff_server_via_api_for_update_at( socket_path: &Path, updated_exe: &Path, @@ -1440,6 +1486,7 @@ fn live_handoff_server_via_api_for_update_at( ) } +#[cfg(not(windows))] fn server_shutdown_confirmed_at(socket_path: &Path) -> Result<bool, String> { if !socket_path.exists() { return Ok(true); @@ -1464,6 +1511,7 @@ fn server_shutdown_confirmed_at(socket_path: &Path) -> Result<bool, String> { } } +#[cfg(not(windows))] fn wait_for_server_shutdown_at(socket_path: &Path, timeout: Duration) -> Result<(), String> { let deadline = Instant::now() + timeout; loop { @@ -1481,6 +1529,7 @@ fn wait_for_server_shutdown_at(socket_path: &Path, timeout: Duration) -> Result< } } +#[cfg(not(windows))] fn stop_running_server_for_update(plan: &RunningServerUpdatePlan) -> Result<(), String> { eprintln!("stopping herdr {} {}...", plan.target_noun(), plan.label()); stop_server_via_api_at(plan.socket_path(), SERVER_STOP_RESPONSE_TIMEOUT)?; @@ -1488,6 +1537,7 @@ fn stop_running_server_for_update(plan: &RunningServerUpdatePlan) -> Result<(), Ok(()) } +#[cfg(not(windows))] fn wait_for_server_handoff_at( socket_path: &Path, timeout: Duration, @@ -1501,6 +1551,7 @@ fn wait_for_server_handoff_at( ) } +#[cfg(not(windows))] fn wait_for_running_server_protocol_at( socket_path: &Path, timeout: Duration, @@ -1532,6 +1583,7 @@ fn wait_for_running_server_protocol_at( } } +#[cfg(not(windows))] fn apply_running_session_update_decisions( release: &ReleaseInfo, updated_exe: &Path, @@ -1570,6 +1622,7 @@ fn apply_running_session_update_decisions( Ok(outcomes) } +#[cfg(not(windows))] fn print_running_session_update_outcomes( outcomes: &[RunningSessionUpdateOutcome], release: &ReleaseInfo, @@ -1886,6 +1939,13 @@ fn homebrew_cellar_keg_root(path: &Path) -> Option<PathBuf> { /// Manual self-update command (`herdr update`). pub fn self_update(options: SelfUpdateOptions) -> Result<Version, String> { let channel = UpdateChannel::configured(); + #[cfg(windows)] + if channel == UpdateChannel::Stable { + return Err( + "Windows builds are preview-only for now; run `herdr channel set preview`".into(), + ); + } + if is_homebrew_managed_install() { if channel == UpdateChannel::Preview { return Err( @@ -1935,10 +1995,6 @@ pub fn self_update(options: SelfUpdateOptions) -> Result<Version, String> { } }; - let running_server_plans = plan_running_server_updates(&release)?; - let server_update_decisions = - confirm_running_server_update_action(running_server_plans, &release, options)?; - if let Some(commit) = &release.commit { tracing::info!(commit = %commit, build_id = ?release.build_id, "selected preview update build"); } @@ -1948,13 +2004,7 @@ pub fn self_update(options: SelfUpdateOptions) -> Result<Version, String> { #[cfg(windows)] { - if !options.live_handoff - && !prompt_to_complete_plain_update(&server_update_decisions, &release)? - { - eprintln!("Herdr was not updated."); - eprintln!("Stop running Herdr sessions when ready, then run `herdr update` again."); - return Ok(current); - } + let _ = options; eprintln!( "installing {} with the Windows installer...", @@ -1966,23 +2016,19 @@ pub fn self_update(options: SelfUpdateOptions) -> Result<Version, String> { install_windows_update_with_installer(channel)?; let updated_exe = windows_installed_herdr_exe_path()?; eprintln!("installed {}", release.label()); - let server_update_decisions = if options.live_handoff { - server_update_decisions - } else { - mark_plain_update_stop_decisions(server_update_decisions) - }; - let server_update_outcomes = apply_running_session_update_decisions( - &release, - &updated_exe, - server_update_decisions, - )?; print_outdated_integration_notice_with_updated_binary(&updated_exe); - - print_running_session_update_outcomes(&server_update_outcomes, &release); + eprintln!( + "Restart any running Herdr sessions to use {}.", + release.label() + ); } #[cfg(not(windows))] { + let running_server_plans = plan_running_server_updates(&release)?; + let server_update_decisions = + confirm_running_server_update_action(running_server_plans, &release, options)?; + eprintln!("downloading {}...", release.label()); let downloaded_update = download_update(&release)?; let updated_exe = downloaded_update.current_exe.clone(); |
