aboutsummaryrefslogtreecommitdiff
path: root/src/cli.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/cli.rs')
-rw-r--r--src/cli.rs93
1 files changed, 89 insertions, 4 deletions
diff --git a/src/cli.rs b/src/cli.rs
index 9d7389b5..4d3c67c0 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -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));
+ }
}