1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
//! Error types for the daemonization sequence, with `sysexits.h` exit codes.
/// Errors produced during configuration validation or the daemonization sequence.
///
/// Each variant maps to a `sysexits.h` exit code via [`exit_code`](DaemonizeError::exit_code).
/// `ProgramNotFound` and `ExecFailed` are produced only by the CLI binary, never by the library.
///
/// # Payload stability
///
/// `String` payloads are human-readable display text, not a stable API:
/// match on the variant and [`exit_code`](DaemonizeError::exit_code), not
/// on payload contents. Only named struct fields (e.g.
/// [`LockConflict::path`](DaemonizeError::LockConflict),
/// [`Application::code`](DaemonizeError::Application)) are stable.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum DaemonizeError {
/// Bad path, bad env key, path overlap, or other config error.
#[error("validation error: {0}")]
ValidationError(String),
/// CLI-only: program missing or not executable — caught by the pre-fork
/// path check, or at exec time by `ENOENT` (missing PATH command or script
/// interpreter) or `EACCES` (program not executable).
#[error("program not found: {0}")]
ProgramNotFound(String),
/// User does not exist at runtime during user switching.
#[error("user not found: {0}")]
UserNotFound(String),
/// Group does not exist at runtime during group switching.
#[error("group not found: {0}")]
GroupNotFound(String),
/// flock already held by another process — typically "the daemon is
/// already running".
#[error("lock conflict: {} is already locked by another process", path.display())]
LockConflict {
/// The effective lockfile (by default the pidfile itself). Callers
/// handling already-running can read the running instance's PID from
/// this path when it is the pidfile.
path: std::path::PathBuf,
},
/// Lockfile cannot be opened.
#[error("lockfile error: {0}")]
LockfileError(String),
/// `fork()` returned an error.
#[error("fork failed: {0}")]
ForkFailed(String),
/// `setsid()` returned an error.
#[error("setsid failed: {0}")]
SetsidFailed(String),
/// `chdir()` failed at runtime after fork.
#[error("chdir failed: {0}")]
ChdirFailed(String),
/// A required system call in the post-fork sequence failed: opening
/// `/dev/null`, `sigaction` (resetting signal dispositions),
/// `sigprocmask` (clearing the signal mask), or `getrlimit` (querying
/// the fd limit before closing inherited descriptors).
#[error("system error: {0}")]
SystemError(String),
/// Non-root caller with user switch, or setuid/setgid failure.
#[error("permission denied: {0}")]
PermissionDenied(String),
/// Pidfile cannot be written.
#[error("pidfile error: {0}")]
PidfileError(String),
/// stdout/stderr file cannot be opened or dup2'd.
#[error("output file error: {0}")]
OutputFileError(String),
/// chown of pidfile/lockfile/output file failed.
#[error("chown error: {0}")]
ChownError(String),
/// CLI-only: exec of target program failed for a reason other than
/// `ENOENT`/`EACCES` (which map to [`ProgramNotFound`](Self::ProgramNotFound)).
#[error("exec failed: {0}")]
ExecFailed(String),
/// Writing the readiness byte to the parent notification pipe failed.
///
/// Produced by [`notify_parent`](crate::DaemonContext::notify_parent) when
/// the pipe write returns an `io::Error`.
#[error("failed to notify parent: {0}")]
NotifyFailed(#[source] std::io::Error),
/// A user and/or group was configured but
/// [`drop_privileges`](crate::DaemonContext::drop_privileges) was never
/// called before the daemon signaled readiness.
///
/// Surfaced by [`notify_parent`](crate::DaemonContext::notify_parent) so a
/// daemon configured to run as an unprivileged user fails loudly instead of
/// silently continuing to run with elevated privileges.
#[error(
"privileges not dropped: a user/group was configured but \
drop_privileges() was never called before notifying readiness"
)]
PrivilegesNotDropped,
/// Application-level failure during the privileged init window, reported by
/// the caller (e.g. a socket bind or database connect that failed after
/// [`daemonize`](crate::daemonize) but before
/// [`notify_parent`](crate::DaemonContext::notify_parent)).
///
/// Unlike the other variants, this one is meant to be constructed by users
/// — typically via [`application`](DaemonizeError::application) — so they
/// can surface their own startup errors to the parent through
/// [`report_error`](crate::DaemonContext::report_error) with an exit code
/// of their choosing. The stored `code` is surfaced by
/// [`exit_code`](DaemonizeError::exit_code), except that 0 is remapped to
/// `70` (`EX_SOFTWARE`) so it can never alias success.
#[error("application error: {message}")]
Application {
/// Exit code to report to the parent (typically a non-zero `sysexits.h`
/// value; 0 is treated as `EX_SOFTWARE`).
code: u8,
/// Human-readable description of the failure.
message: String,
},
}
impl DaemonizeError {
/// Constructs an [`Application`](DaemonizeError::Application) error with a
/// caller-chosen exit `code` and `message`.
///
/// Use this to report a failure that happens in the privileged init window
/// (after [`daemonize`](crate::daemonize), before
/// [`notify_parent`](crate::DaemonContext::notify_parent)) to the parent
/// process via [`report_error`](crate::DaemonContext::report_error):
///
/// ```no_run
/// # use blivet::DaemonizeError;
/// # fn bind() -> std::io::Result<()> { Ok(()) }
/// // `75` is EX_TEMPFAIL; pick whichever sysexits code fits the failure.
/// if let Err(e) = bind() {
/// let err = DaemonizeError::application(75, format!("bind failed: {e}"));
/// // ctx.report_error(&err);
/// # let _ = err;
/// }
/// ```
pub fn application(code: u8, message: impl Into<String>) -> Self {
DaemonizeError::Application {
code,
message: message.into(),
}
}
/// Returns the `sysexits.h` exit code for this error variant.
///
/// Always non-zero, so it is safe to pass to `std::process::exit`: a
/// reported error can never be mistaken for success. A caller-supplied
/// [`Application`](DaemonizeError::Application) code of 0 is remapped to
/// `70` (`EX_SOFTWARE`).
pub fn exit_code(&self) -> u8 {
match self {
DaemonizeError::ValidationError(_) => 64, // EX_USAGE
DaemonizeError::ProgramNotFound(_) => 66, // EX_NOINPUT
DaemonizeError::UserNotFound(_) => 67, // EX_NOUSER
DaemonizeError::GroupNotFound(_) => 67, // EX_NOUSER
DaemonizeError::LockConflict { .. } => 69, // EX_UNAVAILABLE
DaemonizeError::LockfileError(_) => 73, // EX_CANTCREAT
DaemonizeError::ForkFailed(_) => 71, // EX_OSERR
DaemonizeError::SetsidFailed(_) => 71, // EX_OSERR
DaemonizeError::ChdirFailed(_) => 71, // EX_OSERR
DaemonizeError::SystemError(_) => 71, // EX_OSERR
DaemonizeError::PermissionDenied(_) => 77, // EX_NOPERM
DaemonizeError::PidfileError(_) => 73, // EX_CANTCREAT
DaemonizeError::OutputFileError(_) => 73, // EX_CANTCREAT
DaemonizeError::ChownError(_) => 73, // EX_CANTCREAT
DaemonizeError::ExecFailed(_) => 71, // EX_OSERR
DaemonizeError::NotifyFailed(_) => 71, // EX_OSERR
DaemonizeError::PrivilegesNotDropped => 70, // EX_SOFTWARE
// Caller-chosen, but never 0: that would alias success.
DaemonizeError::Application { code: 0, .. } => 70, // EX_SOFTWARE
DaemonizeError::Application { code, .. } => *code,
}
}
}