1use thiserror::Error;
4
5#[derive(Error, Debug)]
7pub enum ShellTunnelError {
8 #[error("session not found: {0}")]
10 SessionNotFound(String),
11
12 #[error("session already exists: {0}")]
14 SessionExists(String),
15
16 #[error("invalid state transition from {from:?} to {to:?}")]
18 InvalidStateTransition {
19 from: crate::session::SessionState,
20 to: crate::session::SessionState,
21 },
22
23 #[error("PTY error: {0}")]
25 Pty(String),
26
27 #[error("I/O error: {0}")]
29 Io(#[from] std::io::Error),
30
31 #[error("command execution timeout")]
33 Timeout,
34
35 #[error("session terminated")]
37 SessionTerminated,
38
39 #[error("internal lock poisoned")]
41 LockPoisoned,
42
43 #[error("channel send error: {0}")]
45 ChannelSend(String),
46
47 #[error("channel closed")]
49 ChannelClosed,
50
51 #[error("command execution failed: {0}")]
53 ExecutionFailed(String),
54
55 #[error("output parse error: {0}")]
57 ParseError(String),
58
59 #[error("session not executable: current state is {0:?}")]
61 NotExecutable(crate::session::SessionState),
62
63 #[cfg(feature = "tls")]
65 #[error("tls error: {0}")]
66 Tls(String),
67
68 #[error("tunnel error: {0}")]
70 Tunnel(String),
71
72 #[cfg(feature = "self-update")]
74 #[error("update error: {0}")]
75 Update(String),
76}
77
78pub type Result<T> = std::result::Result<T, ShellTunnelError>;
80
81pub 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 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}