fix: clarify update restart prompts
This commit is contained in:
parent
53e4b27165
commit
6f9453a480
|
|
@ -12,6 +12,7 @@
|
|||
### Fixed
|
||||
- Pane input no longer waits behind the PTY actor's idle read poll, restoring responsive typing at quiet shell prompts. (#379)
|
||||
- Pane apps that query OSC 4 ANSI palette colors now receive the active terminal palette response, so OpenCode and similar TUIs can enable system-theme behavior inside Herdr. (#387)
|
||||
- Plain `herdr update` and remote binary replacement now ask before stopping running sessions, avoid protocol-heavy prompt text, and leave the current install untouched when the user chooses not to stop active pane processes. Explicit `--handoff` update flows try live handoff without a second handoff prompt.
|
||||
|
||||
## [0.6.6] - 2026-05-31
|
||||
|
||||
|
|
|
|||
177
src/remote.rs
177
src/remote.rs
|
|
@ -166,6 +166,7 @@ pub(crate) fn run_remote(remote: RemoteLaunch) -> io::Result<()> {
|
|||
&remote.target,
|
||||
&prepared_remote.remote_herdr,
|
||||
prepared_remote.installed_or_replaced,
|
||||
prepared_remote.stop_after_install_approved,
|
||||
remote.live_handoff,
|
||||
)?;
|
||||
|
||||
|
|
@ -222,10 +223,9 @@ fn ensure_remote_server_running() -> io::Result<()> {
|
|||
if status.protocol == Some(CURRENT_PROTOCOL) {
|
||||
return Ok(());
|
||||
}
|
||||
return Err(io::Error::other(format!(
|
||||
"remote herdr server is running with protocol {}, but this bridge needs protocol {CURRENT_PROTOCOL}; rerun `herdr --remote` from an interactive terminal to approve stopping it",
|
||||
protocol_label(status.protocol)
|
||||
)));
|
||||
return Err(io::Error::other(
|
||||
"remote herdr server must restart before this bridge can attach; rerun `herdr --remote` from an interactive terminal to approve stopping it",
|
||||
));
|
||||
}
|
||||
|
||||
crate::server::autodetect::spawn_server_daemon()?;
|
||||
|
|
@ -370,6 +370,7 @@ struct InstallSource {
|
|||
struct PreparedRemoteHerdr {
|
||||
remote_herdr: RemoteHerdr,
|
||||
installed_or_replaced: bool,
|
||||
stop_after_install_approved: bool,
|
||||
}
|
||||
|
||||
impl InstallSource {
|
||||
|
|
@ -411,22 +412,25 @@ fn prepare_remote_herdr(
|
|||
return Ok(PreparedRemoteHerdr {
|
||||
remote_herdr: path_remote_herdr.clone(),
|
||||
installed_or_replaced: false,
|
||||
stop_after_install_approved: false,
|
||||
});
|
||||
}
|
||||
if remote_binary_matches(target, &remote_herdr)? {
|
||||
return Ok(PreparedRemoteHerdr {
|
||||
remote_herdr,
|
||||
installed_or_replaced: false,
|
||||
stop_after_install_approved: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut stop_after_install_approved = false;
|
||||
if let Some(status_probe_herdr) = path_remote_herdr.as_ref().or_else(|| {
|
||||
remote_binary_exists(target, &remote_herdr)
|
||||
.ok()
|
||||
.and_then(|exists| exists.then_some(&remote_herdr))
|
||||
}) {
|
||||
confirm_remote_install_with_running_server(
|
||||
stop_after_install_approved = confirm_remote_install_with_running_server(
|
||||
target,
|
||||
status_probe_herdr,
|
||||
live_handoff_enabled,
|
||||
|
|
@ -453,6 +457,7 @@ fn prepare_remote_herdr(
|
|||
Ok(PreparedRemoteHerdr {
|
||||
remote_herdr,
|
||||
installed_or_replaced: true,
|
||||
stop_after_install_approved,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -659,6 +664,7 @@ fn ensure_remote_server_ready(
|
|||
target: &str,
|
||||
remote_herdr: &RemoteHerdr,
|
||||
remote_binary_changed: bool,
|
||||
stop_after_install_approved: bool,
|
||||
live_handoff_enabled: bool,
|
||||
) -> io::Result<()> {
|
||||
let status = remote_server_status(target, remote_herdr)?;
|
||||
|
|
@ -677,10 +683,7 @@ fn ensure_remote_server_ready(
|
|||
return Ok(());
|
||||
};
|
||||
|
||||
if live_handoff_enabled
|
||||
&& live_handoff
|
||||
&& confirm_remote_server_handoff(target, version.as_deref(), protocol, reason)?
|
||||
{
|
||||
if live_handoff_enabled && live_handoff {
|
||||
match live_handoff_remote_server(target, remote_herdr) {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(err) => {
|
||||
|
|
@ -690,6 +693,11 @@ fn ensure_remote_server_ready(
|
|||
}
|
||||
}
|
||||
|
||||
if stop_after_install_approved {
|
||||
stop_remote_server(target, remote_herdr)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if confirm_remote_server_stop(target, version.as_deref(), protocol, reason)? {
|
||||
stop_remote_server(target, remote_herdr)?;
|
||||
}
|
||||
|
|
@ -717,7 +725,7 @@ fn confirm_remote_install_with_running_server(
|
|||
target: &str,
|
||||
remote_herdr: &RemoteHerdr,
|
||||
live_handoff_enabled: bool,
|
||||
) -> io::Result<()> {
|
||||
) -> io::Result<bool> {
|
||||
let status = match remote_server_status(target, remote_herdr) {
|
||||
Ok(status) => status,
|
||||
Err(err) => {
|
||||
|
|
@ -729,65 +737,69 @@ fn confirm_remote_install_with_running_server(
|
|||
eprintln!(
|
||||
"could not inspect the running remote herdr server on {target} before installing: {err}"
|
||||
);
|
||||
eprint!("continue installing the remote herdr binary? [Y/n] ");
|
||||
eprint!("continue installing the remote herdr binary? [y/N] ");
|
||||
io::stderr().flush()?;
|
||||
|
||||
let mut answer = String::new();
|
||||
io::stdin().read_line(&mut answer)?;
|
||||
let answer = answer.trim().to_ascii_lowercase();
|
||||
if answer == "n" || answer == "no" {
|
||||
if answer != "y" && answer != "yes" {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Interrupted,
|
||||
"remote herdr install cancelled",
|
||||
));
|
||||
}
|
||||
return Ok(());
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
let RemoteServerStatus::Running {
|
||||
version,
|
||||
protocol,
|
||||
protocol: _,
|
||||
live_handoff,
|
||||
} = status
|
||||
else {
|
||||
return Ok(());
|
||||
return Ok(false);
|
||||
};
|
||||
if live_handoff_enabled && live_handoff {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !io::stdin().is_terminal() {
|
||||
if live_handoff_enabled && live_handoff {
|
||||
return Ok(false);
|
||||
}
|
||||
return Err(io::Error::other(format!(
|
||||
"remote herdr server on {target} is running v{} protocol {}; run from an interactive terminal to approve updating the remote binary",
|
||||
version_label(version.as_deref()),
|
||||
protocol_label(protocol)
|
||||
"remote herdr server on {target} is running v{}; run from an interactive terminal to approve stopping it for the update",
|
||||
version_label(version.as_deref())
|
||||
)));
|
||||
}
|
||||
|
||||
if live_handoff_enabled && live_handoff {
|
||||
eprintln!("remote herdr server on {target} is currently running:");
|
||||
eprintln!(" server: v{}", version_label(version.as_deref()));
|
||||
eprintln!(
|
||||
"Herdr will install v{CURRENT_VERSION} and hand off live pane processes to the prepared server."
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
eprintln!("remote herdr server on {target} is currently running:");
|
||||
eprintln!(" server: v{}", version_label(version.as_deref()));
|
||||
eprintln!(
|
||||
" server: v{} protocol {}",
|
||||
version_label(version.as_deref()),
|
||||
protocol_label(protocol)
|
||||
);
|
||||
eprintln!(
|
||||
"this attach will not preserve running panes unless you pass --handoff and the remote server supports live handoff."
|
||||
"To complete the remote update, Herdr must stop the running remote server after installing."
|
||||
);
|
||||
eprintln!("This stops active remote pane processes, including shells, dev servers, and tests.");
|
||||
eprintln!();
|
||||
eprint!("continue installing the remote herdr binary? [Y/n] ");
|
||||
eprint!("Install v{CURRENT_VERSION} and stop the remote server now? [y/N] ");
|
||||
io::stderr().flush()?;
|
||||
|
||||
let mut answer = String::new();
|
||||
io::stdin().read_line(&mut answer)?;
|
||||
let answer = answer.trim().to_ascii_lowercase();
|
||||
if answer == "n" || answer == "no" {
|
||||
if answer != "y" && answer != "yes" {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Interrupted,
|
||||
"remote herdr install cancelled",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn remote_server_status(
|
||||
|
|
@ -848,14 +860,13 @@ fn parse_remote_server_status_json(status: &str) -> io::Result<RemoteServerStatu
|
|||
fn confirm_remote_server_stop(
|
||||
target: &str,
|
||||
version: Option<&str>,
|
||||
protocol: Option<u32>,
|
||||
_protocol: Option<u32>,
|
||||
reason: RemoteServerRestartReason,
|
||||
) -> io::Result<bool> {
|
||||
if !io::stdin().is_terminal() {
|
||||
if reason == RemoteServerRestartReason::ProtocolMismatch {
|
||||
return Err(io::Error::other(format!(
|
||||
"remote herdr server on {target} is running with protocol {}, but this client needs protocol {CURRENT_PROTOCOL}; run from an interactive terminal to approve stopping it",
|
||||
protocol_label(protocol)
|
||||
"remote herdr server on {target} must stop before this client can attach; run from an interactive terminal to approve stopping it"
|
||||
)));
|
||||
}
|
||||
|
||||
|
|
@ -867,19 +878,13 @@ fn confirm_remote_server_stop(
|
|||
}
|
||||
|
||||
eprintln!("remote herdr server on {target} is currently running:");
|
||||
eprintln!(
|
||||
" server: v{} protocol {}",
|
||||
version_label(version),
|
||||
protocol_label(protocol)
|
||||
);
|
||||
eprintln!(" prepared binary: v{CURRENT_VERSION} protocol {CURRENT_PROTOCOL}");
|
||||
eprintln!(" server: v{}", version_label(version));
|
||||
eprintln!(" prepared binary: v{CURRENT_VERSION}");
|
||||
eprintln!();
|
||||
|
||||
match reason {
|
||||
RemoteServerRestartReason::ProtocolMismatch => {
|
||||
eprintln!(
|
||||
"the remote server protocol does not match this client. the remote server must be stopped before attaching."
|
||||
);
|
||||
eprintln!("the remote server must stop before this client can attach.");
|
||||
}
|
||||
RemoteServerRestartReason::BinaryUpdated => {
|
||||
eprintln!(
|
||||
|
|
@ -896,7 +901,7 @@ fn confirm_remote_server_stop(
|
|||
let prompt = if reason == RemoteServerRestartReason::ProtocolMismatch {
|
||||
"stop the remote server and continue attaching? [Y/n] "
|
||||
} else {
|
||||
"restart the remote server now? [Y/n] "
|
||||
"restart the remote server now? [y/N] "
|
||||
};
|
||||
eprint!("{prompt}");
|
||||
io::stderr().flush()?;
|
||||
|
|
@ -904,74 +909,20 @@ fn confirm_remote_server_stop(
|
|||
let mut answer = String::new();
|
||||
io::stdin().read_line(&mut answer)?;
|
||||
let answer = answer.trim().to_ascii_lowercase();
|
||||
if answer == "n" || answer == "no" {
|
||||
if reason == RemoteServerRestartReason::ProtocolMismatch {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Interrupted,
|
||||
"remote herdr server stop cancelled",
|
||||
));
|
||||
}
|
||||
return Ok(false);
|
||||
if answer == "y" || answer == "yes" {
|
||||
return Ok(true);
|
||||
}
|
||||
if answer.is_empty() && reason == RemoteServerRestartReason::ProtocolMismatch {
|
||||
return Ok(true);
|
||||
}
|
||||
if reason == RemoteServerRestartReason::ProtocolMismatch {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Interrupted,
|
||||
"remote herdr server stop cancelled",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn confirm_remote_server_handoff(
|
||||
target: &str,
|
||||
version: Option<&str>,
|
||||
protocol: Option<u32>,
|
||||
reason: RemoteServerRestartReason,
|
||||
) -> io::Result<bool> {
|
||||
if !io::stdin().is_terminal() {
|
||||
if reason == RemoteServerRestartReason::ProtocolMismatch {
|
||||
return Err(io::Error::other(format!(
|
||||
"remote herdr server on {target} is running with protocol {}, but this client needs protocol {CURRENT_PROTOCOL}; run from an interactive terminal to approve live handoff or stopping it",
|
||||
protocol_label(protocol)
|
||||
)));
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"remote herdr server on {target} is still running v{}; it will use v{CURRENT_VERSION} after it restarts.",
|
||||
version_label(version)
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
eprintln!("remote herdr server on {target} is currently running:");
|
||||
eprintln!(
|
||||
" server: v{} protocol {}",
|
||||
version_label(version),
|
||||
protocol_label(protocol)
|
||||
);
|
||||
eprintln!(" prepared binary: v{CURRENT_VERSION} protocol {CURRENT_PROTOCOL}");
|
||||
eprintln!();
|
||||
|
||||
match reason {
|
||||
RemoteServerRestartReason::ProtocolMismatch => {
|
||||
eprintln!(
|
||||
"the remote server protocol does not match this client. herdr will try to hand off live pane processes to the prepared remote server before the old server exits."
|
||||
);
|
||||
}
|
||||
RemoteServerRestartReason::BinaryUpdated => {
|
||||
eprintln!(
|
||||
"the remote herdr binary was installed or replaced. herdr will try to hand off live pane processes to the prepared remote server."
|
||||
);
|
||||
}
|
||||
RemoteServerRestartReason::VersionMismatch => {
|
||||
eprintln!(
|
||||
"the remote server is still running a different herdr version. herdr will try to hand off live pane processes to the prepared remote server."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
eprint!("live-handoff remote panes to the prepared server? [Y/n] ");
|
||||
io::stderr().flush()?;
|
||||
|
||||
let mut answer = String::new();
|
||||
io::stdin().read_line(&mut answer)?;
|
||||
let answer = answer.trim().to_ascii_lowercase();
|
||||
Ok(answer != "n" && answer != "no")
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn live_handoff_remote_server(target: &str, remote_herdr: &RemoteHerdr) -> io::Result<()> {
|
||||
|
|
@ -1026,12 +977,6 @@ fn version_label(version: Option<&str>) -> &str {
|
|||
version.unwrap_or("unknown")
|
||||
}
|
||||
|
||||
fn protocol_label(protocol: Option<u32>) -> String {
|
||||
protocol
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
}
|
||||
|
||||
fn warn_if_remote_bin_not_on_path(target: &str) -> io::Result<()> {
|
||||
let output = ssh_output(
|
||||
target,
|
||||
|
|
|
|||
317
src/update.rs
317
src/update.rs
|
|
@ -414,12 +414,6 @@ fn client_protocol_server_is_running() -> bool {
|
|||
client_protocol_server_is_running_at(&crate::server::socket_paths::client_socket_path())
|
||||
}
|
||||
|
||||
fn protocol_label(protocol: Option<u32>) -> String {
|
||||
protocol
|
||||
.map(|value| value.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
}
|
||||
|
||||
fn version_label(version: Option<&str>) -> &str {
|
||||
version.unwrap_or("unknown")
|
||||
}
|
||||
|
|
@ -441,10 +435,11 @@ fn server_supports_live_handoff(server: &crate::api::RuntimeStatus) -> bool {
|
|||
.is_some_and(|capabilities| capabilities.live_handoff)
|
||||
}
|
||||
|
||||
fn parse_live_handoff_before_update_response(input: &str) -> Option<bool> {
|
||||
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() {
|
||||
"" | "y" | "yes" => Some(true),
|
||||
"" => Some(default_yes),
|
||||
"y" | "yes" => Some(true),
|
||||
"n" | "no" => Some(false),
|
||||
_ => None,
|
||||
}
|
||||
|
|
@ -514,7 +509,6 @@ struct RunningSessionUpdateOutcome {
|
|||
stop_command: String,
|
||||
attach_command: Option<String>,
|
||||
server_version: Option<String>,
|
||||
server_protocol: Option<u32>,
|
||||
outcome: RunningServerUpdateOutcome,
|
||||
}
|
||||
|
||||
|
|
@ -703,28 +697,22 @@ fn prompt_to_stop_old_servers_before_update(
|
|||
) -> Result<bool, String> {
|
||||
if !io::stdin().is_terminal() {
|
||||
return Err(
|
||||
"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."
|
||||
"one or more Herdr sessions must stop for this update. Stop running Herdr sessions when ready, then run `herdr update` again from an interactive terminal."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"This update changes Herdr's client/server protocol.\n\nRunning sessions that must restart to use v{}:",
|
||||
"Running sessions that must stop to use v{}:",
|
||||
release.version
|
||||
);
|
||||
for plan in plans {
|
||||
eprintln!(
|
||||
" {}: server v{} protocol {}",
|
||||
" {}: server v{}",
|
||||
plan.label(),
|
||||
version_label(plan.server.version.as_deref()),
|
||||
protocol_label(plan.server.protocol)
|
||||
version_label(plan.server.version.as_deref())
|
||||
);
|
||||
}
|
||||
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.");
|
||||
|
|
@ -763,48 +751,20 @@ fn confirm_running_server_update_action(
|
|||
print_running_session_update_summary(&plans, release, options);
|
||||
|
||||
if !options.live_handoff {
|
||||
let restart_required: Vec<RunningServerUpdatePlan> = plans
|
||||
.iter()
|
||||
.filter(|plan| plan.requires_server_restart)
|
||||
.cloned()
|
||||
.collect();
|
||||
let stop_restart_required = if restart_required.is_empty() {
|
||||
false
|
||||
} else {
|
||||
prompt_to_stop_old_servers_before_update(&restart_required, release)?
|
||||
};
|
||||
return Ok(plans
|
||||
.into_iter()
|
||||
.map(|plan| {
|
||||
let action = if plan.requires_server_restart && stop_restart_required {
|
||||
RunningServerUpdateAction::StopOldServer
|
||||
} else {
|
||||
RunningServerUpdateAction::None
|
||||
};
|
||||
RunningServerUpdateDecision { plan, action }
|
||||
.map(|plan| RunningServerUpdateDecision {
|
||||
plan,
|
||||
action: RunningServerUpdateAction::None,
|
||||
})
|
||||
.collect());
|
||||
}
|
||||
|
||||
let handoff_supported: Vec<&RunningServerUpdatePlan> = plans
|
||||
.iter()
|
||||
.filter(|plan| server_supports_live_handoff(&plan.server))
|
||||
.collect();
|
||||
let handoff_unsupported_requiring_update: Vec<&RunningServerUpdatePlan> = plans
|
||||
.iter()
|
||||
.filter(|plan| !server_supports_live_handoff(&plan.server) && plan.requires_server_restart)
|
||||
.collect();
|
||||
|
||||
let live_handoff = if handoff_supported.is_empty() {
|
||||
false
|
||||
} else {
|
||||
prompt_to_live_handoff_sessions_before_update(
|
||||
&handoff_supported,
|
||||
release,
|
||||
plans.iter().any(|plan| plan.requires_server_restart),
|
||||
)?
|
||||
};
|
||||
|
||||
let stop_unsupported = if handoff_unsupported_requiring_update.is_empty() {
|
||||
false
|
||||
} else {
|
||||
|
|
@ -817,7 +777,7 @@ fn confirm_running_server_update_action(
|
|||
|
||||
let mut decisions = Vec::new();
|
||||
for plan in plans {
|
||||
let action = if server_supports_live_handoff(&plan.server) && live_handoff {
|
||||
let action = if server_supports_live_handoff(&plan.server) {
|
||||
RunningServerUpdateAction::LiveHandoff
|
||||
} else if !server_supports_live_handoff(&plan.server)
|
||||
&& plan.requires_server_restart
|
||||
|
|
@ -833,6 +793,85 @@ fn confirm_running_server_update_action(
|
|||
Ok(decisions)
|
||||
}
|
||||
|
||||
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");
|
||||
if all_sessions {
|
||||
("session", "sessions")
|
||||
} else if all_servers {
|
||||
("server", "servers")
|
||||
} else {
|
||||
("target", "targets")
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_to_complete_plain_update(
|
||||
decisions: &[RunningServerUpdateDecision],
|
||||
release: &ReleaseInfo,
|
||||
) -> Result<bool, String> {
|
||||
if decisions.is_empty() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if !io::stdin().is_terminal() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let plans: Vec<&RunningServerUpdatePlan> =
|
||||
decisions.iter().map(|decision| &decision.plan).collect();
|
||||
let (singular, plural) = target_group_nouns(&plans);
|
||||
let noun = if plans.len() == 1 { singular } else { plural };
|
||||
eprintln!(
|
||||
"To complete the update, Herdr must stop {} running {}.",
|
||||
plans.len(),
|
||||
noun
|
||||
);
|
||||
eprintln!("This stops active pane processes, including shells, dev servers, and tests.");
|
||||
for plan in plans {
|
||||
eprintln!(
|
||||
" {} {}: server v{}",
|
||||
plan.target_noun(),
|
||||
plan.label(),
|
||||
version_label(plan.server.version.as_deref())
|
||||
);
|
||||
}
|
||||
|
||||
loop {
|
||||
eprint!(
|
||||
"Stop running {} and install v{} now? [y/N] ",
|
||||
noun, release.version
|
||||
);
|
||||
io::stderr()
|
||||
.flush()
|
||||
.map_err(|e| format!("failed to flush prompt: {e}"))?;
|
||||
|
||||
let mut input = String::new();
|
||||
let read = io::stdin()
|
||||
.read_line(&mut input)
|
||||
.map_err(|e| format!("failed to read prompt response: {e}"))?;
|
||||
if read == 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if let Some(answer) = parse_stop_old_servers_after_update_response(&input, false) {
|
||||
return Ok(answer);
|
||||
}
|
||||
eprintln!("please answer y or n");
|
||||
}
|
||||
}
|
||||
|
||||
fn mark_plain_update_stop_decisions(
|
||||
decisions: Vec<RunningServerUpdateDecision>,
|
||||
) -> Vec<RunningServerUpdateDecision> {
|
||||
decisions
|
||||
.into_iter()
|
||||
.map(|mut decision| {
|
||||
decision.action = RunningServerUpdateAction::StopOldServer;
|
||||
decision
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn print_running_session_update_summary(
|
||||
plans: &[RunningServerUpdatePlan],
|
||||
release: &ReleaseInfo,
|
||||
|
|
@ -847,81 +886,23 @@ fn print_running_session_update_summary(
|
|||
"too old for handoff"
|
||||
};
|
||||
eprintln!(
|
||||
" {}: v{} protocol {} ({})",
|
||||
" {}: server v{} ({})",
|
||||
plan.label(),
|
||||
version_label(plan.server.version.as_deref()),
|
||||
protocol_label(plan.server.protocol),
|
||||
capability
|
||||
);
|
||||
} else {
|
||||
eprintln!(
|
||||
" {}: v{} protocol {}",
|
||||
" {}: server v{}",
|
||||
plan.label(),
|
||||
version_label(plan.server.version.as_deref()),
|
||||
protocol_label(plan.server.protocol)
|
||||
version_label(plan.server.version.as_deref())
|
||||
);
|
||||
}
|
||||
}
|
||||
eprintln!(
|
||||
" update: v{} protocol {}",
|
||||
release.version,
|
||||
protocol_label(release.target_protocol)
|
||||
);
|
||||
eprintln!(" update: v{}", release.version);
|
||||
eprintln!();
|
||||
}
|
||||
|
||||
fn prompt_to_live_handoff_sessions_before_update(
|
||||
plans: &[&RunningServerUpdatePlan],
|
||||
release: &ReleaseInfo,
|
||||
requires_live_handoff: bool,
|
||||
) -> Result<bool, String> {
|
||||
if !io::stdin().is_terminal() {
|
||||
if requires_live_handoff {
|
||||
return Err(format!(
|
||||
"one or more herdr targets are running and updating to v{} requires live server handoff; run `herdr update` from an interactive terminal, or stop those targets and run `herdr update` again",
|
||||
release.version
|
||||
));
|
||||
}
|
||||
eprintln!(
|
||||
"herdr targets are running. updating the binary will not affect them until they restart."
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"herdr can hand off {} running target{} to the new server so pane processes keep running.",
|
||||
plans.len(),
|
||||
if plans.len() == 1 { "" } else { "s" }
|
||||
);
|
||||
eprintln!("connected clients will disconnect during handoff and can attach again afterward.");
|
||||
|
||||
loop {
|
||||
let prompt = if requires_live_handoff {
|
||||
"update and live-handoff supported targets to the new server? [Y/n] "
|
||||
} else {
|
||||
"live-handoff supported running targets after updating? [Y/n] "
|
||||
};
|
||||
eprint!("{prompt}");
|
||||
io::stderr()
|
||||
.flush()
|
||||
.map_err(|e| format!("failed to flush prompt: {e}"))?;
|
||||
|
||||
let mut input = String::new();
|
||||
let read = io::stdin()
|
||||
.read_line(&mut input)
|
||||
.map_err(|e| format!("failed to read prompt response: {e}"))?;
|
||||
if read == 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if let Some(answer) = parse_live_handoff_before_update_response(&input) {
|
||||
return Ok(answer);
|
||||
}
|
||||
|
||||
eprintln!("please answer y or n");
|
||||
}
|
||||
}
|
||||
|
||||
fn live_handoff_running_server_for_update(
|
||||
plan: &RunningServerUpdatePlan,
|
||||
release: &ReleaseInfo,
|
||||
|
|
@ -974,16 +955,8 @@ fn prompt_to_stop_old_server_after_failed_handoff(
|
|||
plan.target_noun(),
|
||||
plan.label()
|
||||
);
|
||||
eprintln!(
|
||||
" server: v{} protocol {}",
|
||||
version_label(status.version.as_deref()),
|
||||
protocol_label(status.protocol)
|
||||
);
|
||||
eprintln!(
|
||||
" installed: v{} protocol {}",
|
||||
release.version,
|
||||
protocol_label(release.target_protocol)
|
||||
);
|
||||
eprintln!(" server: v{}", version_label(status.version.as_deref()));
|
||||
eprintln!(" installed: v{}", release.version);
|
||||
eprintln!(
|
||||
"you can keep using the old server, or stop it now so the next `herdr` start uses v{}.",
|
||||
release.version
|
||||
|
|
@ -1329,7 +1302,6 @@ fn apply_running_session_update_decisions(
|
|||
stop_command,
|
||||
attach_command: decision.plan.attach_command(),
|
||||
server_version: decision.plan.server.version.clone(),
|
||||
server_protocol: decision.plan.server.protocol,
|
||||
outcome,
|
||||
});
|
||||
}
|
||||
|
|
@ -1339,7 +1311,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.");
|
||||
|
|
@ -1363,19 +1335,20 @@ fn print_running_session_update_outcomes(
|
|||
}
|
||||
RunningServerUpdateOutcome::RestartDeferred => {
|
||||
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()
|
||||
)
|
||||
"{} {} kept running.",
|
||||
outcome.target_noun, outcome.session_label
|
||||
);
|
||||
eprintln!("Stopping exits active pane processes.");
|
||||
match &outcome.attach_command {
|
||||
Some(command) => eprintln!(
|
||||
"Run `{}`, then run `{command}` when ready to use v{}.",
|
||||
outcome.stop_command, release.version
|
||||
),
|
||||
None => eprintln!(
|
||||
"Run `{}`, then restart Herdr with the same socket override when ready to use v{}.",
|
||||
outcome.stop_command, release.version
|
||||
),
|
||||
}
|
||||
}
|
||||
RunningServerUpdateOutcome::Stopped
|
||||
| RunningServerUpdateOutcome::FailedHandoffOldServerStopped
|
||||
|
|
@ -1394,11 +1367,10 @@ fn print_running_session_update_outcomes(
|
|||
}
|
||||
RunningServerUpdateOutcome::FailedHandoffOldServerKept => {
|
||||
eprintln!(
|
||||
"{} {} is still running v{} protocol {}.",
|
||||
"{} {} is still running server v{}.",
|
||||
outcome.target_noun,
|
||||
outcome.session_label,
|
||||
version_label(outcome.server_version.as_deref()),
|
||||
protocol_label(outcome.server_protocol)
|
||||
version_label(outcome.server_version.as_deref())
|
||||
);
|
||||
eprintln!(
|
||||
"{}",
|
||||
|
|
@ -1567,10 +1539,23 @@ pub fn self_update(options: SelfUpdateOptions) -> Result<Version, String> {
|
|||
}
|
||||
let downloaded_update = download_update(&release)?;
|
||||
let updated_exe = downloaded_update.current_exe.clone();
|
||||
eprintln!("downloaded v{}", release.version);
|
||||
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);
|
||||
}
|
||||
install_downloaded_update(downloaded_update)?;
|
||||
eprintln!("installed v{}", release.version);
|
||||
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)?;
|
||||
eprintln!("updated to v{}", release.version);
|
||||
print_outdated_integration_notice_with_updated_binary(&updated_exe);
|
||||
|
||||
print_running_session_update_outcomes(&server_update_outcomes, &release);
|
||||
|
|
@ -2054,14 +2039,27 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn parse_live_handoff_before_update_response_defaults_yes_for_blank() {
|
||||
assert_eq!(parse_live_handoff_before_update_response(""), Some(true));
|
||||
assert_eq!(parse_live_handoff_before_update_response("\n"), Some(true));
|
||||
assert_eq!(parse_live_handoff_before_update_response("y"), Some(true));
|
||||
assert_eq!(parse_live_handoff_before_update_response("yes"), Some(true));
|
||||
assert_eq!(parse_live_handoff_before_update_response("n"), Some(false));
|
||||
assert_eq!(parse_live_handoff_before_update_response("no"), Some(false));
|
||||
assert_eq!(parse_live_handoff_before_update_response("later"), None);
|
||||
fn parse_stop_old_servers_after_update_response_uses_prompt_default_for_blank() {
|
||||
assert_eq!(
|
||||
parse_stop_old_servers_after_update_response("", true),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_stop_old_servers_after_update_response("\n", false),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_stop_old_servers_after_update_response("y", false),
|
||||
Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_stop_old_servers_after_update_response("no", true),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_stop_old_servers_after_update_response("later", true),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -2098,7 +2096,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn plain_update_requires_restart_for_supported_servers_without_handoff() {
|
||||
fn plain_update_defers_stop_prompt_until_after_install() {
|
||||
assert!(
|
||||
!io::stdin().is_terminal(),
|
||||
"this test relies on noninteractive test stdin"
|
||||
|
|
@ -2122,17 +2120,18 @@ mod tests {
|
|||
},
|
||||
};
|
||||
|
||||
let err = confirm_running_server_update_action(
|
||||
let decisions = confirm_running_server_update_action(
|
||||
vec![plan],
|
||||
&release,
|
||||
SelfUpdateOptions {
|
||||
live_handoff: false,
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
.unwrap();
|
||||
|
||||
assert!(err.contains("must restart"), "unexpected error: {err}");
|
||||
assert!(!err.contains("live handoff"), "unexpected error: {err}");
|
||||
assert_eq!(decisions.len(), 1);
|
||||
assert_eq!(decisions[0].action, RunningServerUpdateAction::None);
|
||||
assert!(decisions[0].plan.requires_server_restart);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -2291,7 +2290,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn noninteractive_plain_update_requiring_restart_fails_without_handoff() {
|
||||
fn noninteractive_plain_update_does_not_complete_with_running_server() {
|
||||
let _guard = env_lock().lock().unwrap();
|
||||
assert!(
|
||||
!io::stdin().is_terminal(),
|
||||
|
|
@ -2324,17 +2323,17 @@ mod tests {
|
|||
server,
|
||||
};
|
||||
|
||||
let err = confirm_running_server_update_action(
|
||||
let decisions = confirm_running_server_update_action(
|
||||
vec![plan],
|
||||
&release,
|
||||
SelfUpdateOptions {
|
||||
live_handoff: false,
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
.unwrap();
|
||||
let complete = prompt_to_complete_plain_update(&decisions, &release).unwrap();
|
||||
|
||||
assert!(err.contains("must restart"), "unexpected error: {err}");
|
||||
assert!(!err.contains("live handoff"), "unexpected error: {err}");
|
||||
assert!(!complete);
|
||||
std::env::remove_var(crate::session::SESSION_ENV_VAR);
|
||||
crate::session::clear_explicit_session_for_test();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue