diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-31 03:02:45 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-31 03:04:20 +0300 |
| commit | 77819797385c5538f624a8881208921342911db0 (patch) | |
| tree | 931a03aa3825a10d6b6d73b63e4ce80e19ecbf56 /src | |
| parent | 3acd499ffb1119e8032bbcba3f1f46d4ff5f3da9 (diff) | |
fix: clarify update restart guidance
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/actions.rs | 14 | ||||
| -rw-r--r-- | src/app/api.rs | 6 | ||||
| -rw-r--r-- | src/server/autodetect.rs | 28 | ||||
| -rw-r--r-- | src/server/headless.rs | 8 | ||||
| -rw-r--r-- | src/session.rs | 53 | ||||
| -rw-r--r-- | src/ui/release_notes.rs | 21 | ||||
| -rw-r--r-- | src/update.rs | 103 |
7 files changed, 169 insertions, 64 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs index 981212b1..160947ff 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -1932,7 +1932,7 @@ impl AppState { self.toast = Some(ToastNotification { kind: ToastKind::UpdateInstalled, title: format!("v{version} available"), - context: format!("detach, then run `{install_command}`"), + context: crate::update::update_install_instruction(&install_command), target: None, }); } @@ -2795,7 +2795,10 @@ mod tests { assert!(state.latest_release_notes_available); let toast = state.toast.as_ref().expect("update toast"); assert_eq!(toast.title, "v0.5.0 available"); - assert_eq!(toast.context, "detach, then run `herdr update`"); + assert_eq!( + toast.context, + "detach, run `herdr update`, then follow its restart guidance" + ); } fn mark_agent(state: &mut AppState, ws_idx: usize, tab_idx: usize, pane_id: PaneId) { @@ -3756,7 +3759,10 @@ mod tests { let toast = state.toast.as_ref().expect("update toast"); assert_eq!(toast.kind, ToastKind::UpdateInstalled); assert_eq!(toast.title, "v0.5.0 available"); - assert_eq!(toast.context, "detach, then run `herdr update`"); + assert_eq!( + toast.context, + "detach, run `herdr update`, then follow its restart guidance" + ); } #[test] @@ -3776,7 +3782,7 @@ mod tests { let toast = state.toast.as_ref().expect("update toast"); assert_eq!( toast.context, - "detach, then run `brew update && brew upgrade herdr`" + "detach, run `brew update && brew upgrade herdr`, then restart this Herdr session when ready" ); } diff --git a/src/app/api.rs b/src/app/api.rs index d8224506..1442012b 100644 --- a/src/app/api.rs +++ b/src/app/api.rs @@ -152,10 +152,8 @@ impl App { }; if let Some((version, install_command)) = update_ready { - let _ = notify( - &format!("v{version} available"), - Some(&format!("detach, then run `{install_command}`")), - ); + let instruction = crate::update::update_install_instruction(&install_command); + let _ = notify(&format!("v{version} available"), Some(&instruction)); } else { for update in &pane_updates { let is_active_tab = self diff --git a/src/server/autodetect.rs b/src/server/autodetect.rs index c278329d..c012aa03 100644 --- a/src/server/autodetect.rs +++ b/src/server/autodetect.rs @@ -85,11 +85,10 @@ fn read_server_status() -> io::Result<Option<crate::api::RuntimeStatus>> { } fn validate_running_server_compatibility() -> io::Result<()> { - let stop_command = crate::session::local_stop_command(); - let attach_command = crate::session::local_attach_command(); let Some(status) = read_server_status()? else { return Err(io::Error::other(format!( - "a herdr server is listening, but its status API is unavailable. Try `{stop_command}`; if that fails, stop the old server process manually, then run `{attach_command}` again." + "a herdr server is listening, but its status API is unavailable.\n\n{}\nIf that fails, stop the old server process manually.", + crate::session::active_restart_after_update_guidance() ))); }; @@ -98,14 +97,15 @@ fn validate_running_server_compatibility() -> io::Result<()> { } Err(io::Error::other(format!( - "herdr server is running from v{} / protocol {}, but this client is v{} / protocol {}.\nStop the old server with `{stop_command}`, then run `{attach_command}` again.", + "Herdr was updated, but this session is still running the old server.\n\nserver: v{} protocol {}\nclient: v{} protocol {}\n\n{}", status.version.as_deref().unwrap_or("unknown"), status .protocol .map(|value| value.to_string()) .unwrap_or_else(|| "unknown".to_string()), env!("CARGO_PKG_VERSION"), - crate::protocol::PROTOCOL_VERSION + crate::protocol::PROTOCOL_VERSION, + crate::session::active_restart_after_update_guidance() ))) } @@ -432,12 +432,13 @@ mod tests { fn validate_running_server_compatibility_names_session_commands_for_protocol_mismatch() { let _guard = env_lock().lock().unwrap(); let dir = unique_test_dir("named-protocol"); - std::fs::create_dir_all(&dir).unwrap(); - let path = dir.join("api.sock"); - let listener = UnixListener::bind(&path).unwrap(); - std::env::set_var(crate::api::SOCKET_PATH_ENV_VAR, &path); + std::env::set_var("XDG_CONFIG_HOME", &dir); std::env::set_var(crate::session::SESSION_ENV_VAR, "work"); + std::env::remove_var(crate::api::SOCKET_PATH_ENV_VAR); crate::session::clear_explicit_session_for_test(); + let path = crate::session::api_socket_path_for(Some("work")); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + let listener = UnixListener::bind(&path).unwrap(); let handle = std::thread::spawn(move || { let (mut stream, _) = listener.accept().unwrap(); let mut request = String::new(); @@ -458,15 +459,20 @@ mod tests { let _ = handle.join(); assert!( - message.contains("Stop the old server with `herdr session stop work`"), + message.contains("Stop the old server to use the new version"), + "unexpected error: {message}" + ); + assert!( + message.contains("Run `herdr session stop work`"), "unexpected error: {message}" ); assert!( message.contains("then run `herdr session attach work` again"), "unexpected error: {message}" ); - std::env::remove_var(crate::api::SOCKET_PATH_ENV_VAR); + std::env::remove_var("XDG_CONFIG_HOME"); std::env::remove_var(crate::session::SESSION_ENV_VAR); + std::env::remove_var(crate::api::SOCKET_PATH_ENV_VAR); crate::session::clear_explicit_session_for_test(); let _ = std::fs::remove_dir_all(dir); } diff --git a/src/server/headless.rs b/src/server/headless.rs index 3d273b1c..2bf8fa15 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -1359,7 +1359,8 @@ impl HeadlessServer { .map(|toast| format!("{}: {}", toast.title, toast.context)) } else { Some(format!( - "v{version} available: detach, then run `{install_command}`" + "v{version} available: {}", + crate::update::update_install_instruction(&install_command) )) } } else { @@ -5062,7 +5063,10 @@ next_tab = "" ) { ServerMessage::Notify { kind, message } => { assert_eq!(kind, protocol::NotifyKind::SystemToast); - assert_eq!(message, "v9.9.9 available: detach, then run `herdr update`"); + assert_eq!( + message, + "v9.9.9 available: detach, run `herdr update`, then follow its restart guidance" + ); } other => panic!("expected system toast notify, got {other:?}"), } diff --git a/src/session.rs b/src/session.rs index 5e407df8..2e9b0d12 100644 --- a/src/session.rs +++ b/src/session.rs @@ -111,6 +111,33 @@ pub fn stop_command_for(name: Option<&str>) -> String { } } +pub fn restart_after_update_guidance(stop_command: &str, attach_command: Option<&str>) -> String { + let restart = match attach_command { + Some(command) => format!("Run `{stop_command}`, then run `{command}` again."), + None => format!("Run `{stop_command}`, then restart Herdr with the same socket override."), + }; + format!( + "Stop the old server to use the new version.\nStopping exits pane processes.\n{restart}" + ) +} + +pub fn active_restart_after_update_guidance() -> String { + if !explicit_session_requested() { + if let Ok(socket_path) = std::env::var(crate::api::SOCKET_PATH_ENV_VAR) { + return restart_after_update_guidance( + &format!( + "{}={} herdr server stop", + crate::api::SOCKET_PATH_ENV_VAR, + socket_path + ), + None, + ); + } + } + + restart_after_update_guidance(&local_stop_command(), Some(&local_attach_command())) +} + pub fn explicit_session_requested() -> bool { EXPLICIT_SESSION_REQUESTED.load(Ordering::Relaxed) } @@ -699,6 +726,32 @@ mod tests { } #[test] + fn restart_after_update_guidance_names_stop_and_attach_commands() { + assert_eq!( + restart_after_update_guidance( + "herdr session stop work", + Some("herdr session attach work") + ), + "Stop the old server to use the new version.\nStopping exits pane processes.\nRun `herdr session stop work`, then run `herdr session attach work` again." + ); + } + + #[test] + fn active_restart_after_update_guidance_respects_socket_override() { + let _guard = env_lock().lock().unwrap(); + std::env::set_var(crate::api::SOCKET_PATH_ENV_VAR, "/tmp/custom-herdr.sock"); + std::env::remove_var(SESSION_ENV_VAR); + clear_explicit_session_for_test(); + + assert_eq!( + active_restart_after_update_guidance(), + "Stop the old server to use the new version.\nStopping exits pane processes.\nRun `HERDR_SOCKET_PATH=/tmp/custom-herdr.sock herdr server stop`, then restart Herdr with the same socket override." + ); + + std::env::remove_var(crate::api::SOCKET_PATH_ENV_VAR); + } + + #[test] fn explicit_session_socket_ignores_inherited_socket_override() { let _guard = env_lock().lock().unwrap(); let config_home = diff --git a/src/ui/release_notes.rs b/src/ui/release_notes.rs index 1e0769e7..9941a216 100644 --- a/src/ui/release_notes.rs +++ b/src/ui/release_notes.rs @@ -425,17 +425,14 @@ fn release_notes_preview_panel_height(area_width: u16, install_command: &str, p: text_height.saturating_add(3) } -pub(super) fn release_notes_preview_lines<'a>( +pub(super) fn release_notes_preview_lines( _version: &str, - install_command: &'a str, + install_command: &str, p: &Palette, -) -> Vec<Line<'a>> { +) -> Vec<Line<'static>> { let title_style = Style::default().fg(p.text).add_modifier(Modifier::BOLD); let text_style = Style::default().fg(p.text); - let code_style = Style::default() - .fg(p.accent) - .bg(p.surface0) - .add_modifier(Modifier::BOLD); + let instruction = crate::update::update_install_instruction(install_command); vec![ Line::from(vec![ @@ -445,11 +442,7 @@ pub(super) fn release_notes_preview_lines<'a>( ), Span::styled(" update ready", title_style), ]), - Line::from(vec![ - Span::styled("detach from this session, then run ", text_style), - Span::styled(install_command, code_style), - Span::styled(" in your shell", text_style), - ]), + Line::from(vec![Span::styled(instruction, text_style)]), ] } @@ -586,7 +579,7 @@ mod tests { assert_eq!(line_text(&lines[0]), "● update ready"); assert_eq!( line_text(&lines[1]), - "detach from this session, then run herdr update in your shell" + "detach, run `herdr update`, then follow its restart guidance" ); assert_eq!(lines[0].spans[0].style.fg, Some(palette.accent)); assert_eq!(lines[0].spans[1].style.fg, Some(palette.text)); @@ -641,7 +634,7 @@ mod tests { .unwrap(); let rendered = buffer_text(terminal.backend().buffer(), area); - assert!(rendered.contains("your shell"), "{rendered}"); + assert!(rendered.contains("Herdr session"), "{rendered}"); } #[test] diff --git a/src/update.rs b/src/update.rs index 135fccfd..ed421c89 100644 --- a/src/update.rs +++ b/src/update.rs @@ -516,8 +516,11 @@ enum RunningServerUpdateOutcome { #[derive(Debug, Clone, PartialEq, Eq)] struct RunningSessionUpdateOutcome { session_label: String, + target_noun: &'static str, stop_command: String, attach_command: Option<String>, + server_version: Option<String>, + server_protocol: Option<u32>, outcome: RunningServerUpdateOutcome, } @@ -706,28 +709,34 @@ fn prompt_to_stop_old_servers_before_update( ) -> Result<bool, String> { if !io::stdin().is_terminal() { return Err( - "one or more herdr targets must restart for this update; run `herdr update` from an interactive terminal, or stop those targets and run `herdr update` again" + "one or more Herdr sessions must restart for this update. Stop the old server to use the new version, then run `herdr update` again from an interactive terminal." .to_string(), ); } eprintln!( - "these running herdr targets must restart to use v{}:", + "This update changes Herdr's client/server protocol.\n\nRunning sessions that must restart to use v{}:", release.version ); for plan in plans { eprintln!( - " {}: v{} protocol {}", + " {}: server v{} protocol {}", plan.label(), version_label(plan.server.version.as_deref()), protocol_label(plan.server.protocol) ); } - eprintln!("herdr can leave them running, or stop them after installing the update."); - eprintln!("stopping them will exit their pane processes."); + eprintln!( + " update: v{} protocol {}", + release.version, + protocol_label(release.target_protocol) + ); + eprintln!(); + eprintln!("If you choose no, these sessions keep using the old server until you stop them."); + eprintln!("Stop the old server after installing? Stopping exits pane processes."); loop { - eprint!("stop these old targets after updating? [y/N] "); + eprint!("stop after installing? [y/N] "); io::stderr() .flush() .map_err(|e| format!("failed to flush prompt: {e}"))?; @@ -1322,8 +1331,11 @@ fn apply_running_session_update_decisions( let stop_command = decision.plan.stop_command(); outcomes.push(RunningSessionUpdateOutcome { session_label: decision.plan.label().to_string(), + target_noun: decision.plan.target_noun(), stop_command, attach_command: decision.plan.attach_command(), + server_version: decision.plan.server.version.clone(), + server_protocol: decision.plan.server.protocol, outcome, }); } @@ -1333,7 +1345,7 @@ fn apply_running_session_update_decisions( fn print_running_session_update_outcomes( outcomes: &[RunningSessionUpdateOutcome], - release: &ReleaseInfo, + _release: &ReleaseInfo, ) { if outcomes.is_empty() { eprintln!("run herdr again."); @@ -1356,17 +1368,20 @@ fn print_running_session_update_outcomes( } } RunningServerUpdateOutcome::RestartDeferred => { - if outcome.attach_command.is_some() { - eprintln!( - "session {} will use v{} after it restarts.", - outcome.session_label, release.version - ); - } else { - eprintln!( - "server {} will use v{} after it restarts.", - outcome.session_label, release.version - ); - } + eprintln!( + "{} {} is still running v{} protocol {}.", + outcome.target_noun, + outcome.session_label, + version_label(outcome.server_version.as_deref()), + protocol_label(outcome.server_protocol) + ); + eprintln!( + "{}", + crate::session::restart_after_update_guidance( + &outcome.stop_command, + outcome.attach_command.as_deref() + ) + ); } RunningServerUpdateOutcome::Stopped | RunningServerUpdateOutcome::FailedHandoffOldServerStopped @@ -1384,17 +1399,20 @@ fn print_running_session_update_outcomes( } } RunningServerUpdateOutcome::FailedHandoffOldServerKept => { - if outcome.attach_command.is_some() { - eprintln!( - "session {} is still running with your panes; stop it later with `{}` to use v{}.", - outcome.session_label, outcome.stop_command, release.version - ); - } else { - eprintln!( - "server {} is still running with your panes; stop it later with `{}` to use v{}.", - outcome.session_label, outcome.stop_command, release.version - ); - } + eprintln!( + "{} {} is still running v{} protocol {}.", + outcome.target_noun, + outcome.session_label, + version_label(outcome.server_version.as_deref()), + protocol_label(outcome.server_protocol) + ); + eprintln!( + "{}", + crate::session::restart_after_update_guidance( + &outcome.stop_command, + outcome.attach_command.as_deref() + ) + ); } RunningServerUpdateOutcome::FailedHandoffUnknown => { if let Some(command) = &outcome.attach_command { @@ -1427,6 +1445,21 @@ pub(crate) fn update_install_command() -> &'static str { } } +pub(crate) fn update_install_instruction(install_command: &str) -> String { + match install_command { + HERDR_UPDATE_COMMAND => { + "detach, run `herdr update`, then follow its restart guidance".to_string() + } + HOMEBREW_UPDATE_COMMAND => { + "detach, run `brew update && brew upgrade herdr`, then restart this Herdr session when ready".to_string() + } + NIX_UPDATE_COMMAND => { + "detach, update through Nix, then restart this Herdr session when ready".to_string() + } + command => format!("detach, run `{command}`, then restart this Herdr session when ready"), + } +} + fn is_homebrew_managed_install() -> bool { let Ok(current_exe) = env::current_exe() else { return false; @@ -2255,6 +2288,18 @@ mod tests { } #[test] + fn update_install_instruction_distinguishes_install_from_restart() { + assert_eq!( + update_install_instruction(HERDR_UPDATE_COMMAND), + "detach, run `herdr update`, then follow its restart guidance" + ); + assert_eq!( + update_install_instruction(HOMEBREW_UPDATE_COMMAND), + "detach, run `brew update && brew upgrade herdr`, then restart this Herdr session when ready" + ); + } + + #[test] fn fake_release_notes_default_to_real_large_changelog_section() { let _guard = env_lock().lock().unwrap(); std::env::remove_var(FAKE_UPDATE_NOTES_VERSION_ENV); |
