aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-07 21:53:59 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-07 21:54:29 +0300
commit6f459691056b68c2c99d8341b5f8dd4b811a53c1 (patch)
tree24c780f3813945feef4c0b48be1645ac22067691 /src
parent0a635a3fc1e971030079945a447c73e2b24489f8 (diff)
fix(update): keep windows installs on preview
Diffstat (limited to 'src')
-rw-r--r--src/cli.rs46
-rw-r--r--src/config/model.rs15
-rw-r--r--src/update.rs94
3 files changed, 119 insertions, 36 deletions
diff --git a/src/cli.rs b/src/cli.rs
index 3650ebdb..88408fc8 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -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();