diff options
Diffstat (limited to 'src/cli')
| -rw-r--r-- | src/cli/agent.rs | 7 | ||||
| -rw-r--r-- | src/cli/plugin.rs | 21 | ||||
| -rw-r--r-- | src/cli/server_not_running.rs | 74 | ||||
| -rw-r--r-- | src/cli/status.rs | 9 |
4 files changed, 95 insertions, 16 deletions
diff --git a/src/cli/agent.rs b/src/cli/agent.rs index e48d6b3b..e1271f48 100644 --- a/src/cli/agent.rs +++ b/src/cli/agent.rs @@ -577,6 +577,13 @@ fn print_agent_transport_error( if super::protocol_mismatch_was_reported(&err) { return Ok(1); } + // A dead-server marker reaches here from `send_request` in the agent + // startup path; surface its deferred response exactly once instead of + // printing a second, generic transport-error line. + if let Some(response) = super::server_not_running_reported_response(&err) { + let value = serde_json::to_value(response).map_err(std::io::Error::other)?; + return super::print_response(&value); + } super::print_response(&cli_agent_error(request_id, code, err.to_string())) } diff --git a/src/cli/plugin.rs b/src/cli/plugin.rs index b4e6d514..efdb9eca 100644 --- a/src/cli/plugin.rs +++ b/src/cli/plugin.rs @@ -1618,14 +1618,19 @@ fn current_unix_ms() -> u64 { } fn is_connection_error(err: &std::io::Error) -> bool { - matches!( - err.kind(), - std::io::ErrorKind::NotFound - | std::io::ErrorKind::ConnectionRefused - | std::io::ErrorKind::ConnectionAborted - | std::io::ErrorKind::ConnectionReset - | std::io::ErrorKind::BrokenPipe - ) + // A `server_not_running` marker is a connect failure for recovery purposes: + // treating it as a connection error lets plugin commands fall back to the + // offline registry. The marker carries (but does not print) a friendly + // response, so recovering here prints nothing. + super::server_not_running_was_reported(err) + || matches!( + err.kind(), + std::io::ErrorKind::NotFound + | std::io::ErrorKind::ConnectionRefused + | std::io::ErrorKind::ConnectionAborted + | std::io::ErrorKind::ConnectionReset + | std::io::ErrorKind::BrokenPipe + ) } fn print_plugin_response(method: Method) -> std::io::Result<i32> { diff --git a/src/cli/server_not_running.rs b/src/cli/server_not_running.rs new file mode 100644 index 00000000..9e974f3c --- /dev/null +++ b/src/cli/server_not_running.rs @@ -0,0 +1,74 @@ +use std::fmt; +use std::path::Path; + +use crate::api::schema::{ErrorBody, ErrorResponse}; + +/// Marker error signalling a dead API socket. Carries the `ErrorResponse` that +/// should be printed at the edge that finally surfaces the error, so callers +/// that recover (e.g. plugin offline fallback) print nothing. Mirrors +/// `ProtocolMismatchReported`, except printing is deferred because several CLI +/// commands recover from a dead server instead of reporting it. +#[derive(Debug)] +pub(super) struct ServerNotRunningReported { + pub(super) response: ErrorResponse, +} + +impl fmt::Display for ServerNotRunningReported { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Delegate to the carried message so pre-existing paths that + // stringify transport errors still show the actionable text. + f.write_str(&self.response.error.message) + } +} + +impl std::error::Error for ServerNotRunningReported {} + +/// Builds the friendly `server_not_running` ErrorResponse shown when no +/// server is listening on the resolved API socket. +pub(super) fn response(request_id: &str, socket_path: &Path) -> ErrorResponse { + let attach_command = startup_command(socket_path); + ErrorResponse { + id: request_id.to_string(), + error: ErrorBody { + code: "server_not_running".into(), + message: format!( + "no herdr server is running at {}; run `{attach_command}` to start or attach it", + socket_path.display() + ), + }, + } +} + +fn startup_command(socket_path: &Path) -> String { + let session_socket = + crate::session::api_socket_path_for(crate::session::active_name().as_deref()); + if socket_path == session_socket { + crate::session::local_attach_command() + } else { + // A socket override wins over an inherited HERDR_SESSION. Keep the + // command in the current environment so it starts the overridden + // target instead of directing the user to an unrelated session. + "herdr".to_string() + } +} + +/// Wraps the response in the recognizable marker WITHOUT printing. The caller +/// that ultimately surfaces the error prints the carried response (see +/// `reported_response`); recovering callers simply drop it. +pub(super) fn reported_error(response: ErrorResponse) -> std::io::Error { + std::io::Error::other(ServerNotRunningReported { response }) +} + +pub(super) fn was_reported(err: &std::io::Error) -> bool { + err.get_ref() + .and_then(|source| source.downcast_ref::<ServerNotRunningReported>()) + .is_some() +} + +/// Returns the `ErrorResponse` carried by a `server_not_running` marker, if any, +/// so the surfacing edge can print it exactly once. +pub(super) fn reported_response(err: &std::io::Error) -> Option<&ErrorResponse> { + err.get_ref() + .and_then(|source| source.downcast_ref::<ServerNotRunningReported>()) + .map(|reported| &reported.response) +} diff --git a/src/cli/status.rs b/src/cli/status.rs index 49b3c129..74bb3e2a 100644 --- a/src/cli/status.rs +++ b/src/cli/status.rs @@ -159,20 +159,13 @@ fn read_server_runtime_status() -> std::io::Result<ServerRuntimeStatus> { protocol: status.protocol, capabilities: status.capabilities, }), - Err(ApiClientError::Io(err)) if server_not_running_error(&err) => { + Err(ApiClientError::Io(err)) if super::server_not_running_error(&err) => { Ok(ServerRuntimeStatus::NotRunning) } Err(err) => Err(api_client_error_to_io(err)), } } -fn server_not_running_error(err: &std::io::Error) -> bool { - matches!( - err.kind(), - std::io::ErrorKind::NotFound | std::io::ErrorKind::ConnectionRefused - ) -} - fn api_client_error_to_io(err: ApiClientError) -> std::io::Error { match err { ApiClientError::Io(err) => err, |
