Skip to main content

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}