aboutsummaryrefslogtreecommitdiff
path: root/src/cli
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-02 02:45:20 +0300
committerGitHub <noreply@github.com>2026-09-02 02:45:20 +0300
commitcc88b3b8e5bb9f7d9f23ed6ae85a52fd7b5b9ed6 (patch)
tree4d37c760099070f1a1488b61c7b7271875aafef8 /src/cli
parent18e69891dca486d669a584facd80644bb51f54a2 (diff)
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
Diffstat (limited to 'src/cli')
-rw-r--r--src/cli/status.rs135
1 files changed, 120 insertions, 15 deletions
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<i32> {
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<u32>) -> &'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<String>,
}
@@ -226,20 +267,24 @@ struct ServerStatusJson {
protocol: Option<u32>,
capabilities: Option<ServerCapabilitiesJson>,
compatible: Option<bool>,
+ endpoint_compatible: Option<bool>,
socket: String,
session: Option<String>,
restart_needed: Option<bool>,
+ server_binary_stale: Option<bool>,
}
#[derive(Serialize)]
struct ServerCapabilitiesJson {
live_handoff: bool,
detached_server_daemon: bool,
+ endpoint_protocol_generation: Option<u32>,
}
#[derive(Serialize)]
struct UpdateStatusJson {
restart_needed: Option<bool>,
+ server_binary_stale: Option<bool>,
}
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<bool> {
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<bool> {
+ 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<u32>,
+ ) -> 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));
+ }
+}