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: that one is what a Flockfile's `kill_signal` may say and the stop
5//! ladder delivers; this one is a nudge an operator hands a running app,
6//! with no ladder and no escalation.
7//!
8//! No `as_raw`: signal numbers are not portable (`SIGUSR1` is 10 on Linux,
9//! 30 on macOS), and shep-core has no libc to ask.
10//!
11//! `SIGSTOP` parses to nothing: a `SIGSTOP`ed sheep still reads `online`
12//! everywhere shep reports state. `SIGCONT` is accepted, for the way back.
13
14/// A signal `shep signal` may name.
15///
16/// Nine, not every signal on the platform: each is something an operator
17/// plausibly means to say to an application. Nothing here is a signal shep
18/// would deliver on the kernel's behalf (`SIGSEGV`, `SIGBUS`, `SIGPIPE` and
19/// the rest are the kernel's to send).
20///
21/// Exhaustive, not `#[non_exhaustive]`: a caller matching on all nine
22/// should get a compile error the day a tenth arrives, not a silent
23/// wildcard arm.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum OperatorSignal {
26 /// `SIGHUP`: hang up, the near-universal "re-read your configuration".
27 Hup,
28 /// `SIGINT`: interrupt, what Ctrl-C sends.
29 Int,
30 /// `SIGQUIT`: quit, core-dumping by default. Several runtimes dump every
31 /// thread's stack on it instead.
32 Quit,
33 /// `SIGTERM`: the polite stop. Sending it here bypasses the stop ladder
34 /// entirely: shep does not start a `kill_timeout`, does not escalate, and
35 /// does not mark the sheep stopped. Use `shep stop` for a stop.
36 Term,
37 /// `SIGUSR1`: user-defined signal 1.
38 Usr1,
39 /// `SIGUSR2`: user-defined signal 2, the one several runtimes reserve for
40 /// a graceful restart.
41 Usr2,
42 /// `SIGWINCH`: terminal resized. Harmless to nearly everything, which is
43 /// what makes it the signal to test a wiring with.
44 Winch,
45 /// `SIGCONT`: continue a stopped process.
46 Cont,
47 /// `SIGKILL`: unblockable, immediate. The restart policy will see the
48 /// exit as any other unexpected one and act on it: an app with
49 /// `autorestart` on comes back.
50 Kill,
51}
52
53impl OperatorSignal {
54 /// Every spelling this grammar accepts, canonical form, in the order a
55 /// refusal lists them.
56 ///
57 /// Public because it is rendered into the refusal an operator reads and
58 /// into `shep signal --help`; a second hand-written list in either place
59 /// is one free to drift.
60 pub const ACCEPTED: [&'static str; 9] = [
61 "SIGHUP", "SIGINT", "SIGQUIT", "SIGTERM", "SIGUSR1", "SIGUSR2", "SIGWINCH", "SIGCONT",
62 "SIGKILL",
63 ];
64
65 /// Parses one signal name, case-insensitively, with or without the `SIG`
66 /// prefix. `None` for anything else, including a raw number: a number
67 /// means different signals on different platforms, and shep will not
68 /// guess which one an operator meant.
69 #[must_use]
70 pub fn parse(name: &str) -> Option<Self> {
71 match name.to_ascii_uppercase().as_str() {
72 "SIGHUP" | "HUP" => Some(Self::Hup),
73 "SIGINT" | "INT" => Some(Self::Int),
74 "SIGQUIT" | "QUIT" => Some(Self::Quit),
75 "SIGTERM" | "TERM" => Some(Self::Term),
76 "SIGUSR1" | "USR1" => Some(Self::Usr1),
77 "SIGUSR2" | "USR2" => Some(Self::Usr2),
78 "SIGWINCH" | "WINCH" => Some(Self::Winch),
79 "SIGCONT" | "CONT" => Some(Self::Cont),
80 "SIGKILL" | "KILL" => Some(Self::Kill),
81 _ => None,
82 }
83 }
84
85 /// The canonical name, always `SIG`-prefixed and uppercase.
86 #[must_use]
87 pub fn as_str(self) -> &'static str {
88 match self {
89 Self::Hup => "SIGHUP",
90 Self::Int => "SIGINT",
91 Self::Quit => "SIGQUIT",
92 Self::Term => "SIGTERM",
93 Self::Usr1 => "SIGUSR1",
94 Self::Usr2 => "SIGUSR2",
95 Self::Winch => "SIGWINCH",
96 Self::Cont => "SIGCONT",
97 Self::Kill => "SIGKILL",
98 }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 /// The list is what a refusal prints, so a name advertised but not
107 /// parsed sends an operator in a circle.
108 #[test]
109 fn every_accepted_name_round_trips_through_parse() {
110 for name in OperatorSignal::ACCEPTED {
111 let parsed = OperatorSignal::parse(name)
112 .unwrap_or_else(|| panic!("`{name}` is advertised but not parsed"));
113 assert_eq!(parsed.as_str(), name);
114 }
115 }
116
117 /// Both accepted for the reason `KillSignal` accepts both: an operator
118 /// types what `kill -l` prints, the bare form.
119 #[test]
120 fn the_prefix_and_the_case_are_both_optional() {
121 assert_eq!(OperatorSignal::parse("hup"), Some(OperatorSignal::Hup));
122 assert_eq!(OperatorSignal::parse("SigUsr1"), Some(OperatorSignal::Usr1));
123 assert_eq!(OperatorSignal::parse("WINCH"), Some(OperatorSignal::Winch));
124 }
125
126 /// The one real, spellable, deliverable signal this grammar refuses: a
127 /// stopped sheep still reads `online` everywhere shep reports state.
128 #[test]
129 fn sigstop_is_refused_because_the_shepherd_could_not_report_it() {
130 assert_eq!(OperatorSignal::parse("SIGSTOP"), None);
131 assert_eq!(OperatorSignal::parse("stop"), None);
132 }
133
134 /// `SIGSEGV` is the shape that matters: a real signal, plausibly typed,
135 /// that shep has no business delivering on an operator's behalf.
136 #[test]
137 fn a_name_outside_the_table_does_not_parse() {
138 assert_eq!(OperatorSignal::parse("SIGSEGV"), None);
139 assert_eq!(OperatorSignal::parse(""), None);
140 assert_eq!(OperatorSignal::parse("9"), None);
141 }
142
143 /// The two exist for different jobs and may differ, but the
144 /// operator-facing set being narrower than the config-facing one would
145 /// mean an operator cannot name a signal shep itself sends.
146 #[test]
147 fn every_kill_signal_name_is_also_an_operator_signal() {
148 for name in crate::config::KillSignal::ACCEPTED {
149 assert!(
150 OperatorSignal::parse(name).is_some(),
151 "`{name}` is a kill_signal but not an operator signal"
152 );
153 }
154 }
155}