From cc88b3b8e5bb9f7d9f23ed6ae85a52fd7b5b9ed6 Mon Sep 17 00:00:00 2001 From: Can Celik Date: Wed, 2 Sep 2026 02:45:20 +0300 Subject: feat: add stable client endpoint compatibility (#3509) * feat: add stable client endpoint compatibility * fix: keep portable-pty patch checks composable * ci: run maintenance checks in pull requests * fix: reject endpoint errors from terminal clients * feat: keep compatible servers running across updates --- src/cli/status.rs | 135 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 120 insertions(+), 15 deletions(-) (limited to 'src/cli') diff --git a/src/cli/status.rs b/src/cli/status.rs index 74bb3e2a..4dba81b7 100644 --- a/src/cli/status.rs +++ b/src/cli/status.rs @@ -98,12 +98,20 @@ fn print_full_status(json: bool) -> std::io::Result { crate::config::Config::load().config.update.channel.as_str() ); println!(" protocol: {}", crate::protocol::PROTOCOL_VERSION); + println!( + " endpoint_protocol_generation: {}", + crate::protocol::endpoint::ENDPOINT_PROTOCOL_GENERATION + ); println!(); println!("server:"); print_server_status_body(&server, " "); println!(); println!("update:"); println!(" restart_needed: {}", restart_needed_label(&server)); + println!( + " server_binary_stale: {}", + server_binary_stale_label(&server) + ); Ok(0) } @@ -130,6 +138,10 @@ fn print_client_status(json: bool) -> std::io::Result<()> { crate::config::Config::load().config.update.channel.as_str() ); println!("protocol: {}", crate::protocol::PROTOCOL_VERSION); + println!( + "endpoint_protocol_generation: {}", + crate::protocol::endpoint::ENDPOINT_PROTOCOL_GENERATION + ); println!("binary: {}", current_exe_label()); Ok(()) } @@ -137,12 +149,21 @@ fn print_client_status(json: bool) -> std::io::Result<()> { fn print_server_status_body(server: &ServerRuntimeStatus, indent: &str) { match server { ServerRuntimeStatus::Running { - version, protocol, .. + version, + protocol, + capabilities, } => { println!("{indent}status: running"); println!("{indent}version: {}", option_label(version.as_deref())); - println!("{indent}protocol: {}", protocol_label(*protocol)); - println!("{indent}compatible: {}", compatibility_label(*protocol)); + println!( + "{indent}endpoint_compatible: {}", + endpoint_compatibility_label(capabilities.as_ref()) + ); + println!("{indent}private_protocol: {}", protocol_label(*protocol)); + println!( + "{indent}private_protocol_compatible: {}", + compatibility_label(*protocol) + ); println!("{indent}socket: {}", api::socket_path().display()); } ServerRuntimeStatus::NotRunning => { @@ -191,14 +212,33 @@ fn compatibility_label(protocol: Option) -> &'static str { } } +fn endpoint_compatibility_label( + capabilities: Option<&crate::api::schema::ServerCapabilities>, +) -> &'static str { + match capabilities.and_then(|value| value.endpoint_protocol_generation) { + Some(generation) + if generation == crate::protocol::endpoint::ENDPOINT_PROTOCOL_GENERATION => + { + "yes" + } + Some(_) => "no", + None => "unknown", + } +} + fn restart_needed_label(server: &ServerRuntimeStatus) -> &'static str { - match server { - ServerRuntimeStatus::Running { version, .. } => match version.as_deref() { - Some(version) if version == crate::build_info::version() => "no", - Some(_) => "yes", - None => "unknown", - }, - ServerRuntimeStatus::NotRunning => "no", + match restart_needed_bool(server) { + Some(true) => "yes", + Some(false) => "no", + None => "unknown", + } +} + +fn server_binary_stale_label(server: &ServerRuntimeStatus) -> &'static str { + match server_binary_stale_bool(server) { + Some(true) => "yes", + Some(false) => "no", + None => "unknown", } } @@ -214,6 +254,7 @@ struct ClientStatusJson { version: String, channel: &'static str, protocol: u32, + endpoint_protocol_generation: u32, binary: String, session: Option, } @@ -226,20 +267,24 @@ struct ServerStatusJson { protocol: Option, capabilities: Option, compatible: Option, + endpoint_compatible: Option, socket: String, session: Option, restart_needed: Option, + server_binary_stale: Option, } #[derive(Serialize)] struct ServerCapabilitiesJson { live_handoff: bool, detached_server_daemon: bool, + endpoint_protocol_generation: Option, } #[derive(Serialize)] struct UpdateStatusJson { restart_needed: Option, + server_binary_stale: Option, } fn client_status_json() -> ClientStatusJson { @@ -247,6 +292,7 @@ fn client_status_json() -> ClientStatusJson { version: crate::build_info::version(), channel: crate::config::Config::load().config.update.channel.as_str(), protocol: crate::protocol::PROTOCOL_VERSION, + endpoint_protocol_generation: crate::protocol::endpoint::ENDPOINT_PROTOCOL_GENERATION, binary: current_exe_label(), session: crate::session::active_name(), } @@ -268,11 +314,18 @@ fn server_status_json(server: &ServerRuntimeStatus) -> ServerStatusJson { .map(|capabilities| ServerCapabilitiesJson { live_handoff: capabilities.live_handoff, detached_server_daemon: capabilities.detached_server_daemon, + endpoint_protocol_generation: capabilities.endpoint_protocol_generation, }), compatible: protocol.map(|value| value == crate::protocol::PROTOCOL_VERSION), + endpoint_compatible: capabilities.as_ref().and_then(|capabilities| { + capabilities.endpoint_protocol_generation.map(|generation| { + generation == crate::protocol::endpoint::ENDPOINT_PROTOCOL_GENERATION + }) + }), socket: api::socket_path().display().to_string(), session: crate::session::active_name(), restart_needed: restart_needed_bool(server), + server_binary_stale: server_binary_stale_bool(server), }, ServerRuntimeStatus::NotRunning => ServerStatusJson { status: "not_running", @@ -281,9 +334,11 @@ fn server_status_json(server: &ServerRuntimeStatus) -> ServerStatusJson { protocol: None, capabilities: None, compatible: None, + endpoint_compatible: None, socket: api::socket_path().display().to_string(), session: crate::session::active_name(), restart_needed: Some(false), + server_binary_stale: Some(false), }, } } @@ -291,16 +346,27 @@ fn server_status_json(server: &ServerRuntimeStatus) -> ServerStatusJson { fn update_status_json(server: &ServerRuntimeStatus) -> UpdateStatusJson { UpdateStatusJson { restart_needed: restart_needed_bool(server), + server_binary_stale: server_binary_stale_bool(server), } } fn restart_needed_bool(server: &ServerRuntimeStatus) -> Option { match server { - ServerRuntimeStatus::Running { version, .. } => match version.as_deref() { - Some(version) if version == crate::build_info::version() => Some(false), - Some(_) => Some(true), - None => None, - }, + ServerRuntimeStatus::Running { capabilities, .. } => Some( + capabilities + .as_ref() + .and_then(|value| value.endpoint_protocol_generation) + != Some(crate::protocol::endpoint::ENDPOINT_PROTOCOL_GENERATION), + ), + ServerRuntimeStatus::NotRunning => Some(false), + } +} + +fn server_binary_stale_bool(server: &ServerRuntimeStatus) -> Option { + match server { + ServerRuntimeStatus::Running { version, .. } => version + .as_deref() + .map(|version| version != crate::build_info::version()), ServerRuntimeStatus::NotRunning => Some(false), } } @@ -322,3 +388,42 @@ fn print_status_help() { eprintln!(" herdr status server [--json] show running server status"); eprintln!(" herdr status client [--json] show local client binary status"); } + +#[cfg(test)] +mod tests { + use super::*; + + fn running_server( + version: Option<&str>, + endpoint_generation: Option, + ) -> ServerRuntimeStatus { + ServerRuntimeStatus::Running { + version: version.map(str::to_owned), + protocol: Some(crate::protocol::PROTOCOL_VERSION), + capabilities: Some(crate::api::schema::ServerCapabilities { + live_handoff: true, + detached_server_daemon: true, + endpoint_protocol_generation: endpoint_generation, + }), + } + } + + #[test] + fn stale_compatible_server_does_not_require_restart() { + let server = running_server( + Some("0.0.0-old"), + Some(crate::protocol::endpoint::ENDPOINT_PROTOCOL_GENERATION), + ); + + assert_eq!(restart_needed_bool(&server), Some(false)); + assert_eq!(server_binary_stale_bool(&server), Some(true)); + } + + #[test] + fn server_without_endpoint_baseline_requires_restart() { + let server = running_server(Some(crate::build_info::version().as_str()), None); + + assert_eq!(restart_needed_bool(&server), Some(true)); + assert_eq!(server_binary_stale_bool(&server), Some(false)); + } +} -- cgit v1.2.3