diff --git a/docs/next/website/src/content/docs/install.mdx b/docs/next/website/src/content/docs/install.mdx index 4dac5edea..5ec26d10e 100644 --- a/docs/next/website/src/content/docs/install.mdx +++ b/docs/next/website/src/content/docs/install.mdx @@ -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 diff --git a/docs/next/website/src/content/docs/ja/install.mdx b/docs/next/website/src/content/docs/ja/install.mdx index 8217d311a..587f6df14 100644 --- a/docs/next/website/src/content/docs/ja/install.mdx +++ b/docs/next/website/src/content/docs/ja/install.mdx @@ -141,6 +141,8 @@ Windows の通常利用には安定版を推奨します。プレビューでは デフォルトでは、`herdr update` は新しいバイナリをインストールし、エンドポイント世代 1 のサーバーとペインプロセスを動かしたままにします。再度 Herdr を起動すると、更新済みクライアントで再接続します。クライアントだけの変更はすぐに使えます。新しいサーバー側アクションは、そのサーバーを更新して再起動するまで利用できず、クライアントは切断せずに通知を表示します。エンドポイント世代 1 より前のサーバーだけは、更新時に最後の 1 回の停止が必要です。 +完了時に `herdr update` は、古いバージョンのまま動いているサーバーとそれを再起動するコマンド、そして独自の Herdr を動かしている保存済み SSH マシンを一覧表示します。各マシンでも `herdr update` を実行して更新してください。 + 対応する実行中セッションで実験的なライブサーバーハンドオフにオプトインするには: ```bash diff --git a/docs/next/website/src/content/docs/zh-cn/install.mdx b/docs/next/website/src/content/docs/zh-cn/install.mdx index 8faba5063..938852249 100644 --- a/docs/next/website/src/content/docs/zh-cn/install.mdx +++ b/docs/next/website/src/content/docs/zh-cn/install.mdx @@ -141,6 +141,8 @@ Windows 的常规使用建议选择稳定版。预览版能更早获得修复, 默认情况下,`herdr update` 会安装新二进制文件,并让端点第 1 代服务器及其窗格进程继续运行。再次启动 Herdr 即可用更新后的客户端重新连接。纯客户端改动会立即生效。新的服务器端操作会保持不可用,直到对应服务器更新并重启;客户端会显示通知,而不会断开连接。只有早于端点第 1 代的服务器需要在更新时再停止最后一次。 +完成后,`herdr update` 会列出仍在运行旧版本的服务器及重启它们的命令,以及运行各自 Herdr 的已保存 SSH 机器。请在每台机器上也运行 `herdr update` 来更新。 + 要对受支持的运行中会话启用实验性的实时服务器交接,运行: ```bash diff --git a/src/update.rs b/src/update.rs index 958be6e18..854bacdce 100644 --- a/src/update.rs +++ b/src/update.rs @@ -1781,26 +1781,23 @@ fn print_running_session_update_outcomes( return; } + let kept: Vec> = 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 { "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 { 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 { + 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 { + 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!( + " {: 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> = OnceLock::new(); LOCK.get_or_init(|| Mutex::new(()))