Skip to main content

shell_tunnel/
error.rs

1//! Error types for shell-tunnel.
2
3use thiserror::Error;
4
5/// Main error type for shell-tunnel operations.
6#[derive(Error, Debug)]
7pub enum ShellTunnelError {
8    /// Session with the given ID was not found.
9    #[error("session not found: {0}")]
10    SessionNotFound(String),
11
12    /// Session with the given ID already exists.
13    #[error("session already exists: {0}")]
14    SessionExists(String),
15
16    /// Invalid state transition attempted.
17    #[error("invalid state transition from {from:?} to {to:?}")]
18    InvalidStateTransition {
19        from: crate::session::SessionState,
20        to: crate::session::SessionState,
21    },
22
23    /// PTY-related error.
24    #[error("PTY error: {0}")]
25    Pty(String),
26
27    /// I/O error.
28    #[error("I/O error: {0}")]
29    Io(#[from] std::io::Error),
30
31    /// Command execution timeout.
32    #[error("command execution timeout")]
33    Timeout,
34
35    /// Session has been terminated.
36    #[error("session terminated")]
37    SessionTerminated,
38
39    /// Internal lock was poisoned.
40    #[error("internal lock poisoned")]
41    LockPoisoned,
42
43    /// Channel send error.
44    #[error("channel send error: {0}")]
45    ChannelSend(String),
46
47    /// Channel receive error.
48    #[error("channel closed")]
49    ChannelClosed,
50
51    /// Command execution failed.
52    #[error("command execution failed: {0}")]
53    ExecutionFailed(String),
54
55    /// Output parsing error.
56    #[error("output parse error: {0}")]
57    ParseError(String),
58
59    /// Session is not in executable state.
60    #[error("session not executable: current state is {0:?}")]
61    NotExecutable(crate::session::SessionState),
62
63    /// TLS configuration error.
64    #[cfg(feature = "tls")]
65    #[error("tls error: {0}")]
66    Tls(String),
67
68    /// Tunnel (reachability) error.
69    #[error("tunnel error: {0}")]
70    Tunnel(String),
71
72    /// Update error.
73    #[cfg(feature = "self-update")]
74    #[error("update error: {0}")]
75    Update(String),
76}
77
78/// Convenience Result type for shell-tunnel operations.
79pub type Result<T> = std::result::Result<T, ShellTunnelError>;
80
81/// Report a failure to take a listening socket, in the words this program uses
82/// everywhere else.
83///
84/// Without this the gateway and the relay both ended a failed startup with the
85/// `Debug` form of an `io::Error` — `Error: Io(Os { code: 10048, kind:
86/// AddrInUse, ... })` — which is the one place in the binary a Rust internal
87/// leaks out. Shared because both do it: fixing one leaves the identical screen
88/// on the other, confirmed by running both against a taken port.
89///
90/// `AddrInUse` gets the case worth naming, because "the port is taken" is by
91/// far the most common way this fails and has an obvious next step. Classified
92/// from [`std::io::ErrorKind`] rather than from the message, which the OS
93/// writes in its own language.
94pub fn explain_bind_failure(what: &str, addr: &str, error: &std::io::Error) -> String {
95    let mut message = match error.kind() {
96        std::io::ErrorKind::AddrInUse => {
97            format!("cannot start the {what}: {addr} is already in use by another program.")
98        }
99        std::io::ErrorKind::PermissionDenied => {
100            format!("cannot start the {what}: not allowed to bind {addr}.")
101        }
102        std::io::ErrorKind::AddrNotAvailable => {
103            format!("cannot start the {what}: {addr} is not an address on this machine.")
104        }
105        _ => format!("cannot start the {what}: {addr} could not be bound."),
106    };
107    // The OS text is kept: it is the truth about that machine, and it is what
108    // an operator searches for.
109    message.push_str(&format!("\n  {error}"));
110    if error.kind() == std::io::ErrorKind::AddrInUse {
111        message.push_str("\n  Choose another port with -p, or stop whatever holds this one.");
112        if cfg!(windows) {
113            message.push_str(
114                "\n  To find it: Get-NetTCPConnection -LocalPort <port> | Select OwningProcess",
115            );
116        } else {
117            message.push_str("\n  To find it: ss -ltnp | grep :<port>");
118        }
119    }
120    message
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_session_not_found_display() {
129        let err = ShellTunnelError::SessionNotFound("sess-00000001".into());
130        assert!(err.to_string().contains("sess-00000001"));
131        assert!(err.to_string().contains("not found"));
132    }
133
134    #[test]
135    fn test_session_exists_display() {
136        let err = ShellTunnelError::SessionExists("sess-00000002".into());
137        assert!(err.to_string().contains("sess-00000002"));
138        assert!(err.to_string().contains("already exists"));
139    }
140
141    #[test]
142    fn test_io_error_conversion() {
143        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
144        let shell_err: ShellTunnelError = io_err.into();
145        assert!(matches!(shell_err, ShellTunnelError::Io(_)));
146        assert!(shell_err.to_string().contains("I/O error"));
147    }
148
149    #[test]
150    fn test_timeout_display() {
151        let err = ShellTunnelError::Timeout;
152        assert!(err.to_string().contains("timeout"));
153    }
154
155    #[test]
156    fn test_pty_error_display() {
157        let err = ShellTunnelError::Pty("failed to spawn".into());
158        assert!(err.to_string().contains("PTY error"));
159        assert!(err.to_string().contains("failed to spawn"));
160    }
161}