shep_core/signals.rs
1//! The signals `shep signal` may name.
2//!
3//! A grammar of its own, next to [`KillSignal`](crate::config::KillSignal)'s
4//! four rather than replacing them, because the two answer different
5//! questions. `KillSignal` is what a Flockfile's `kill_signal` may say: a
6//! signal the stop ladder can deliver as its polite rung and then escalate
7//! PAST. This one is what an operator may hand a running app: a nudge, with no
8//! ladder behind it and no escalation to follow. `SIGHUP` belongs here and not
9//! there; `SIGKILL` belongs here and not there for the opposite reason.
10//!
11//! # No raw numbers
12//!
13//! Deliberately no `as_raw`. Signal numbers are not portable — `SIGUSR1` is 10
14//! on Linux and 30 on macOS, `SIGCONT` is 18 and 19 — and shep-core is the
15//! portable crate with no libc to ask. The enum crosses shep-daemon's runner
16//! seam as an enum, exactly as `StopSignal` does, and `tokio_runner.rs` is the
17//! one place that turns it into something the kernel understands.
18//!
19//! # What is not here, and why
20//!
21//! `SIGSTOP` parses to nothing. It is deliverable and an operator might mean
22//! it, but a `SIGSTOP`ed sheep still reads `online` in `shep flock`, in
23//! `describe`, on the bus and to every dog — the shepherd owns no mechanism
24//! that could see the difference. Refusing it keeps shep from producing a
25//! flock state it cannot describe. `SIGCONT` IS accepted, because an operator
26//! who stopped a sheep by some other route needs a way back.
27
28/// A signal `shep signal` may name.
29///
30/// Nine, not every signal on the platform. Each one here is something an
31/// operator plausibly means to say to an application, and nothing here is a
32/// signal shep would be delivering on the kernel's behalf (`SIGSEGV`,
33/// `SIGBUS`, `SIGPIPE` and the rest are the kernel's to send, not an
34/// operator's).
35///
36/// Exhaustive, not `#[non_exhaustive]`, matching
37/// [`KillSignal`](crate::config::KillSignal) and for the same reason (IR-20:
38/// don't cargo-cult it). Growth is possible but is not anticipated, and a
39/// caller matching on all nine — shep-daemon's own mapping to `nix` is the one
40/// that matters — should get a compile error the day a tenth arrives rather
41/// than a silent wildcard arm.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum OperatorSignal {
44 /// `SIGHUP` — hang up. The near-universal "re-read your configuration".
45 Hup,
46 /// `SIGINT` — interrupt, what Ctrl-C sends.
47 Int,
48 /// `SIGQUIT` — quit, core-dumping by default. Several runtimes dump every
49 /// thread's stack on it instead.
50 Quit,
51 /// `SIGTERM` — the polite stop. Sending it here bypasses the stop ladder
52 /// entirely: shep does not start a `kill_timeout`, does not escalate, and
53 /// does not mark the sheep stopped. Use `shep stop` for a stop.
54 Term,
55 /// `SIGUSR1` — user-defined signal 1.
56 Usr1,
57 /// `SIGUSR2` — user-defined signal 2, the one several runtimes reserve for
58 /// a graceful restart.
59 Usr2,
60 /// `SIGWINCH` — terminal resized. Harmless to nearly everything, which is
61 /// what makes it the signal to test a wiring with.
62 Winch,
63 /// `SIGCONT` — continue a stopped process.
64 Cont,
65 /// `SIGKILL` — unblockable, immediate. The restart policy will see the
66 /// exit as any other unexpected one and act on it: an app with
67 /// `autorestart` on comes back.
68 Kill,
69}
70
71impl OperatorSignal {
72 /// Every spelling this grammar accepts, canonical form, in the order a
73 /// refusal lists them.
74 ///
75 /// Public because it is rendered into the refusal an operator reads and
76 /// into `shep signal --help`; a second hand-written list in either place
77 /// is one free to drift.
78 pub const ACCEPTED: [&'static str; 9] = [
79 "SIGHUP", "SIGINT", "SIGQUIT", "SIGTERM", "SIGUSR1", "SIGUSR2", "SIGWINCH", "SIGCONT",
80 "SIGKILL",
81 ];
82
83 /// Parses one signal name, case-insensitively, with or without the `SIG`
84 /// prefix. `None` for anything else, including a raw number — a number
85 /// means different signals on different platforms, and shep will not guess
86 /// which one an operator meant.
87 #[must_use]
88 pub fn parse(name: &str) -> Option<Self> {
89 match name.to_ascii_uppercase().as_str() {
90 "SIGHUP" | "HUP" => Some(Self::Hup),
91 "SIGINT" | "INT" => Some(Self::Int),
92 "SIGQUIT" | "QUIT" => Some(Self::Quit),
93 "SIGTERM" | "TERM" => Some(Self::Term),
94 "SIGUSR1" | "USR1" => Some(Self::Usr1),
95 "SIGUSR2" | "USR2" => Some(Self::Usr2),
96 "SIGWINCH" | "WINCH" => Some(Self::Winch),
97 "SIGCONT" | "CONT" => Some(Self::Cont),
98 "SIGKILL" | "KILL" => Some(Self::Kill),
99 _ => None,
100 }
101 }
102
103 /// The canonical name, always `SIG`-prefixed and uppercase.
104 #[must_use]
105 pub fn as_str(self) -> &'static str {
106 match self {
107 Self::Hup => "SIGHUP",
108 Self::Int => "SIGINT",
109 Self::Quit => "SIGQUIT",
110 Self::Term => "SIGTERM",
111 Self::Usr1 => "SIGUSR1",
112 Self::Usr2 => "SIGUSR2",
113 Self::Winch => "SIGWINCH",
114 Self::Cont => "SIGCONT",
115 Self::Kill => "SIGKILL",
116 }
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 /// fails if `ACCEPTED` and `as_str` disagree. The list is what a refusal
125 /// prints, so an operator picking a replacement word is reading it — a
126 /// name advertised but not parsed sends them in a circle.
127 #[test]
128 fn every_accepted_name_round_trips_through_parse() {
129 for name in OperatorSignal::ACCEPTED {
130 let parsed = OperatorSignal::parse(name)
131 .unwrap_or_else(|| panic!("`{name}` is advertised but not parsed"));
132 assert_eq!(parsed.as_str(), name);
133 }
134 }
135
136 /// fails if the bare form or a lowercase spelling stops parsing. Both are
137 /// accepted for the reason `KillSignal` accepts both: an operator types
138 /// what `kill -l` prints, and that is the bare form.
139 #[test]
140 fn the_prefix_and_the_case_are_both_optional() {
141 assert_eq!(OperatorSignal::parse("hup"), Some(OperatorSignal::Hup));
142 assert_eq!(OperatorSignal::parse("SigUsr1"), Some(OperatorSignal::Usr1));
143 assert_eq!(OperatorSignal::parse("WINCH"), Some(OperatorSignal::Winch));
144 }
145
146 /// fails if SIGSTOP is ever waved through. It is the one real, spellable,
147 /// deliverable signal this grammar refuses, and the refusal is the design:
148 /// a stopped sheep still reads `online` in every listing shep can produce,
149 /// so accepting it would put the flock in a state the shepherd cannot
150 /// report on.
151 #[test]
152 fn sigstop_is_refused_because_the_shepherd_could_not_report_it() {
153 assert_eq!(OperatorSignal::parse("SIGSTOP"), None);
154 assert_eq!(OperatorSignal::parse("stop"), None);
155 }
156
157 /// fails if a name outside the table parses. `SIGSEGV` is the shape that
158 /// matters: a real signal, plausibly typed, that shep has no business
159 /// delivering on an operator's behalf.
160 #[test]
161 fn a_name_outside_the_table_does_not_parse() {
162 assert_eq!(OperatorSignal::parse("SIGSEGV"), None);
163 assert_eq!(OperatorSignal::parse(""), None);
164 assert_eq!(OperatorSignal::parse("9"), None);
165 }
166
167 /// fails if this grammar stops covering the one `kill_signal` already
168 /// accepts. The two exist for different jobs and are allowed to differ —
169 /// but the operator-facing set being NARROWER than the config-facing one
170 /// would mean a signal shep sends on every stop is one an operator may not
171 /// ask for by name, which is indefensible in either direction.
172 #[test]
173 fn every_kill_signal_name_is_also_an_operator_signal() {
174 for name in crate::config::KillSignal::ACCEPTED {
175 assert!(
176 OperatorSignal::parse(name).is_some(),
177 "`{name}` is a kill_signal but not an operator signal"
178 );
179 }
180 }
181}