aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.rs17
-rw-r--r--src/remote.rs88
-rw-r--r--src/server/handoff.rs36
-rw-r--r--src/server/headless.rs10
-rw-r--r--src/update.rs208
5 files changed, 295 insertions, 64 deletions
diff --git a/src/main.rs b/src/main.rs
index 9a33555e..28dfd29d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -345,7 +345,19 @@ fn main() -> io::Result<()> {
}
if args.get(1).map(|s| s.as_str()) == Some("update") {
- match update::self_update() {
+ let options = match update::parse_self_update_args(&args[2..]) {
+ Ok(options) => options,
+ Err(err) if err.starts_with("usage:") => {
+ eprintln!("{err}");
+ std::process::exit(0);
+ }
+ Err(err) => {
+ eprintln!("{err}");
+ eprintln!("usage: herdr update [--handoff]");
+ std::process::exit(2);
+ }
+ };
+ match update::self_update(options) {
Ok(_) => return Ok(()),
Err(e) => {
if e.starts_with("self-update is disabled") {
@@ -365,7 +377,7 @@ fn main() -> io::Result<()> {
println!(" herdr --session <name> [options]");
println!(" herdr --remote <ssh-target> [--session <name>]");
println!(" herdr session attach <name>");
- println!(" herdr update");
+ println!(" herdr update [--handoff]");
println!(" herdr server stop");
println!(" herdr server reload-config");
println!(" herdr config <subcommand> ...");
@@ -440,6 +452,7 @@ fn main() -> io::Result<()> {
println!(" --remote <target> Attach through SSH to a remote Herdr server");
println!(" --remote-keybindings <local|server>");
println!(" Keybindings for --remote app attach (default: local)");
+ println!(" --handoff Opt into live handoff for update or remote attach");
println!(" --default-config Print default configuration and exit");
println!(" --version, -V Print version and exit");
println!(" --help, -h Show this help");
diff --git a/src/remote.rs b/src/remote.rs
index 990ffb7b..9a4cc774 100644
--- a/src/remote.rs
+++ b/src/remote.rs
@@ -54,6 +54,7 @@ impl RemoteKeybindings {
pub(crate) struct RemoteLaunch {
pub(crate) target: String,
pub(crate) keybindings: RemoteKeybindings,
+ pub(crate) live_handoff: bool,
}
pub(crate) fn extract_remote_args(
@@ -67,9 +68,15 @@ pub(crate) fn extract_remote_args(
let mut remote_target = None;
let mut keybindings = RemoteKeybindings::Local;
let mut keybindings_seen = false;
+ let mut live_handoff = false;
let mut index = 1;
while index < args.len() {
let arg = &args[index];
+ if arg == "--handoff" {
+ live_handoff = true;
+ index += 1;
+ continue;
+ }
if arg == "--remote" {
if remote_target.is_some() {
return Err("--remote can only be specified once".to_string());
@@ -118,10 +125,14 @@ pub(crate) fn extract_remote_args(
let remote = remote_target.map(|target| RemoteLaunch {
target,
keybindings,
+ live_handoff,
});
if remote.is_none() && keybindings_seen {
return Err("--remote-keybindings requires --remote".to_string());
}
+ if remote.is_none() && live_handoff {
+ cleaned.push("--handoff".to_string());
+ }
Ok((cleaned, remote))
}
@@ -143,13 +154,19 @@ pub(crate) fn run_remote(remote: RemoteLaunch) -> io::Result<()> {
let program = std::env::args()
.next()
.unwrap_or_else(|| "herdr".to_string());
- let reattach_command =
- reattach_command(&program, &remote.target, &session_name, remote.keybindings);
- let prepared_remote = prepare_remote_herdr(&remote.target)?;
+ let reattach_command = reattach_command(
+ &program,
+ &remote.target,
+ &session_name,
+ remote.keybindings,
+ remote.live_handoff,
+ );
+ let prepared_remote = prepare_remote_herdr(&remote.target, remote.live_handoff)?;
ensure_remote_server_ready(
&remote.target,
&prepared_remote.remote_herdr,
prepared_remote.installed_or_replaced,
+ remote.live_handoff,
)?;
let _bridge = SshStdioBridge::start(
@@ -318,7 +335,10 @@ impl InstallSource {
}
}
-fn prepare_remote_herdr(target: &str) -> io::Result<PreparedRemoteHerdr> {
+fn prepare_remote_herdr(
+ target: &str,
+ live_handoff_enabled: bool,
+) -> io::Result<PreparedRemoteHerdr> {
let platform = detect_remote_platform(target)?;
let remote_herdr = RemoteHerdr::for_platform(platform);
let override_binary = remote_binary_override_path()?;
@@ -347,7 +367,11 @@ fn prepare_remote_herdr(target: &str) -> io::Result<PreparedRemoteHerdr> {
.ok()
.and_then(|exists| exists.then_some(&remote_herdr))
}) {
- confirm_remote_install_with_running_server(target, status_probe_herdr)?;
+ confirm_remote_install_with_running_server(
+ target,
+ status_probe_herdr,
+ live_handoff_enabled,
+ )?;
}
confirm_remote_install(
target,
@@ -552,6 +576,7 @@ fn ensure_remote_server_ready(
target: &str,
remote_herdr: &RemoteHerdr,
remote_binary_changed: bool,
+ live_handoff_enabled: bool,
) -> io::Result<()> {
let status = remote_server_status(target, remote_herdr)?;
let RemoteServerStatus::Running {
@@ -569,7 +594,9 @@ fn ensure_remote_server_ready(
return Ok(());
};
- if live_handoff && confirm_remote_server_handoff(target, version.as_deref(), protocol, reason)?
+ if live_handoff_enabled
+ && live_handoff
+ && confirm_remote_server_handoff(target, version.as_deref(), protocol, reason)?
{
match live_handoff_remote_server(target, remote_herdr) {
Ok(()) => return Ok(()),
@@ -606,6 +633,7 @@ fn remote_server_restart_reason(
fn confirm_remote_install_with_running_server(
target: &str,
remote_herdr: &RemoteHerdr,
+ live_handoff_enabled: bool,
) -> io::Result<()> {
let status = match remote_server_status(target, remote_herdr) {
Ok(status) => status,
@@ -641,13 +669,13 @@ fn confirm_remote_install_with_running_server(
else {
return Ok(());
};
- if live_handoff {
+ if live_handoff_enabled && live_handoff {
return Ok(());
}
if !io::stdin().is_terminal() {
return Err(io::Error::other(format!(
- "remote herdr server on {target} is running v{} protocol {}, but it does not advertise live handoff; run from an interactive terminal to approve updating the remote binary",
+ "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)
)));
@@ -660,10 +688,7 @@ fn confirm_remote_install_with_running_server(
protocol_label(protocol)
);
eprintln!(
- "this server does not advertise live handoff, so this attach cannot preserve its running panes during the update."
- );
- eprintln!(
- "future remote updates can preserve panes after the remote server has run a handoff-capable version once."
+ "this attach will not preserve running panes unless you pass --handoff and the remote server supports live handoff."
);
eprintln!();
eprint!("continue installing the remote herdr binary? [Y/n] ");
@@ -1118,6 +1143,7 @@ fn reattach_command(
target: &str,
session_name: &str,
keybindings: RemoteKeybindings,
+ live_handoff: bool,
) -> String {
let program = if program.is_empty() { "herdr" } else { program };
let mut command = format!("{} --remote {}", shell_quote(program), shell_quote(target));
@@ -1125,6 +1151,9 @@ fn reattach_command(
command.push_str(" --remote-keybindings ");
command.push_str(keybindings.as_str());
}
+ if live_handoff {
+ command.push_str(" --handoff");
+ }
if session_name != crate::session::DEFAULT_SESSION_NAME {
command.push_str(" --session ");
command.push_str(&shell_quote(session_name));
@@ -1471,6 +1500,28 @@ mod tests {
}
#[test]
+ fn extract_remote_args_accepts_explicit_handoff() {
+ let args = vec!["herdr".into(), "--remote=dev".into(), "--handoff".into()];
+
+ let (cleaned, remote) = extract_remote_args(&args).unwrap();
+
+ assert_eq!(cleaned, vec!["herdr"]);
+ let remote = remote.unwrap();
+ assert_eq!(remote.target, "dev");
+ assert!(remote.live_handoff);
+ }
+
+ #[test]
+ fn extract_remote_args_preserves_handoff_without_remote() {
+ let args = vec!["herdr".into(), "update".into(), "--handoff".into()];
+
+ let (cleaned, remote) = extract_remote_args(&args).unwrap();
+
+ assert_eq!(cleaned, args);
+ assert!(remote.is_none());
+ }
+
+ #[test]
fn extract_remote_args_rejects_remote_keybindings_without_remote() {
let args = vec!["herdr".into(), "--remote-keybindings=server".into()];
let err = extract_remote_args(&args).unwrap_err();
@@ -1551,6 +1602,7 @@ mod tests {
"user@host",
"work",
RemoteKeybindings::Local,
+ false,
),
"target/release/herdr --remote user@host --session work"
);
@@ -1560,6 +1612,7 @@ mod tests {
"host name",
crate::session::DEFAULT_SESSION_NAME,
RemoteKeybindings::Local,
+ false,
),
"herdr --remote 'host name'"
);
@@ -1569,9 +1622,20 @@ mod tests {
"host",
crate::session::DEFAULT_SESSION_NAME,
RemoteKeybindings::Server,
+ false,
),
"herdr --remote host --remote-keybindings server"
);
+ assert_eq!(
+ reattach_command(
+ "herdr",
+ "host",
+ crate::session::DEFAULT_SESSION_NAME,
+ RemoteKeybindings::Local,
+ true,
+ ),
+ "herdr --remote host --handoff"
+ );
}
#[test]
diff --git a/src/server/handoff.rs b/src/server/handoff.rs
index d38bdc29..6dadfdf4 100644
--- a/src/server/handoff.rs
+++ b/src/server/handoff.rs
@@ -9,7 +9,7 @@ use std::os::unix::process::CommandExt;
#[cfg(unix)]
use std::path::{Path, PathBuf};
#[cfg(unix)]
-use std::process::Command;
+use std::process::{Child, Command};
#[cfg(unix)]
use std::time::Duration;
@@ -71,7 +71,7 @@ pub(crate) fn spawn_handoff_import(
import_exe: Option<&Path>,
socket_path: &Path,
token: &str,
-) -> io::Result<u32> {
+) -> io::Result<Child> {
let fallback_exe;
let exe = if let Some(import_exe) = import_exe {
import_exe
@@ -94,7 +94,7 @@ pub(crate) fn spawn_handoff_import(
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
- let child = command.spawn().map_err(|err| {
+ command.spawn().map_err(|err| {
io::Error::new(
err.kind(),
format!(
@@ -102,8 +102,34 @@ pub(crate) fn spawn_handoff_import(
exe.display()
),
)
- })?;
- Ok(child.id())
+ })
+}
+
+#[cfg(unix)]
+pub(crate) fn cleanup_failed_import_child(child: &mut Child) {
+ let pid = child.id();
+ match child.try_wait() {
+ Ok(Some(status)) => {
+ info!(pid, status = %status, "handoff import server exited during rollback");
+ return;
+ }
+ Ok(None) => {}
+ Err(err) => {
+ warn!(pid, err = %err, "failed to inspect handoff import server before rollback");
+ }
+ }
+
+ if let Err(err) = child.kill() {
+ warn!(pid, err = %err, "failed to kill handoff import server during rollback");
+ }
+ match child.wait() {
+ Ok(status) => {
+ info!(pid, status = %status, "handoff import server reaped during rollback");
+ }
+ Err(err) => {
+ warn!(pid, err = %err, "failed to reap handoff import server during rollback");
+ }
+ }
}
#[cfg(unix)]
diff --git a/src/server/headless.rs b/src/server/headless.rs
index bde4d362..2b8550b4 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -655,17 +655,18 @@ impl HeadlessServer {
params.expected_protocol,
params.expected_version,
);
- let child_pid = match crate::server::handoff::spawn_handoff_import(
+ let mut import_child = match crate::server::handoff::spawn_handoff_import(
import_exe.as_deref(),
&socket_path,
&token,
) {
- Ok(child_pid) => child_pid,
+ Ok(child) => child,
Err(err) => {
self.rollback_handoff_before_commit(&socket_path, &paused_terminal_ids);
return Err(err);
}
};
+ let child_pid = import_child.id();
info!(pid = child_pid, socket = %socket_path.display(), "spawned handoff import server");
let mut fds = Vec::new();
@@ -682,6 +683,7 @@ impl HeadlessServer {
for fd in fds {
let _ = unsafe { libc::close(fd) };
}
+ crate::server::handoff::cleanup_failed_import_child(&mut import_child);
self.rollback_handoff_before_commit(&socket_path, &paused_terminal_ids);
return Err(err);
}
@@ -697,6 +699,7 @@ impl HeadlessServer {
for fd in fds {
let _ = unsafe { libc::close(fd) };
}
+ crate::server::handoff::cleanup_failed_import_child(&mut import_child);
self.rollback_handoff_before_commit(&socket_path, &paused_terminal_ids);
return Err(err);
}
@@ -707,6 +710,7 @@ impl HeadlessServer {
let _ = unsafe { libc::close(fd) };
}
if let Err(err) = send_result {
+ crate::server::handoff::cleanup_failed_import_child(&mut import_child);
self.rollback_handoff_before_commit(&socket_path, &paused_terminal_ids);
return Err(err);
}
@@ -718,6 +722,7 @@ impl HeadlessServer {
}
let _ = remove_socket_file_if_owned(&self.client_socket_path, self.client_socket_identity);
if let Err(err) = crate::server::handoff::wait_ready(&mut stream) {
+ crate::server::handoff::cleanup_failed_import_child(&mut import_child);
match self.wait_then_restore_public_sockets_after_failed_handoff() {
Ok(()) => {
self.rollback_handoff_before_commit(&socket_path, &paused_terminal_ids);
@@ -734,6 +739,7 @@ impl HeadlessServer {
)));
}
if let Err(err) = crate::server::handoff::report_committed(&mut stream) {
+ crate::server::handoff::cleanup_failed_import_child(&mut import_child);
match self.wait_then_restore_public_sockets_after_failed_handoff() {
Ok(()) => {
self.rollback_handoff_before_commit(&socket_path, &paused_terminal_ids);
diff --git a/src/update.rs b/src/update.rs
index afd13bb4..afbbc662 100644
--- a/src/update.rs
+++ b/src/update.rs
@@ -27,6 +27,7 @@ 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";
const SERVER_STOP_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5);
+const SERVER_HANDOFF_REQUEST_TIMEOUT: Duration = Duration::from_secs(240);
const SERVER_HANDOFF_CONFIRM_TIMEOUT: Duration = Duration::from_secs(30);
const SERVER_SHUTDOWN_POLL_INTERVAL: Duration = Duration::from_millis(100);
const STAR_PROMPT_REPO: &str = "ogulcancelik/herdr";
@@ -422,7 +423,10 @@ fn version_label(version: Option<&str>) -> &str {
version.unwrap_or("unknown")
}
-fn update_requires_live_handoff(server: &crate::api::RuntimeStatus, release: &ReleaseInfo) -> bool {
+fn update_requires_server_restart(
+ server: &crate::api::RuntimeStatus,
+ release: &ReleaseInfo,
+) -> bool {
match (server.protocol, release.target_protocol) {
(Some(server_protocol), Some(target_protocol)) => server_protocol != target_protocol,
_ => true,
@@ -449,7 +453,7 @@ fn parse_live_handoff_before_update_response(input: &str) -> Option<bool> {
struct RunningServerUpdatePlan {
target: RunningUpdateTarget,
server: crate::api::RuntimeStatus,
- requires_live_handoff: bool,
+ requires_server_restart: bool,
}
impl RunningServerUpdatePlan {
@@ -558,7 +562,7 @@ fn plan_running_server_updates(
};
plans.push(RunningServerUpdatePlan {
- requires_live_handoff: update_requires_live_handoff(&server, release),
+ requires_server_restart: update_requires_server_restart(&server, release),
server,
target,
});
@@ -670,19 +674,38 @@ fn target_client_protocol_server_is_running() -> Result<bool, String> {
}))
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
+pub(crate) struct SelfUpdateOptions {
+ pub(crate) live_handoff: bool,
+}
+
+pub(crate) fn parse_self_update_args(args: &[String]) -> Result<SelfUpdateOptions, String> {
+ let mut options = SelfUpdateOptions::default();
+ for arg in args {
+ match arg.as_str() {
+ "--handoff" => options.live_handoff = true,
+ "--help" | "-h" => {
+ return Err("usage: herdr update [--handoff]".to_string());
+ }
+ _ => return Err(format!("unknown update option: {arg}")),
+ }
+ }
+ Ok(options)
+}
+
fn prompt_to_stop_old_servers_before_update(
plans: &[RunningServerUpdatePlan],
release: &ReleaseInfo,
) -> Result<bool, String> {
if !io::stdin().is_terminal() {
return Err(
- "one or more herdr targets are running and cannot perform live handoff for this update; run `herdr update` from an interactive terminal, or stop those targets and run `herdr update` again"
+ "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"
.to_string(),
);
}
eprintln!(
- "these running herdr targets are too old to preserve panes during this update to v{}:",
+ "these running herdr targets must restart to use v{}:",
release.version
);
for plan in plans {
@@ -693,9 +716,8 @@ fn prompt_to_stop_old_servers_before_update(
protocol_label(plan.server.protocol)
);
}
- eprintln!(
- "herdr can leave them running, or stop them after installing the update. stopping them will exit their pane processes."
- );
+ eprintln!("herdr can leave them running, or stop them after installing the update.");
+ eprintln!("stopping them will exit their pane processes.");
loop {
eprint!("stop these old targets after updating? [y/N] ");
@@ -722,12 +744,37 @@ fn prompt_to_stop_old_servers_before_update(
fn confirm_running_server_update_action(
plans: Vec<RunningServerUpdatePlan>,
release: &ReleaseInfo,
+ options: SelfUpdateOptions,
) -> Result<Vec<RunningServerUpdateDecision>, String> {
if plans.is_empty() {
return Ok(Vec::new());
}
- print_running_session_update_summary(&plans, release);
+ 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 }
+ })
+ .collect());
+ }
let handoff_supported: Vec<&RunningServerUpdatePlan> = plans
.iter()
@@ -735,7 +782,7 @@ fn confirm_running_server_update_action(
.collect();
let handoff_unsupported_requiring_update: Vec<&RunningServerUpdatePlan> = plans
.iter()
- .filter(|plan| !server_supports_live_handoff(&plan.server) && plan.requires_live_handoff)
+ .filter(|plan| !server_supports_live_handoff(&plan.server) && plan.requires_server_restart)
.collect();
let live_handoff = if handoff_supported.is_empty() {
@@ -744,7 +791,7 @@ fn confirm_running_server_update_action(
prompt_to_live_handoff_sessions_before_update(
&handoff_supported,
release,
- plans.iter().any(|plan| plan.requires_live_handoff),
+ plans.iter().any(|plan| plan.requires_server_restart),
)?
};
@@ -763,7 +810,7 @@ fn confirm_running_server_update_action(
let action = if server_supports_live_handoff(&plan.server) && live_handoff {
RunningServerUpdateAction::LiveHandoff
} else if !server_supports_live_handoff(&plan.server)
- && plan.requires_live_handoff
+ && plan.requires_server_restart
&& stop_unsupported
{
RunningServerUpdateAction::StopOldServer
@@ -776,21 +823,34 @@ fn confirm_running_server_update_action(
Ok(decisions)
}
-fn print_running_session_update_summary(plans: &[RunningServerUpdatePlan], release: &ReleaseInfo) {
+fn print_running_session_update_summary(
+ plans: &[RunningServerUpdatePlan],
+ release: &ReleaseInfo,
+ options: SelfUpdateOptions,
+) {
eprintln!("running herdr targets:");
for plan in plans {
- let capability = if server_supports_live_handoff(&plan.server) {
- "handoff supported"
+ if options.live_handoff {
+ let capability = if server_supports_live_handoff(&plan.server) {
+ "handoff supported"
+ } else {
+ "too old for handoff"
+ };
+ eprintln!(
+ " {}: v{} protocol {} ({})",
+ plan.label(),
+ version_label(plan.server.version.as_deref()),
+ protocol_label(plan.server.protocol),
+ capability
+ );
} else {
- "too old for handoff"
- };
- eprintln!(
- " {}: v{} protocol {} ({})",
- plan.label(),
- version_label(plan.server.version.as_deref()),
- protocol_label(plan.server.protocol),
- capability
- );
+ eprintln!(
+ " {}: v{} protocol {}",
+ plan.label(),
+ version_label(plan.server.version.as_deref()),
+ protocol_label(plan.server.protocol)
+ );
+ }
}
eprintln!(
" update: v{} protocol {}",
@@ -1130,7 +1190,7 @@ fn live_handoff_server_via_api_for_update_at(
) -> Result<(), String> {
live_handoff_server_via_api_for_release_at(
socket_path,
- SERVER_HANDOFF_CONFIRM_TIMEOUT,
+ SERVER_HANDOFF_REQUEST_TIMEOUT,
updated_exe,
release,
)
@@ -1679,7 +1739,7 @@ fn maybe_offer_star_after_successful_update() {
// ---------------------------------------------------------------------------
/// Manual self-update command (`herdr update`).
-pub fn self_update() -> Result<Version, String> {
+pub fn self_update(options: SelfUpdateOptions) -> Result<Version, String> {
if is_homebrew_managed_install() {
return Err(format!(
"self-update is disabled for Homebrew installs; run `{HOMEBREW_UPDATE_COMMAND}`"
@@ -1710,7 +1770,7 @@ pub fn self_update() -> 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)?;
+ confirm_running_server_update_action(running_server_plans, &release, options)?;
eprintln!("downloading v{}...", release.version);
if let Err(e) =
@@ -2096,6 +2156,24 @@ mod tests {
}
#[test]
+ fn self_update_args_gate_live_handoff() {
+ assert_eq!(
+ parse_self_update_args(&[]).unwrap(),
+ SelfUpdateOptions {
+ live_handoff: false
+ }
+ );
+ assert_eq!(
+ parse_self_update_args(&["--handoff".to_string()]).unwrap(),
+ SelfUpdateOptions { live_handoff: true }
+ );
+ assert_eq!(
+ parse_self_update_args(&["--unknown".to_string()]).unwrap_err(),
+ "unknown update option: --unknown"
+ );
+ }
+
+ #[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));
@@ -2107,7 +2185,7 @@ mod tests {
}
#[test]
- fn update_requires_live_handoff_when_target_protocol_differs_or_unknown() {
+ fn update_requires_server_restart_when_target_protocol_differs_or_unknown() {
let server = crate::api::RuntimeStatus {
version: Some("0.5.5".to_string()),
protocol: Some(2),
@@ -2128,9 +2206,53 @@ mod tests {
..compatible_release.clone()
};
- assert!(!update_requires_live_handoff(&server, &compatible_release));
- assert!(update_requires_live_handoff(&server, &incompatible_release));
- assert!(update_requires_live_handoff(&server, &unknown_release));
+ assert!(!update_requires_server_restart(
+ &server,
+ &compatible_release
+ ));
+ assert!(update_requires_server_restart(
+ &server,
+ &incompatible_release
+ ));
+ assert!(update_requires_server_restart(&server, &unknown_release));
+ }
+
+ #[test]
+ fn plain_update_requires_restart_for_supported_servers_without_handoff() {
+ assert!(
+ !io::stdin().is_terminal(),
+ "this test relies on noninteractive test stdin"
+ );
+ let release = fake_release("9.8.7", Some(77));
+ let plan = RunningServerUpdatePlan {
+ target: RunningUpdateTarget {
+ name: Some("work".to_string()),
+ label: "work".to_string(),
+ stop_command: "herdr session stop work".to_string(),
+ attach_command: Some("herdr session attach work".to_string()),
+ socket_path: crate::session::api_socket_path_for(Some("work")),
+ client_socket_path: crate::session::client_socket_path_for(Some("work")),
+ must_be_running: true,
+ },
+ requires_server_restart: true,
+ server: crate::api::RuntimeStatus {
+ version: Some("0.6.2".to_string()),
+ protocol: Some(76),
+ capabilities: Some(crate::api::schema::ServerCapabilities { live_handoff: true }),
+ },
+ };
+
+ let err = confirm_running_server_update_action(
+ vec![plan],
+ &release,
+ SelfUpdateOptions {
+ live_handoff: false,
+ },
+ )
+ .unwrap_err();
+
+ assert!(err.contains("must restart"), "unexpected error: {err}");
+ assert!(!err.contains("live handoff"), "unexpected error: {err}");
}
#[test]
@@ -2289,7 +2411,7 @@ mod tests {
}
#[test]
- fn noninteractive_update_requires_handoff_fails_before_install() {
+ fn noninteractive_plain_update_requiring_restart_fails_without_handoff() {
let _guard = env_lock().lock().unwrap();
assert!(
!io::stdin().is_terminal(),
@@ -2318,21 +2440,21 @@ mod tests {
client_socket_path: crate::session::client_socket_path_for(Some("work")),
must_be_running: true,
},
- requires_live_handoff: true,
+ requires_server_restart: true,
server,
};
- let err =
- prompt_to_live_handoff_sessions_before_update(&[&plan], &release, true).unwrap_err();
+ let err = confirm_running_server_update_action(
+ vec![plan],
+ &release,
+ SelfUpdateOptions {
+ live_handoff: false,
+ },
+ )
+ .unwrap_err();
- assert!(
- err.contains("requires live server handoff"),
- "unexpected error: {err}"
- );
- assert!(
- err.contains("run `herdr update` from an interactive terminal"),
- "unexpected error: {err}"
- );
+ assert!(err.contains("must restart"), "unexpected error: {err}");
+ assert!(!err.contains("live handoff"), "unexpected error: {err}");
std::env::remove_var(crate::session::SESSION_ENV_VAR);
crate::session::clear_explicit_session_for_test();
}