Skip to main content

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}