fix: summarize old servers and saved machines after herdr update (#4722)

* fix: summarize old servers and saved machines after herdr update

* fix: include reconnect and per-session restart commands in update summary
This commit is contained in:
Can Celik
2026-09-29 03:47:12 +03:00
committed by GitHub
parent d5680d84fd
commit 9dc3a1df2b
4 changed files with 225 additions and 18 deletions
@@ -141,6 +141,8 @@ Stable is the recommended channel for normal Windows use. Preview receives fixes
By default, `herdr update` installs the new binary and leaves endpoint-generation-1 servers and their pane processes running. Start Herdr again to reconnect with the updated client. Client-only changes work immediately. A new server-side action stays unavailable until that server is updated and restarted; the client shows a notice instead of disconnecting. Servers from before endpoint generation 1 need one final stop during update.
When it finishes, `herdr update` lists any running server still on the old version with the command that restarts it, and your saved SSH machines, which run their own Herdr. Run `herdr update` on each machine to update it too.
To opt into experimental live server handoff for supported running sessions, run:
```bash
@@ -141,6 +141,8 @@ Windows の通常利用には安定版を推奨します。プレビューでは
デフォルトでは、`herdr update` は新しいバイナリをインストールし、エンドポイント世代 1 のサーバーとペインプロセスを動かしたままにします。再度 Herdr を起動すると、更新済みクライアントで再接続します。クライアントだけの変更はすぐに使えます。新しいサーバー側アクションは、そのサーバーを更新して再起動するまで利用できず、クライアントは切断せずに通知を表示します。エンドポイント世代 1 より前のサーバーだけは、更新時に最後の 1 回の停止が必要です。
完了時に `herdr update` は、古いバージョンのまま動いているサーバーとそれを再起動するコマンド、そして独自の Herdr を動かしている保存済み SSH マシンを一覧表示します。各マシンでも `herdr update` を実行して更新してください。
対応する実行中セッションで実験的なライブサーバーハンドオフにオプトインするには:
```bash
@@ -141,6 +141,8 @@ Windows 的常规使用建议选择稳定版。预览版能更早获得修复,
默认情况下,`herdr update` 会安装新二进制文件,并让端点第 1 代服务器及其窗格进程继续运行。再次启动 Herdr 即可用更新后的客户端重新连接。纯客户端改动会立即生效。新的服务器端操作会保持不可用,直到对应服务器更新并重启;客户端会显示通知,而不会断开连接。只有早于端点第 1 代的服务器需要在更新时再停止最后一次。
完成后,`herdr update` 会列出仍在运行旧版本的服务器及重启它们的命令,以及运行各自 Herdr 的已保存 SSH 机器。请在每台机器上也运行 `herdr update` 来更新。
要对受支持的运行中会话启用实验性的实时服务器交接,运行:
```bash
+219 -18
View File
@@ -1781,26 +1781,23 @@ fn print_running_session_update_outcomes(
return;
}
let kept: Vec<KeptServer<'_>> = outcomes
.iter()
.filter(|outcome| outcome.outcome == RunningServerUpdateOutcome::CompatibleServerKept)
.map(|outcome| KeptServer {
label: &outcome.session_label,
version: version_label(outcome.server_version.as_deref()),
stop_command: &outcome.stop_command,
attach_command: outcome.attach_command.as_deref(),
})
.collect();
for line in kept_server_notice_lines(&kept, release.label()) {
eprintln!("{line}");
}
for outcome in outcomes {
match outcome.outcome {
RunningServerUpdateOutcome::CompatibleServerKept => {
eprintln!(
"{} {} kept running server v{}.",
outcome.target_noun,
outcome.session_label,
version_label(outcome.server_version.as_deref())
);
match &outcome.attach_command {
Some(command) => eprintln!(
"Run `{command}` to reconnect with the updated client. Restart the server later only if you need server-side changes from {}.",
release.label()
),
None => eprintln!(
"Reconnect with the same socket override to use the updated client. Restart the server later only if you need server-side changes from {}.",
release.label()
),
}
}
RunningServerUpdateOutcome::CompatibleServerKept => {}
RunningServerUpdateOutcome::LiveHandoffComplete => {
if let Some(command) = &outcome.attach_command {
eprintln!(
@@ -2190,6 +2187,7 @@ pub fn self_update(options: SelfUpdateOptions) -> Result<Version, String> {
"Open a new terminal, or reconnect SSH, then start Herdr again to use the updated client. Running servers remain active; restart them later only if you need server-side changes from {}.",
release.label()
);
print_saved_machine_update_notice();
}
#[cfg(not(windows))]
@@ -2224,11 +2222,117 @@ pub fn self_update(options: SelfUpdateOptions) -> Result<Version, String> {
print_outdated_integration_notice_with_updated_binary(&updated_exe);
print_running_session_update_outcomes(&server_update_outcomes, &release);
print_saved_machine_update_notice();
}
Ok(release.version)
}
fn print_saved_machine_update_notice() {
let profiles = match crate::client::endpoint::EndpointCatalog::load_profiles() {
Ok(profiles) => profiles,
Err(error) => {
tracing::debug!(%error, "skipping saved machine update notice");
return;
}
};
for line in saved_machine_update_notice_lines(&profiles) {
eprintln!("{line}");
}
}
fn saved_machine_update_notice_lines(
profiles: &[crate::client::endpoint::SavedSshEndpoint],
) -> Vec<String> {
let mut targets: Vec<&str> = Vec::new();
let mut labels: Vec<&str> = Vec::new();
for profile in profiles.iter().filter(|profile| profile.enabled) {
if !targets.contains(&profile.target.as_str()) {
targets.push(&profile.target);
labels.push(&profile.label);
}
}
if labels.is_empty() {
return Vec::new();
}
let mut lines = vec![
String::new(),
"your SSH machines run their own herdr and may be older:".to_string(),
];
lines.extend(labels.into_iter().map(|label| format!(" {label}")));
lines.push(
"run `herdr update` on each one. it will tell you what to restart there.".to_string(),
);
lines
}
#[cfg(not(windows))]
struct KeptServer<'a> {
label: &'a str,
version: &'a str,
stop_command: &'a str,
attach_command: Option<&'a str>,
}
#[cfg(not(windows))]
impl KeptServer<'_> {
fn reconnect(&self) -> String {
match self.attach_command {
Some(attach) => format!("`{attach}`"),
None => "herdr with the same socket override".to_string(),
}
}
}
#[cfg(not(windows))]
fn kept_server_notice_lines(kept: &[KeptServer<'_>], release_label: &str) -> Vec<String> {
match kept {
[] => Vec::new(),
[server] => vec![
String::new(),
format!(
"your running server is still v{}. run {} to reconnect; everything keeps working.",
server.version,
server.reconnect()
),
format!("server fixes in v{release_label} apply only after it restarts."),
format!(
"when your agents are idle, run `{}`, then {}.",
server.stop_command,
server.reconnect()
),
"this closes running panes and their agents.".to_string(),
],
servers => {
let width = servers
.iter()
.map(|server| server.label.chars().count())
.max()
.unwrap_or(0);
let mut lines = vec![
String::new(),
"your running servers are still older. reconnect as usual; everything keeps working."
.to_string(),
format!("server fixes in v{release_label} apply only after each one restarts:"),
];
lines.extend(servers.iter().map(|server| {
format!(
" {:<width$} v{} `{}`, then {}",
server.label,
server.version,
server.stop_command,
server.reconnect()
)
}));
lines.push(
"restart one when its agents are idle. this closes its panes and their agents."
.to_string(),
);
lines
}
}
}
fn print_outdated_integration_notice_with_updated_binary(updated_exe: &Path) {
let status = Command::new(updated_exe)
.args(["integration", "status", "--outdated-only"])
@@ -2407,6 +2511,103 @@ mod tests {
use std::sync::{Mutex, OnceLock};
use std::thread;
fn saved_machine(
label: &str,
target: &str,
enabled: bool,
) -> crate::client::endpoint::SavedSshEndpoint {
let mut profile =
crate::client::endpoint::SavedSshEndpoint::new(label, target, "default").unwrap();
profile.enabled = enabled;
profile
}
#[test]
fn saved_machine_notice_lists_enabled_machines_once_per_target() {
let profiles = [
saved_machine("rohan", "rohan", true),
saved_machine("rohan/agents", "rohan", true),
saved_machine("off", "offbox", false),
saved_machine("workbox", "dev@workbox", true),
];
assert_eq!(
saved_machine_update_notice_lines(&profiles),
[
"",
"your SSH machines run their own herdr and may be older:",
" rohan",
" workbox",
"run `herdr update` on each one. it will tell you what to restart there.",
]
);
}
#[test]
fn saved_machine_notice_is_empty_without_enabled_machines() {
assert!(saved_machine_update_notice_lines(&[]).is_empty());
assert!(
saved_machine_update_notice_lines(&[saved_machine("off", "offbox", false)]).is_empty()
);
}
#[test]
fn kept_server_notice_names_the_session_restart_commands() {
let default = KeptServer {
label: "default",
version: "0.9.1",
stop_command: "herdr server stop",
attach_command: Some("herdr"),
};
assert_eq!(
kept_server_notice_lines(&[default], "0.10.0"),
[
"",
"your running server is still v0.9.1. run `herdr` to reconnect; everything keeps working.",
"server fixes in v0.10.0 apply only after it restarts.",
"when your agents are idle, run `herdr server stop`, then `herdr`.",
"this closes running panes and their agents.",
]
);
let work = KeptServer {
label: "work",
version: "0.9.0",
stop_command: "herdr session stop work",
attach_command: Some("herdr session attach work"),
};
let default = KeptServer {
label: "default",
version: "0.9.1",
stop_command: "herdr server stop",
attach_command: Some("herdr"),
};
assert_eq!(
kept_server_notice_lines(&[default, work], "0.10.0"),
[
"",
"your running servers are still older. reconnect as usual; everything keeps working.",
"server fixes in v0.10.0 apply only after each one restarts:",
" default v0.9.1 `herdr server stop`, then `herdr`",
" work v0.9.0 `herdr session stop work`, then `herdr session attach work`",
"restart one when its agents are idle. this closes its panes and their agents.",
]
);
assert!(kept_server_notice_lines(&[], "0.10.0").is_empty());
let socket_override = KeptServer {
label: "/tmp/herdr.sock",
version: "0.9.1",
stop_command: "herdr server stop",
attach_command: None,
};
let lines = kept_server_notice_lines(&[socket_override], "0.10.0");
assert_eq!(
lines[3],
"when your agents are idle, run `herdr server stop`, then herdr with the same socket override."
);
}
fn env_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))