rusty_autossh/error.rs
1//! Public error type for `rusty-autossh`.
2//!
3//! Defines the [`AutosshError`] enum returned from the library API
4//! ([`crate::SshSupervisor::run`], [`crate::SshSupervisorBuilder::build`]) and
5//! used internally by all crate modules.
6//!
7//! # Forward-compatibility (SemVer policy)
8//!
9//! [`AutosshError`] is `#[non_exhaustive]` per AD-014, so additive variants in
10//! later releases are NOT a breaking change. Downstream consumers MUST include
11//! a wildcard `_` arm when pattern-matching:
12//!
13//! ```
14//! # use rusty_autossh::AutosshError;
15//! # fn handle(e: AutosshError) {
16//! match e {
17//! AutosshError::SshNotFound { .. } => { /* ... */ }
18//! AutosshError::Io(_) => { /* ... */ }
19//! _ => { /* required wildcard arm */ }
20//! }
21//! # }
22//! ```
23//!
24//! Exhaustive matches on `#[non_exhaustive]` types from a different crate are
25//! a compile error — this guards downstream consumers from breakage when new
26//! variants are added in later releases:
27//!
28//! ```compile_fail
29//! use rusty_autossh::AutosshError;
30//!
31//! fn handle(e: AutosshError) {
32//! // Missing the required wildcard `_` arm — fails to compile because
33//! // `AutosshError` is `#[non_exhaustive]`.
34//! match e {
35//! AutosshError::SshNotFound { .. } => {}
36//! AutosshError::MonitorBindFailed { .. } => {}
37//! AutosshError::MaxStartReached { .. } => {}
38//! AutosshError::MaxLifetimeReached => {}
39//! AutosshError::PidfileWrite { .. } => {}
40//! AutosshError::LogfileWrite { .. } => {}
41//! AutosshError::Io(_) => {}
42//! AutosshError::Daemonize { .. } => {}
43//! AutosshError::Internal(_) => {}
44//! }
45//! }
46//! ```
47
48use std::io;
49use std::path::PathBuf;
50
51/// Errors returned by the `rusty-autossh` library API.
52///
53/// `Send + Sync + 'static` per SC-009. `#[non_exhaustive]` per AD-014 so
54/// additive variants are not a breaking change.
55///
56/// `source()` returns the wrapped inner error for wrapping variants (those
57/// holding a `source: io::Error` field, the `#[from]` `Io` variant) and
58/// `None` for leaf variants (no inner source).
59///
60/// # Example
61///
62/// ```
63/// use std::io;
64/// use rusty_autossh::AutosshError;
65///
66/// // io::Error converts via `#[from]` (AD-014).
67/// let io_err = io::Error::new(io::ErrorKind::NotFound, "boom");
68/// let err: AutosshError = io_err.into();
69/// match err {
70/// AutosshError::Io(_) => {}
71/// _ => unreachable!(),
72/// }
73/// ```
74#[non_exhaustive]
75#[derive(Debug, thiserror::Error)]
76pub enum AutosshError {
77 /// The `ssh` binary could not be resolved from `AUTOSSH_PATH` or any
78 /// entry in the host `PATH`. The `searched` field enumerates the
79 /// directories probed (in walk order) for diagnostic surfacing.
80 #[error("ssh binary not found; searched {} location(s)", searched.len())]
81 SshNotFound {
82 /// Directories probed during the `PATH` walk (or the verbatim
83 /// `AUTOSSH_PATH` value when that env var was set).
84 searched: Vec<PathBuf>,
85 },
86
87 /// Failed to bind a monitor-port [`tokio::net::TcpListener`] on
88 /// `127.0.0.1:<port>` (typically `EADDRINUSE` or a permission error).
89 #[error("failed to bind monitor port {port}: {source}")]
90 MonitorBindFailed {
91 /// The TCP port that failed to bind.
92 port: u16,
93 /// Underlying OS error.
94 #[source]
95 source: io::Error,
96 },
97
98 /// The consecutive-retry counter reached the `AUTOSSH_MAXSTART` cap
99 /// (or `--max-start <n>` CLI override). Maps to upstream's
100 /// `autossh: maximum retries reached` stderr.
101 #[error("maximum retries reached after {attempts} attempts")]
102 MaxStartReached {
103 /// Number of consecutive child-spawn attempts performed before the
104 /// cap was hit.
105 attempts: u32,
106 },
107
108 /// `AUTOSSH_MAXLIFETIME` (or `--max-lifetime <secs>`) elapsed. Clean
109 /// self-termination; supervisor exits 0.
110 #[error("max lifetime reached")]
111 MaxLifetimeReached,
112
113 /// Atomic write of the pidfile failed at startup.
114 #[error("failed to write pidfile {}: {source}", path.display())]
115 PidfileWrite {
116 /// Pidfile path that failed to write.
117 path: PathBuf,
118 /// Underlying OS error.
119 #[source]
120 source: io::Error,
121 },
122
123 /// Failed to open/append to the logfile.
124 #[error("failed to write logfile {}: {source}", path.display())]
125 LogfileWrite {
126 /// Logfile path that failed to write.
127 path: PathBuf,
128 /// Underlying OS error.
129 #[source]
130 source: io::Error,
131 },
132
133 /// Generic I/O error surfaced from underlying syscalls.
134 #[error("io error: {0}")]
135 Io(#[from] io::Error),
136
137 /// The `daemonize` crate or Windows `CreateProcessW` self-respawn
138 /// failed during `-f` background mode setup.
139 #[error("daemonize failed: {reason}")]
140 Daemonize {
141 /// Human-readable reason for the daemonize failure.
142 reason: String,
143 },
144
145 /// An internal invariant was violated. The `&'static str` payload is a
146 /// short diagnostic tag (never user-supplied content).
147 #[error("internal error: {0}")]
148 Internal(&'static str),
149}