use serde::Serialize; use crate::api; use crate::api::client::ApiClientError; pub(super) fn run_status_command(args: &[String]) -> std::io::Result { let Some((scope, json)) = parse_status_args(args) else { return Ok(2); }; match scope { StatusScope::Full => print_full_status(json), StatusScope::Server => print_server_status(json), StatusScope::Client => { print_client_status(json)?; Ok(0) } StatusScope::Help => { print_status_help(); Ok(0) } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum StatusScope { Full, Server, Client, Help, } fn parse_status_args(args: &[String]) -> Option<(StatusScope, bool)> { match args.first().map(|arg| arg.as_str()) { None => Some((StatusScope::Full, false)), Some("--json") if args.len() == 1 => Some((StatusScope::Full, true)), Some("server") => { parse_status_scope_args(args, StatusScope::Server, "herdr status server [--json]") } Some("client") => { parse_status_scope_args(args, StatusScope::Client, "herdr status client [--json]") } Some("help" | "--help" | "-h") => { if args.len() > 1 { print_status_help(); return None; } Some((StatusScope::Help, false)) } Some(_) => { print_status_help(); None } } } fn parse_status_scope_args( args: &[String], scope: StatusScope, usage: &str, ) -> Option<(StatusScope, bool)> { match args.get(1).map(|arg| arg.as_str()) { None => Some((scope, false)), Some("--json") if args.len() == 2 => Some((scope, true)), _ => { eprintln!("usage: {usage}"); None } } } #[derive(Debug, Clone, PartialEq, Eq)] enum ServerRuntimeStatus { Running { version: Option, protocol: Option, capabilities: Option, }, NotRunning, } fn print_full_status(json: bool) -> std::io::Result { let server = read_server_runtime_status()?; if json { print_json(&FullStatusJson { client: client_status_json(), server: server_status_json(&server), update: update_status_json(&server), })?; return Ok(0); } println!("client:"); println!(" version: {}", crate::build_info::version()); println!( " channel: {}", 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) } fn print_server_status(json: bool) -> std::io::Result { let server = read_server_runtime_status()?; if json { print_json(&server_status_json(&server))?; return Ok(0); } print_server_status_body(&server, ""); Ok(0) } fn print_client_status(json: bool) -> std::io::Result<()> { if json { print_json(&client_status_json())?; return Ok(()); } println!("version: {}", crate::build_info::version()); println!( "channel: {}", 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(()) } fn print_server_status_body(server: &ServerRuntimeStatus, indent: &str) { match server { ServerRuntimeStatus::Running { version, protocol, capabilities, } => { println!("{indent}status: running"); println!("{indent}version: {}", option_label(version.as_deref())); 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: {}", super::target::socket_label()); } ServerRuntimeStatus::NotRunning => { println!("{indent}status: not running"); println!("{indent}socket: {}", super::target::socket_label()); } } } fn read_server_runtime_status() -> std::io::Result { match super::target::server_status(&super::target::api_client()?) { Ok(status) => Ok(ServerRuntimeStatus::Running { version: status.version, protocol: status.protocol, capabilities: status.capabilities, }), Err(err) if super::target::is_remote() => Err(super::target::remote_error( super::api_client_error_to_io(err), )), Err(ApiClientError::Io(err)) if super::server_not_running_error(&err) => { Ok(ServerRuntimeStatus::NotRunning) } Err(err) => Err(super::api_client_error_to_io(err)), } } fn option_label(value: Option<&str>) -> &str { value.unwrap_or("unknown") } fn protocol_label(protocol: Option) -> String { protocol .map(|value| value.to_string()) .unwrap_or_else(|| "unknown".to_string()) } fn compatibility_label(protocol: Option) -> &'static str { match protocol { Some(protocol) if protocol == crate::protocol::PROTOCOL_VERSION => "yes", Some(_) => "no", None => "unknown", } } 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 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", } } #[derive(Serialize)] struct FullStatusJson { client: ClientStatusJson, server: ServerStatusJson, update: UpdateStatusJson, } #[derive(Serialize)] struct ClientStatusJson { version: String, channel: &'static str, protocol: u32, endpoint_protocol_generation: u32, endpoint_capabilities: Vec<&'static str>, remote_host_bridge: bool, remote_bridge_idle_timeout: bool, binary: String, session: Option, } #[derive(Serialize)] struct ServerStatusJson { status: &'static str, running: bool, version: Option, 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, surface_interest: bool, health_check: bool, } #[derive(Serialize)] struct UpdateStatusJson { restart_needed: Option, server_binary_stale: Option, } fn client_status_json() -> ClientStatusJson { 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, endpoint_capabilities: vec![ crate::protocol::endpoint::SURFACE_INTEREST_CAPABILITY, crate::protocol::endpoint::PRESENTATION_EFFECTS_FENCE_CAPABILITY, crate::protocol::endpoint::HEALTH_CHECK_CAPABILITY, ], remote_host_bridge: true, remote_bridge_idle_timeout: crate::platform::REMOTE_BRIDGE_IDLE_TIMEOUT_SUPPORTED, binary: current_exe_label(), session: crate::session::active_name(), } } fn server_status_json(server: &ServerRuntimeStatus) -> ServerStatusJson { let mut status = match server { ServerRuntimeStatus::Running { version, protocol, capabilities, } => ServerStatusJson { status: "running", running: true, version: version.clone(), protocol: *protocol, capabilities: capabilities .as_ref() .map(|capabilities| ServerCapabilitiesJson { live_handoff: capabilities.live_handoff, detached_server_daemon: capabilities.detached_server_daemon, endpoint_protocol_generation: capabilities.endpoint_protocol_generation, surface_interest: capabilities.surface_interest, health_check: capabilities.health_check, }), 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", running: false, version: None, 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), }, }; if let Some((_, session)) = super::target::remote_identity() { status.socket = super::target::socket_label(); status.session = Some(session); status.server_binary_stale = None; } status } 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 { 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), } } fn print_json(value: &impl Serialize) -> std::io::Result<()> { println!("{}", serde_json::to_string(value)?); Ok(()) } fn current_exe_label() -> String { std::env::current_exe() .map(|path| path.display().to_string()) .unwrap_or_else(|err| format!("unknown ({err})")) } fn print_status_help() { eprintln!("herdr status commands:"); eprintln!(" herdr status [--json] show local client and running server status"); 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, surface_interest: true, health_check: true, }), } } #[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)); } }