1use std::error::Error;
8use std::fmt;
9use std::io;
10
11use crate::broadcast::PartialBroadcastFailure;
12use crate::diagnostics;
13use crate::wait::WaitTimeoutError;
14use crate::{PaneId, SessionName};
15
16const PROTOCOL_HINT: &str =
17 "check the request and daemon state, then retry after correcting the request";
18const TRANSPORT_HINT: &str = "verify the rmux daemon is running and the endpoint is reachable";
19
20#[derive(Debug)]
25#[non_exhaustive]
26pub enum RmuxError {
27 #[non_exhaustive]
32 Unsupported {
33 feature: String,
36 hint: String,
38 },
39 #[non_exhaustive]
41 Protocol {
42 source: rmux_proto::RmuxError,
44 },
45 #[non_exhaustive]
47 Transport {
48 operation: String,
50 source: io::Error,
52 },
53 #[non_exhaustive]
55 Collect {
56 source: CollectError,
58 },
59 #[non_exhaustive]
61 PartialBroadcast {
62 source: PartialBroadcastFailure,
64 },
65 #[non_exhaustive]
67 WaitTimeout {
68 source: WaitTimeoutError,
70 },
71 #[non_exhaustive]
73 PaneNotFound {
74 session_name: SessionName,
76 pane_id: PaneId,
78 },
79 #[non_exhaustive]
81 ProcessStillRunning {
82 message: String,
84 },
85 #[non_exhaustive]
87 SpawnFailed {
88 message: String,
90 },
91 #[non_exhaustive]
93 InvalidRegex {
94 pattern: String,
96 message: String,
98 },
99 #[non_exhaustive]
101 OwnedSessionLeaseLost {
102 message: String,
104 },
105}
106
107impl RmuxError {
108 #[must_use]
111 pub fn unsupported(feature: impl Into<String>, hint: impl Into<String>) -> Self {
112 Self::Unsupported {
113 feature: feature.into(),
114 hint: hint.into(),
115 }
116 }
117
118 #[must_use]
124 pub fn protocol(error: rmux_proto::RmuxError) -> Self {
125 match error {
126 rmux_proto::RmuxError::UnsupportedWireVersion {
127 got,
128 minimum,
129 maximum,
130 } => Self::unsupported(
131 diagnostics::FEATURE_PROTOCOL_WIRE_VERSION,
132 format!(
133 "upgrade the rmux daemon or use an SDK that supports wire version {got} \
134 (supported range {minimum}..={maximum})"
135 ),
136 ),
137 rmux_proto::RmuxError::UnsupportedCapability { feature, supported } => {
138 let hint = diagnostics::unsupported_capability_hint(&feature, &supported);
139 Self::unsupported(feature, hint)
140 }
141 rmux_proto::RmuxError::UnknownCommand(command) => {
142 let feature = diagnostics::command_feature_id(&command);
143 Self::unsupported(
144 feature,
145 format!(
146 "upgrade the rmux daemon or use a command advertised by the negotiated \
147 command inventory before sending `{command}`"
148 ),
149 )
150 }
151 source => map_known_protocol_error(source),
152 }
153 }
154
155 #[must_use]
157 pub fn transport(operation: impl Into<String>, source: io::Error) -> Self {
158 Self::Transport {
159 operation: operation.into(),
160 source,
161 }
162 }
163
164 #[must_use]
166 pub fn collect(source: CollectError) -> Self {
167 Self::Collect { source }
168 }
169
170 #[must_use]
172 pub fn partial_broadcast(source: PartialBroadcastFailure) -> Self {
173 Self::PartialBroadcast { source }
174 }
175
176 #[must_use]
178 pub fn wait_timeout(source: WaitTimeoutError) -> Self {
179 Self::WaitTimeout { source }
180 }
181
182 #[must_use]
184 pub fn pane_not_found(session_name: SessionName, pane_id: PaneId) -> Self {
185 Self::PaneNotFound {
186 session_name,
187 pane_id,
188 }
189 }
190
191 #[must_use]
193 pub fn invalid_regex(pattern: impl Into<String>, message: impl Into<String>) -> Self {
194 Self::InvalidRegex {
195 pattern: pattern.into(),
196 message: message.into(),
197 }
198 }
199
200 #[must_use]
206 pub fn hint(&self) -> Option<&str> {
207 match self {
208 Self::Unsupported { hint, .. } => Some(hint),
209 Self::Protocol { .. } => Some(PROTOCOL_HINT),
210 Self::Transport { .. } => Some(TRANSPORT_HINT),
211 Self::PaneNotFound { .. }
212 | Self::ProcessStillRunning { .. }
213 | Self::SpawnFailed { .. }
214 | Self::InvalidRegex { .. }
215 | Self::OwnedSessionLeaseLost { .. }
216 | Self::Collect { .. }
217 | Self::PartialBroadcast { .. }
218 | Self::WaitTimeout { .. } => None,
219 }
220 }
221
222 #[must_use]
226 pub fn feature(&self) -> Option<&str> {
227 match self {
228 Self::Unsupported { feature, .. } => Some(feature),
229 Self::Protocol { .. }
230 | Self::Transport { .. }
231 | Self::Collect { .. }
232 | Self::PartialBroadcast { .. }
233 | Self::WaitTimeout { .. }
234 | Self::PaneNotFound { .. }
235 | Self::ProcessStillRunning { .. }
236 | Self::SpawnFailed { .. }
237 | Self::InvalidRegex { .. }
238 | Self::OwnedSessionLeaseLost { .. } => None,
239 }
240 }
241}
242
243fn map_known_protocol_error(source: rmux_proto::RmuxError) -> RmuxError {
244 match source {
245 rmux_proto::RmuxError::PaneNotFound {
246 session_name,
247 pane_id,
248 } => RmuxError::PaneNotFound {
249 session_name,
250 pane_id,
251 },
252 rmux_proto::RmuxError::ProcessStillRunning => RmuxError::ProcessStillRunning {
253 message: rmux_proto::RmuxError::ProcessStillRunning.to_string(),
254 },
255 rmux_proto::RmuxError::SpawnFailed { message } => RmuxError::SpawnFailed { message },
256 rmux_proto::RmuxError::OwnedSessionLeaseLost { session_name } => {
257 RmuxError::OwnedSessionLeaseLost {
258 message: rmux_proto::RmuxError::OwnedSessionLeaseLost { session_name }.to_string(),
259 }
260 }
261 source => RmuxError::Protocol { source },
262 }
263}
264
265impl From<rmux_proto::RmuxError> for RmuxError {
266 fn from(error: rmux_proto::RmuxError) -> Self {
267 Self::protocol(error)
268 }
269}
270
271impl From<rmux_proto::ErrorResponse> for RmuxError {
272 fn from(response: rmux_proto::ErrorResponse) -> Self {
273 Self::protocol(response.error)
274 }
275}
276
277impl From<io::Error> for RmuxError {
278 fn from(error: io::Error) -> Self {
279 Self::transport("communicate with rmux daemon", error)
280 }
281}
282
283impl From<CollectError> for RmuxError {
284 fn from(error: CollectError) -> Self {
285 Self::collect(error)
286 }
287}
288
289#[derive(Debug, Default)]
295pub struct CollectError {
296 errors: Vec<RmuxError>,
297}
298
299impl CollectError {
300 #[must_use]
302 pub fn new(errors: Vec<RmuxError>) -> Self {
303 Self { errors }
304 }
305
306 #[must_use]
308 pub fn errors(&self) -> &[RmuxError] {
309 &self.errors
310 }
311
312 #[must_use]
314 pub fn len(&self) -> usize {
315 self.errors.len()
316 }
317
318 #[must_use]
320 pub fn is_empty(&self) -> bool {
321 self.errors.is_empty()
322 }
323
324 pub fn push(&mut self, error: RmuxError) {
326 self.errors.push(error);
327 }
328
329 #[must_use]
331 pub fn into_errors(self) -> Vec<RmuxError> {
332 self.errors
333 }
334}
335
336impl From<Vec<RmuxError>> for CollectError {
337 fn from(errors: Vec<RmuxError>) -> Self {
338 Self::new(errors)
339 }
340}
341
342impl FromIterator<RmuxError> for CollectError {
343 fn from_iter<T: IntoIterator<Item = RmuxError>>(iter: T) -> Self {
344 Self::new(iter.into_iter().collect())
345 }
346}
347
348impl Extend<RmuxError> for CollectError {
349 fn extend<T: IntoIterator<Item = RmuxError>>(&mut self, iter: T) {
350 self.errors.extend(iter);
351 }
352}
353
354impl fmt::Display for CollectError {
355 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
356 match self.errors.as_slice() {
357 [] => write!(formatter, "no SDK diagnostics were collected"),
358 [error] => {
359 writeln!(formatter, "1 SDK diagnostic collected:")?;
360 write_numbered_error(formatter, 1, error)
361 }
362 errors => {
363 writeln!(formatter, "{} SDK diagnostics collected:", errors.len())?;
364 for (index, error) in errors.iter().enumerate() {
365 if index > 0 {
366 writeln!(formatter)?;
367 }
368 write_numbered_error(formatter, index + 1, error)?;
369 }
370 Ok(())
371 }
372 }
373 }
374}
375
376impl Error for CollectError {}
377
378impl fmt::Display for RmuxError {
379 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
380 match self {
381 Self::Unsupported { feature, hint } => {
382 write!(formatter, "unsupported feature `{feature}`\nhint: {hint}")
383 }
384 Self::Protocol { source } => {
385 write!(
386 formatter,
387 "rmux protocol error: {source}\nhint: {PROTOCOL_HINT}"
388 )
389 }
390 Self::Transport { operation, source } => {
391 write!(
392 formatter,
393 "rmux transport error while {operation}: {source}\nhint: {TRANSPORT_HINT}"
394 )
395 }
396 Self::Collect { source } => source.fmt(formatter),
397 Self::PartialBroadcast { source } => source.fmt(formatter),
398 Self::WaitTimeout { source } => source.fmt(formatter),
399 Self::PaneNotFound {
400 session_name,
401 pane_id,
402 } => write!(
403 formatter,
404 "pane id {} was not found in session {session_name}",
405 pane_id
406 ),
407 Self::ProcessStillRunning { message } => {
408 write!(formatter, "pane process still running: {message}")
409 }
410 Self::SpawnFailed { message } => write!(formatter, "pane spawn failed: {message}"),
411 Self::InvalidRegex { pattern, message } => {
412 write!(formatter, "invalid regex `{pattern}`: {message}")
413 }
414 Self::OwnedSessionLeaseLost { message } => {
415 write!(formatter, "owned session lease lost: {message}")
416 }
417 }
418 }
419}
420
421impl Error for RmuxError {
422 fn source(&self) -> Option<&(dyn Error + 'static)> {
423 match self {
424 Self::Unsupported { .. } => None,
425 Self::Protocol { source } => Some(source),
426 Self::Transport { source, .. } => Some(source),
427 Self::Collect { source } => Some(source),
428 Self::PartialBroadcast { source } => Some(source),
429 Self::WaitTimeout { source } => Some(source),
430 Self::PaneNotFound { .. }
431 | Self::ProcessStillRunning { .. }
432 | Self::SpawnFailed { .. }
433 | Self::InvalidRegex { .. }
434 | Self::OwnedSessionLeaseLost { .. } => None,
435 }
436 }
437}
438
439pub type Result<T> = core::result::Result<T, RmuxError>;
441
442trait NonCloneGuard {}
443
444impl<T: Clone> NonCloneGuard for T {}
445impl NonCloneGuard for RmuxError {}
446impl NonCloneGuard for CollectError {}
447
448const _: fn() = sdk_errors_remain_non_clone;
449
450#[allow(dead_code)]
451fn sdk_errors_remain_non_clone() {
452 fn assert_non_clone_guard<T: NonCloneGuard>() {}
453
454 assert_non_clone_guard::<RmuxError>();
455 assert_non_clone_guard::<CollectError>();
456}
457
458fn write_numbered_error(
459 formatter: &mut fmt::Formatter<'_>,
460 index: usize,
461 error: &RmuxError,
462) -> fmt::Result {
463 let rendered = error.to_string();
464 let mut lines = rendered.lines();
465
466 let Some(first) = lines.next() else {
467 return write!(formatter, "{index}. <empty SDK diagnostic>");
468 };
469
470 write!(formatter, "{index}. {first}")?;
471 for line in lines {
472 write!(formatter, "\n {line}")?;
473 }
474 Ok(())
475}