use thiserror::Error;
#[derive(Error, Debug)]
pub enum ShellTunnelError {
#[error("session not found: {0}")]
SessionNotFound(String),
#[error("session already exists: {0}")]
SessionExists(String),
#[error("invalid state transition from {from:?} to {to:?}")]
InvalidStateTransition {
from: crate::session::SessionState,
to: crate::session::SessionState,
},
#[error("PTY error: {0}")]
Pty(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("command execution timeout")]
Timeout,
#[error("session terminated")]
SessionTerminated,
#[error("internal lock poisoned")]
LockPoisoned,
#[error("channel send error: {0}")]
ChannelSend(String),
#[error("channel closed")]
ChannelClosed,
#[error("command execution failed: {0}")]
ExecutionFailed(String),
#[error("output parse error: {0}")]
ParseError(String),
#[error("session not executable: current state is {0:?}")]
NotExecutable(crate::session::SessionState),
#[cfg(feature = "tls")]
#[error("tls error: {0}")]
Tls(String),
#[error("tunnel error: {0}")]
Tunnel(String),
#[cfg(feature = "self-update")]
#[error("update error: {0}")]
Update(String),
}
pub type Result<T> = std::result::Result<T, ShellTunnelError>;
pub fn explain_bind_failure(what: &str, addr: &str, error: &std::io::Error) -> String {
let mut message = match error.kind() {
std::io::ErrorKind::AddrInUse => {
format!("cannot start the {what}: {addr} is already in use by another program.")
}
std::io::ErrorKind::PermissionDenied => {
format!("cannot start the {what}: not allowed to bind {addr}.")
}
std::io::ErrorKind::AddrNotAvailable => {
format!("cannot start the {what}: {addr} is not an address on this machine.")
}
_ => format!("cannot start the {what}: {addr} could not be bound."),
};
message.push_str(&format!("\n {error}"));
if error.kind() == std::io::ErrorKind::AddrInUse {
message.push_str("\n Choose another port with -p, or stop whatever holds this one.");
if cfg!(windows) {
message.push_str(
"\n To find it: Get-NetTCPConnection -LocalPort <port> | Select OwningProcess",
);
} else {
message.push_str("\n To find it: ss -ltnp | grep :<port>");
}
}
message
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_session_not_found_display() {
let err = ShellTunnelError::SessionNotFound("sess-00000001".into());
assert!(err.to_string().contains("sess-00000001"));
assert!(err.to_string().contains("not found"));
}
#[test]
fn test_session_exists_display() {
let err = ShellTunnelError::SessionExists("sess-00000002".into());
assert!(err.to_string().contains("sess-00000002"));
assert!(err.to_string().contains("already exists"));
}
#[test]
fn test_io_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let shell_err: ShellTunnelError = io_err.into();
assert!(matches!(shell_err, ShellTunnelError::Io(_)));
assert!(shell_err.to_string().contains("I/O error"));
}
#[test]
fn test_timeout_display() {
let err = ShellTunnelError::Timeout;
assert!(err.to_string().contains("timeout"));
}
#[test]
fn test_pty_error_display() {
let err = ShellTunnelError::Pty("failed to spawn".into());
assert!(err.to_string().contains("PTY error"));
assert!(err.to_string().contains("failed to spawn"));
}
}