use std::error::Error;
use std::fmt;
use std::io;
use crate::broadcast::PartialBroadcastFailure;
use crate::diagnostics;
use crate::wait::WaitTimeoutError;
use crate::{PaneId, SessionName};
const PROTOCOL_HINT: &str =
"check the request and daemon state, then retry after correcting the request";
const TRANSPORT_HINT: &str = "verify the rmux daemon is running and the endpoint is reachable";
#[derive(Debug)]
#[non_exhaustive]
pub enum RmuxError {
#[non_exhaustive]
Unsupported {
feature: String,
hint: String,
},
#[non_exhaustive]
Protocol {
source: rmux_proto::RmuxError,
},
#[non_exhaustive]
Transport {
operation: String,
source: io::Error,
},
#[non_exhaustive]
Collect {
source: CollectError,
},
#[non_exhaustive]
PartialBroadcast {
source: PartialBroadcastFailure,
},
#[non_exhaustive]
WaitTimeout {
source: WaitTimeoutError,
},
#[non_exhaustive]
PaneNotFound {
session_name: SessionName,
pane_id: PaneId,
},
#[non_exhaustive]
ProcessStillRunning {
message: String,
},
#[non_exhaustive]
SpawnFailed {
message: String,
},
#[non_exhaustive]
InvalidRegex {
pattern: String,
message: String,
},
#[non_exhaustive]
OwnedSessionLeaseLost {
message: String,
},
}
impl RmuxError {
#[must_use]
pub fn unsupported(feature: impl Into<String>, hint: impl Into<String>) -> Self {
Self::Unsupported {
feature: feature.into(),
hint: hint.into(),
}
}
#[must_use]
pub fn protocol(error: rmux_proto::RmuxError) -> Self {
match error {
rmux_proto::RmuxError::UnsupportedWireVersion {
got,
minimum,
maximum,
} => Self::unsupported(
diagnostics::FEATURE_PROTOCOL_WIRE_VERSION,
format!(
"upgrade the rmux daemon or use an SDK that supports wire version {got} \
(supported range {minimum}..={maximum})"
),
),
rmux_proto::RmuxError::UnsupportedCapability { feature, supported } => {
let hint = diagnostics::unsupported_capability_hint(&feature, &supported);
Self::unsupported(feature, hint)
}
rmux_proto::RmuxError::UnknownCommand(command) => {
let feature = diagnostics::command_feature_id(&command);
Self::unsupported(
feature,
format!(
"upgrade the rmux daemon or use a command advertised by the negotiated \
command inventory before sending `{command}`"
),
)
}
source => map_known_protocol_error(source),
}
}
#[must_use]
pub fn transport(operation: impl Into<String>, source: io::Error) -> Self {
Self::Transport {
operation: operation.into(),
source,
}
}
#[must_use]
pub fn collect(source: CollectError) -> Self {
Self::Collect { source }
}
#[must_use]
pub fn partial_broadcast(source: PartialBroadcastFailure) -> Self {
Self::PartialBroadcast { source }
}
#[must_use]
pub fn wait_timeout(source: WaitTimeoutError) -> Self {
Self::WaitTimeout { source }
}
#[must_use]
pub fn pane_not_found(session_name: SessionName, pane_id: PaneId) -> Self {
Self::PaneNotFound {
session_name,
pane_id,
}
}
#[must_use]
pub fn invalid_regex(pattern: impl Into<String>, message: impl Into<String>) -> Self {
Self::InvalidRegex {
pattern: pattern.into(),
message: message.into(),
}
}
#[must_use]
pub fn hint(&self) -> Option<&str> {
match self {
Self::Unsupported { hint, .. } => Some(hint),
Self::Protocol { .. } => Some(PROTOCOL_HINT),
Self::Transport { .. } => Some(TRANSPORT_HINT),
Self::PaneNotFound { .. }
| Self::ProcessStillRunning { .. }
| Self::SpawnFailed { .. }
| Self::InvalidRegex { .. }
| Self::OwnedSessionLeaseLost { .. }
| Self::Collect { .. }
| Self::PartialBroadcast { .. }
| Self::WaitTimeout { .. } => None,
}
}
#[must_use]
pub fn feature(&self) -> Option<&str> {
match self {
Self::Unsupported { feature, .. } => Some(feature),
Self::Protocol { .. }
| Self::Transport { .. }
| Self::Collect { .. }
| Self::PartialBroadcast { .. }
| Self::WaitTimeout { .. }
| Self::PaneNotFound { .. }
| Self::ProcessStillRunning { .. }
| Self::SpawnFailed { .. }
| Self::InvalidRegex { .. }
| Self::OwnedSessionLeaseLost { .. } => None,
}
}
}
fn map_known_protocol_error(source: rmux_proto::RmuxError) -> RmuxError {
match source {
rmux_proto::RmuxError::PaneNotFound {
session_name,
pane_id,
} => RmuxError::PaneNotFound {
session_name,
pane_id,
},
rmux_proto::RmuxError::ProcessStillRunning => RmuxError::ProcessStillRunning {
message: rmux_proto::RmuxError::ProcessStillRunning.to_string(),
},
rmux_proto::RmuxError::SpawnFailed { message } => RmuxError::SpawnFailed { message },
rmux_proto::RmuxError::OwnedSessionLeaseLost { session_name } => {
RmuxError::OwnedSessionLeaseLost {
message: rmux_proto::RmuxError::OwnedSessionLeaseLost { session_name }.to_string(),
}
}
source => RmuxError::Protocol { source },
}
}
impl From<rmux_proto::RmuxError> for RmuxError {
fn from(error: rmux_proto::RmuxError) -> Self {
Self::protocol(error)
}
}
impl From<rmux_proto::ErrorResponse> for RmuxError {
fn from(response: rmux_proto::ErrorResponse) -> Self {
Self::protocol(response.error)
}
}
impl From<io::Error> for RmuxError {
fn from(error: io::Error) -> Self {
Self::transport("communicate with rmux daemon", error)
}
}
impl From<CollectError> for RmuxError {
fn from(error: CollectError) -> Self {
Self::collect(error)
}
}
#[derive(Debug, Default)]
pub struct CollectError {
errors: Vec<RmuxError>,
}
impl CollectError {
#[must_use]
pub fn new(errors: Vec<RmuxError>) -> Self {
Self { errors }
}
#[must_use]
pub fn errors(&self) -> &[RmuxError] {
&self.errors
}
#[must_use]
pub fn len(&self) -> usize {
self.errors.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
pub fn push(&mut self, error: RmuxError) {
self.errors.push(error);
}
#[must_use]
pub fn into_errors(self) -> Vec<RmuxError> {
self.errors
}
}
impl From<Vec<RmuxError>> for CollectError {
fn from(errors: Vec<RmuxError>) -> Self {
Self::new(errors)
}
}
impl FromIterator<RmuxError> for CollectError {
fn from_iter<T: IntoIterator<Item = RmuxError>>(iter: T) -> Self {
Self::new(iter.into_iter().collect())
}
}
impl Extend<RmuxError> for CollectError {
fn extend<T: IntoIterator<Item = RmuxError>>(&mut self, iter: T) {
self.errors.extend(iter);
}
}
impl fmt::Display for CollectError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.errors.as_slice() {
[] => write!(formatter, "no SDK diagnostics were collected"),
[error] => {
writeln!(formatter, "1 SDK diagnostic collected:")?;
write_numbered_error(formatter, 1, error)
}
errors => {
writeln!(formatter, "{} SDK diagnostics collected:", errors.len())?;
for (index, error) in errors.iter().enumerate() {
if index > 0 {
writeln!(formatter)?;
}
write_numbered_error(formatter, index + 1, error)?;
}
Ok(())
}
}
}
}
impl Error for CollectError {}
impl fmt::Display for RmuxError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unsupported { feature, hint } => {
write!(formatter, "unsupported feature `{feature}`\nhint: {hint}")
}
Self::Protocol { source } => {
write!(
formatter,
"rmux protocol error: {source}\nhint: {PROTOCOL_HINT}"
)
}
Self::Transport { operation, source } => {
write!(
formatter,
"rmux transport error while {operation}: {source}\nhint: {TRANSPORT_HINT}"
)
}
Self::Collect { source } => source.fmt(formatter),
Self::PartialBroadcast { source } => source.fmt(formatter),
Self::WaitTimeout { source } => source.fmt(formatter),
Self::PaneNotFound {
session_name,
pane_id,
} => write!(
formatter,
"pane id {} was not found in session {session_name}",
pane_id
),
Self::ProcessStillRunning { message } => {
write!(formatter, "pane process still running: {message}")
}
Self::SpawnFailed { message } => write!(formatter, "pane spawn failed: {message}"),
Self::InvalidRegex { pattern, message } => {
write!(formatter, "invalid regex `{pattern}`: {message}")
}
Self::OwnedSessionLeaseLost { message } => {
write!(formatter, "owned session lease lost: {message}")
}
}
}
}
impl Error for RmuxError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Unsupported { .. } => None,
Self::Protocol { source } => Some(source),
Self::Transport { source, .. } => Some(source),
Self::Collect { source } => Some(source),
Self::PartialBroadcast { source } => Some(source),
Self::WaitTimeout { source } => Some(source),
Self::PaneNotFound { .. }
| Self::ProcessStillRunning { .. }
| Self::SpawnFailed { .. }
| Self::InvalidRegex { .. }
| Self::OwnedSessionLeaseLost { .. } => None,
}
}
}
pub type Result<T> = core::result::Result<T, RmuxError>;
trait NonCloneGuard {}
impl<T: Clone> NonCloneGuard for T {}
impl NonCloneGuard for RmuxError {}
impl NonCloneGuard for CollectError {}
const _: fn() = sdk_errors_remain_non_clone;
#[allow(dead_code)]
fn sdk_errors_remain_non_clone() {
fn assert_non_clone_guard<T: NonCloneGuard>() {}
assert_non_clone_guard::<RmuxError>();
assert_non_clone_guard::<CollectError>();
}
fn write_numbered_error(
formatter: &mut fmt::Formatter<'_>,
index: usize,
error: &RmuxError,
) -> fmt::Result {
let rendered = error.to_string();
let mut lines = rendered.lines();
let Some(first) = lines.next() else {
return write!(formatter, "{index}. <empty SDK diagnostic>");
};
write!(formatter, "{index}. {first}")?;
for line in lines {
write!(formatter, "\n {line}")?;
}
Ok(())
}