diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-30 19:48:56 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-30 19:49:55 +0300 |
| commit | 26db26e416318e17a6e7f3f196ec72109fb37998 (patch) | |
| tree | 93b18888e9f4bc66d27ef1af5a4e7aad6db77339 /src/remote | |
| parent | c71c6c108245b91f6478d95a3abf9a9a9e2e57a9 (diff) | |
fix: keep remote sessions alive across disconnects
Diffstat (limited to 'src/remote')
| -rw-r--r-- | src/remote/unix.rs | 247 |
1 files changed, 221 insertions, 26 deletions
diff --git a/src/remote/unix.rs b/src/remote/unix.rs index c6432626..b3c7192d 100644 --- a/src/remote/unix.rs +++ b/src/remote/unix.rs @@ -956,6 +956,7 @@ enum RemoteServerStatus { version: Option<String>, protocol: Option<u32>, live_handoff: bool, + detached_server_daemon: bool, }, NotRunning, } @@ -963,10 +964,18 @@ enum RemoteServerStatus { #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum RemoteServerRestartReason { ProtocolMismatch, + DaemonDetachMissing, BinaryUpdated, VersionMismatch, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RemoteInstallRunningServerPlan { + KeepRunning, + LiveHandoff, + StopRequired(RemoteServerRestartReason), +} + fn ensure_remote_server_ready( ssh: &RemoteSsh, remote_herdr: &RemoteHerdr, @@ -979,14 +988,18 @@ fn ensure_remote_server_ready( version, protocol, live_handoff, + detached_server_daemon, } = status else { return Ok(()); }; - let Some(reason) = - remote_server_restart_reason(version.as_deref(), protocol, remote_binary_changed) - else { + let Some(reason) = remote_server_restart_reason( + version.as_deref(), + protocol, + detached_server_daemon, + remote_binary_changed, + ) else { return Ok(()); }; @@ -1014,17 +1027,21 @@ fn ensure_remote_server_ready( fn remote_server_restart_reason( version: Option<&str>, protocol: Option<u32>, + detached_server_daemon: bool, remote_binary_changed: bool, ) -> Option<RemoteServerRestartReason> { if protocol != Some(CURRENT_PROTOCOL) { return Some(RemoteServerRestartReason::ProtocolMismatch); } - if remote_binary_changed { - return Some(RemoteServerRestartReason::BinaryUpdated); + if !detached_server_daemon { + return Some(RemoteServerRestartReason::DaemonDetachMissing); } if version != Some(current_version().as_str()) { return Some(RemoteServerRestartReason::VersionMismatch); } + if remote_binary_changed { + return Some(RemoteServerRestartReason::BinaryUpdated); + } None } @@ -1062,23 +1079,48 @@ fn confirm_remote_install_with_running_server( }; let RemoteServerStatus::Running { version, - protocol: _, + protocol, live_handoff, - } = status + detached_server_daemon, + } = &status else { return Ok(false); }; + let plan = remote_install_running_server_plan( + version.as_deref(), + *protocol, + *detached_server_daemon, + true, + *live_handoff, + live_handoff_enabled, + ); + + if plan == RemoteInstallRunningServerPlan::KeepRunning { + if io::stdin().is_terminal() { + eprintln!("remote herdr server on {target} is already compatible:"); + eprintln!(" server: v{}", version_label(version.as_deref())); + eprintln!( + "Herdr will install {} without stopping the running remote server.", + current_version() + ); + } + return Ok(false); + } + if !io::stdin().is_terminal() { - if live_handoff_enabled && live_handoff { - return Ok(false); + match plan { + RemoteInstallRunningServerPlan::LiveHandoff => return Ok(false), + RemoteInstallRunningServerPlan::StopRequired(_) => { + return Err(io::Error::other(format!( + "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()) + ))); + } + RemoteInstallRunningServerPlan::KeepRunning => return Ok(false), } - return Err(io::Error::other(format!( - "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 { + if plan == RemoteInstallRunningServerPlan::LiveHandoff { eprintln!("remote herdr server on {target} is currently running:"); eprintln!(" server: v{}", version_label(version.as_deref())); eprintln!( @@ -1114,6 +1156,30 @@ fn confirm_remote_install_with_running_server( Ok(true) } +fn remote_install_running_server_plan( + version: Option<&str>, + protocol: Option<u32>, + detached_server_daemon: bool, + remote_binary_changed: bool, + live_handoff: bool, + live_handoff_enabled: bool, +) -> RemoteInstallRunningServerPlan { + let Some(reason) = remote_server_restart_reason( + version, + protocol, + detached_server_daemon, + remote_binary_changed, + ) else { + return RemoteInstallRunningServerPlan::KeepRunning; + }; + + if live_handoff_enabled && live_handoff { + return RemoteInstallRunningServerPlan::LiveHandoff; + } + + RemoteInstallRunningServerPlan::StopRequired(reason) +} + fn remote_server_status( ssh: &RemoteSsh, remote_herdr: &RemoteHerdr, @@ -1144,6 +1210,8 @@ struct RemoteServerStatusJson { #[derive(Debug, Deserialize)] struct RemoteServerCapabilitiesJson { live_handoff: bool, + #[serde(default)] + detached_server_daemon: bool, } fn parse_client_status_json(status: &str) -> Option<RemoteClientStatusJson> { @@ -1160,12 +1228,17 @@ fn parse_remote_server_status_json(status: &str) -> io::Result<RemoteServerStatu return Ok(RemoteServerStatus::NotRunning); } + let capabilities = parsed.capabilities; + Ok(RemoteServerStatus::Running { version: parsed.version, protocol: parsed.protocol, - live_handoff: parsed - .capabilities + live_handoff: capabilities + .as_ref() .is_some_and(|capabilities| capabilities.live_handoff), + detached_server_daemon: capabilities + .as_ref() + .is_some_and(|capabilities| capabilities.detached_server_daemon), }) } @@ -1199,6 +1272,11 @@ fn confirm_remote_server_stop( RemoteServerRestartReason::ProtocolMismatch => { eprintln!("the remote server must stop before this client can attach."); } + RemoteServerRestartReason::DaemonDetachMissing => { + eprintln!( + "the remote server was started by a herdr build that may not survive SSH connection loss. restart it so network drops disconnect only this client." + ); + } RemoteServerRestartReason::BinaryUpdated => { eprintln!( "the remote herdr binary was installed or replaced. restart the remote server so it uses the prepared binary." @@ -2446,19 +2524,20 @@ mod tests { fn parse_remote_server_status_json_reads_running_server() { assert_eq!( parse_remote_server_status_json( - r#"{"status":"running","running":true,"version":"0.6.0","protocol":8,"capabilities":{"live_handoff":true}}"# + r#"{"status":"running","running":true,"version":"0.6.0","protocol":8,"capabilities":{"live_handoff":true,"detached_server_daemon":true}}"# ) .unwrap(), RemoteServerStatus::Running { version: Some("0.6.0".into()), protocol: Some(8), - live_handoff: true + live_handoff: true, + detached_server_daemon: true } ); } #[test] - fn parse_remote_server_status_json_treats_missing_capability_as_no_handoff() { + fn parse_remote_server_status_json_treats_missing_capability_as_old_server() { assert_eq!( parse_remote_server_status_json( r#"{"status":"running","running":true,"version":"0.6.0","protocol":8}"# @@ -2467,7 +2546,8 @@ mod tests { RemoteServerStatus::Running { version: Some("0.6.0".into()), protocol: Some(8), - live_handoff: false + live_handoff: false, + detached_server_daemon: false } ); } @@ -2637,15 +2717,46 @@ mod tests { #[test] fn remote_server_restart_reason_requires_stop_for_protocol_mismatch() { assert_eq!( - remote_server_restart_reason(Some(¤t_version()), Some(0), false), + remote_server_restart_reason(Some(¤t_version()), Some(0), true, false), Some(RemoteServerRestartReason::ProtocolMismatch) ); } #[test] - fn remote_server_restart_reason_offers_restart_after_binary_update() { + fn remote_server_restart_reason_allows_unchanged_compatible_server() { + assert_eq!( + remote_server_restart_reason( + Some(¤t_version()), + Some(CURRENT_PROTOCOL), + true, + false + ), + None + ); + } + + #[test] + fn remote_server_restart_reason_requires_restart_for_old_daemon() { + assert_eq!( + remote_server_restart_reason( + Some(¤t_version()), + Some(CURRENT_PROTOCOL), + false, + false + ), + Some(RemoteServerRestartReason::DaemonDetachMissing) + ); + } + + #[test] + fn remote_server_restart_reason_requires_restart_after_helper_update() { assert_eq!( - remote_server_restart_reason(Some(¤t_version()), Some(CURRENT_PROTOCOL), true), + remote_server_restart_reason( + Some(¤t_version()), + Some(CURRENT_PROTOCOL), + true, + true + ), Some(RemoteServerRestartReason::BinaryUpdated) ); } @@ -2653,11 +2764,11 @@ mod tests { #[test] fn remote_server_restart_reason_offers_restart_for_version_mismatch() { assert_eq!( - remote_server_restart_reason(Some("0.0.0"), Some(CURRENT_PROTOCOL), false), + remote_server_restart_reason(Some("0.0.0"), Some(CURRENT_PROTOCOL), true, false), Some(RemoteServerRestartReason::VersionMismatch) ); assert_eq!( - remote_server_restart_reason(None, Some(CURRENT_PROTOCOL), false), + remote_server_restart_reason(None, Some(CURRENT_PROTOCOL), true, false), Some(RemoteServerRestartReason::VersionMismatch) ); } @@ -2665,12 +2776,96 @@ mod tests { #[test] fn remote_server_restart_reason_allows_current_server() { assert_eq!( - remote_server_restart_reason(Some(¤t_version()), Some(CURRENT_PROTOCOL), false), + remote_server_restart_reason( + Some(¤t_version()), + Some(CURRENT_PROTOCOL), + true, + false + ), None ); } #[test] + fn remote_install_plan_keeps_compatible_running_server() { + assert_eq!( + remote_install_running_server_plan( + Some(¤t_version()), + Some(CURRENT_PROTOCOL), + true, + false, + false, + false + ), + RemoteInstallRunningServerPlan::KeepRunning + ); + } + + #[test] + fn remote_install_plan_requires_stop_for_old_daemon() { + assert_eq!( + remote_install_running_server_plan( + Some(¤t_version()), + Some(CURRENT_PROTOCOL), + false, + true, + false, + false + ), + RemoteInstallRunningServerPlan::StopRequired( + RemoteServerRestartReason::DaemonDetachMissing + ) + ); + } + + #[test] + fn remote_install_plan_requires_stop_after_helper_update() { + assert_eq!( + remote_install_running_server_plan( + Some(¤t_version()), + Some(CURRENT_PROTOCOL), + true, + true, + false, + false + ), + RemoteInstallRunningServerPlan::StopRequired(RemoteServerRestartReason::BinaryUpdated) + ); + } + + #[test] + fn remote_install_plan_requires_stop_for_incompatible_running_server() { + assert_eq!( + remote_install_running_server_plan( + Some("0.0.0"), + Some(CURRENT_PROTOCOL), + true, + true, + false, + false + ), + RemoteInstallRunningServerPlan::StopRequired( + RemoteServerRestartReason::VersionMismatch + ) + ); + } + + #[test] + fn remote_install_plan_uses_live_handoff_for_incompatible_running_server() { + assert_eq!( + remote_install_running_server_plan( + Some("0.0.0"), + Some(CURRENT_PROTOCOL), + true, + true, + true, + true + ), + RemoteInstallRunningServerPlan::LiveHandoff + ); + } + + #[test] fn install_source_description_uses_override_binary() { let platform = RemotePlatform { os: "linux", |
