diff options
Diffstat (limited to 'src/cli.rs')
| -rw-r--r-- | src/cli.rs | 93 |
1 files changed, 89 insertions, 4 deletions
@@ -18,6 +18,7 @@ mod plugin; mod protocol_guard; mod runtime; mod server; +mod server_not_running; mod spec; mod status; mod tab; @@ -745,17 +746,20 @@ pub(super) fn send_request(request: &Request) -> std::io::Result<serde_json::Val ensure_server_protocol_compatible(&client, &request.id)?; client .request_value(request) - .map_err(api_client_error_to_io) + .map_err(|err| map_server_not_running_or_io(err, &request.id, &client)) } pub(super) fn send_request_unchecked(request: &Request) -> std::io::Result<serde_json::Value> { - ApiClient::local() + let client = ApiClient::local(); + client .request_value(request) - .map_err(api_client_error_to_io) + .map_err(|err| map_server_not_running_or_io(err, &request.id, &client)) } fn ensure_server_protocol_compatible(client: &ApiClient, request_id: &str) -> std::io::Result<()> { - let status = client.status().map_err(api_client_error_to_io)?; + let status = client + .status() + .map_err(|err| map_server_not_running_or_io(err, request_id, client))?; let server_protocol = status .protocol .ok_or_else(|| std::io::Error::other("server ping did not include a protocol version"))?; @@ -778,6 +782,49 @@ pub(crate) fn protocol_mismatch_was_reported(err: &std::io::Error) -> bool { protocol_guard::was_reported(err) } +pub(crate) fn server_not_running_was_reported(err: &std::io::Error) -> bool { + server_not_running::was_reported(err) +} + +/// Returns the `ErrorResponse` carried by a `server_not_running` marker, if any, +/// so the edge that surfaces the error can print it exactly once (deferred +/// printing: recovering callers like plugin offline fallback print nothing). +pub(crate) fn server_not_running_reported_response( + err: &std::io::Error, +) -> Option<&crate::api::schema::ErrorResponse> { + server_not_running::reported_response(err) +} + +/// True when an io::Error indicates nothing is listening on the API socket. +/// Classify by `ErrorKind` only: Windows named pipes surface different raw +/// errno values than Unix domain sockets but the same error kinds. +pub(super) fn server_not_running_error(err: &std::io::Error) -> bool { + matches!( + err.kind(), + std::io::ErrorKind::NotFound | std::io::ErrorKind::ConnectionRefused + ) +} + +/// Maps an `ApiClientError` from a socket command into the io::Error that +/// bubbles up to `main`. A dead-server connect failure is reported as a +/// friendly `server_not_running` JSON error plus a recognizable marker; all +/// other errors fall through unchanged so existing handling is preserved. +fn map_server_not_running_or_io( + err: ApiClientError, + request_id: &str, + client: &ApiClient, +) -> std::io::Error { + match err { + ApiClientError::Io(io_err) if server_not_running_error(&io_err) => { + server_not_running::reported_error(server_not_running::response( + request_id, + &client.socket_path(), + )) + } + err => api_client_error_to_io(err), + } +} + fn api_client_error_to_io(err: ApiClientError) -> std::io::Error { match err { ApiClientError::Io(err) => err, @@ -1036,4 +1083,42 @@ mod tests { "env must use KEY=VALUE" ); } + + #[test] + fn maps_dead_server_connect_failure_to_friendly_error() { + use crate::api::client::{ApiClient, ApiClientError}; + + let client = ApiClient::local(); + let socket = client.socket_path().display().to_string(); + + // The helper does NOT print; it returns a recognizable marker carrying + // the ErrorResponse so the surfacing edge can print it exactly once. + let mapped = super::map_server_not_running_or_io( + ApiClientError::Io(std::io::Error::from(std::io::ErrorKind::NotFound)), + "cli:workspace:create", + &client, + ); + + let response = super::server_not_running::reported_response(&mapped) + .expect("dead-server connect failure should carry a server_not_running response"); + assert_eq!(response.id, "cli:workspace:create"); + assert_eq!(response.error.code, "server_not_running"); + assert!(response.error.message.contains(&socket)); + + // The mapping is recognizable without string matching. + assert!(super::server_not_running::was_reported(&mapped)); + } + + #[test] + fn classifier_ignores_unrelated_io_kinds() { + use crate::api::client::{ApiClient, ApiClientError}; + + let client = ApiClient::local(); + let mapped = super::map_server_not_running_or_io( + ApiClientError::Io(std::io::Error::from(std::io::ErrorKind::TimedOut)), + "cli:workspace:create", + &client, + ); + assert!(!super::server_not_running::was_reported(&mapped)); + } } |
