watchexec_signals/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(not(test), warn(unused_crate_dependencies))]
3
4use std::fmt;
5
6#[cfg(feature = "fromstr")]
7use std::str::FromStr;
8
9#[cfg(unix)]
10use nix::sys::signal::Signal as NixSignal;
11
12/// A notification (signals or Windows control events) sent to a process.
13///
14/// This signal type in Watchexec is used for any of:
15/// - signals sent to the main process by some external actor,
16/// - signals received from a sub process by the main process,
17/// - signals sent to a sub process by Watchexec.
18///
19/// On Windows, only some signals are supported, as described. Others will be ignored.
20///
21/// On Unix, there are several "first-class" signals which have their own variants, and a generic
22/// [`Custom`][Signal::Custom] variant which can be used to send arbitrary signals.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24#[non_exhaustive]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26#[cfg_attr(
27	feature = "serde",
28	serde(
29		from = "serde_support::SerdeSignal",
30		into = "serde_support::SerdeSignal"
31	)
32)]
33pub enum Signal {
34	/// Indicate that the terminal is disconnected.
35	///
36	/// On Unix, this is `SIGHUP`. On Windows, this is ignored for now but may be supported in the
37	/// future (see [#219](https://github.com/watchexec/watchexec/issues/219)).
38	///
39	/// Despite its nominal purpose, on Unix this signal is often used to reload configuration files.
40	Hangup,
41
42	/// Indicate to the kernel that the process should stop.
43	///
44	/// On Unix, this is `SIGKILL`. On Windows, this is `TerminateProcess`.
45	///
46	/// This signal is not handled by the process, but directly by the kernel, and thus cannot be
47	/// intercepted. Subprocesses may exit in inconsistent states.
48	ForceStop,
49
50	/// Indicate that the process should stop.
51	///
52	/// On Unix, this is `SIGINT`. On Windows, this is ignored for now but may be supported in the
53	/// future (see [#219](https://github.com/watchexec/watchexec/issues/219)).
54	///
55	/// This signal generally indicates an action taken by the user, so it may be handled
56	/// differently than a termination.
57	Interrupt,
58
59	/// Indicate that the process is to stop, the kernel will then dump its core.
60	///
61	/// On Unix, this is `SIGQUIT`. On Windows, it is ignored.
62	///
63	/// This is rarely used.
64	Quit,
65
66	/// Indicate that the process should stop.
67	///
68	/// On Unix, this is `SIGTERM`. On Windows, this is ignored for now but may be supported in the
69	/// future (see [#219](https://github.com/watchexec/watchexec/issues/219)).
70	///
71	/// On Unix, this signal generally indicates an action taken by the system, so it may be handled
72	/// differently than an interruption.
73	Terminate,
74
75	/// Indicate an application-defined behaviour should happen.
76	///
77	/// On Unix, this is `SIGUSR1`. On Windows, it is ignored.
78	///
79	/// This signal is generally used to start debugging.
80	User1,
81
82	/// Indicate an application-defined behaviour should happen.
83	///
84	/// On Unix, this is `SIGUSR2`. On Windows, it is ignored.
85	///
86	/// This signal is generally used to reload configuration.
87	User2,
88
89	/// Indicate using a custom signal.
90	///
91	/// Internally, this is converted to a [`nix::Signal`](https://docs.rs/nix/*/nix/sys/signal/enum.Signal.html)
92	/// but for portability this variant is a raw `i32`.
93	///
94	/// Invalid signals on the current platform will be ignored. Does nothing on Windows.
95	///
96	/// The special value `0` is used to indicate an unknown signal. That is, a signal was received
97	/// or parsed, but it is not known which. This is not a usual case, and should in general be
98	/// ignored rather than hard-erroring.
99	///
100	/// # Examples
101	///
102	/// ```
103	/// # #[cfg(unix)]
104	/// # {
105	/// use watchexec_signals::Signal;
106	/// use nix::sys::signal::Signal as NixSignal;
107	/// assert_eq!(Signal::Custom(6), Signal::from(NixSignal::SIGABRT as i32));
108	/// # }
109	/// ```
110	///
111	/// On Unix the [`from_nix`][Signal::from_nix] method should be preferred if converting from
112	/// Nix's `Signal` type:
113	///
114	/// ```
115	/// # #[cfg(unix)]
116	/// # {
117	/// use watchexec_signals::Signal;
118	/// use nix::sys::signal::Signal as NixSignal;
119	/// assert_eq!(Signal::Custom(6), Signal::from_nix(NixSignal::SIGABRT));
120	/// # }
121	/// ```
122	Custom(i32),
123}
124
125impl Signal {
126	/// Converts to a [`nix::Signal`][NixSignal] if possible.
127	///
128	/// This will return `None` if the signal is not supported on the current platform (only for
129	/// [`Custom`][Signal::Custom], as the first-class ones are always supported).
130	#[cfg(unix)]
131	#[must_use]
132	pub fn to_nix(self) -> Option<NixSignal> {
133		match self {
134			Self::Hangup => Some(NixSignal::SIGHUP),
135			Self::ForceStop => Some(NixSignal::SIGKILL),
136			Self::Interrupt => Some(NixSignal::SIGINT),
137			Self::Quit => Some(NixSignal::SIGQUIT),
138			Self::Terminate => Some(NixSignal::SIGTERM),
139			Self::User1 => Some(NixSignal::SIGUSR1),
140			Self::User2 => Some(NixSignal::SIGUSR2),
141			Self::Custom(sig) => NixSignal::try_from(sig).ok(),
142		}
143	}
144
145	/// Converts from a [`nix::Signal`][NixSignal].
146	#[cfg(unix)]
147	#[allow(clippy::missing_const_for_fn)]
148	#[must_use]
149	pub fn from_nix(sig: NixSignal) -> Self {
150		match sig {
151			NixSignal::SIGHUP => Self::Hangup,
152			NixSignal::SIGKILL => Self::ForceStop,
153			NixSignal::SIGINT => Self::Interrupt,
154			NixSignal::SIGQUIT => Self::Quit,
155			NixSignal::SIGTERM => Self::Terminate,
156			NixSignal::SIGUSR1 => Self::User1,
157			NixSignal::SIGUSR2 => Self::User2,
158			sig => Self::Custom(sig as _),
159		}
160	}
161}
162
163impl From<i32> for Signal {
164	/// Converts from a raw signal number.
165	///
166	/// This uses hardcoded numbers for the first-class signals.
167	fn from(raw: i32) -> Self {
168		match raw {
169			1 => Self::Hangup,
170			2 => Self::Interrupt,
171			3 => Self::Quit,
172			9 => Self::ForceStop,
173			10 => Self::User1,
174			12 => Self::User2,
175			15 => Self::Terminate,
176			_ => Self::Custom(raw),
177		}
178	}
179}
180
181#[cfg(feature = "fromstr")]
182impl Signal {
183	/// Parse the input as a unix signal.
184	///
185	/// This parses the input as a signal name, or a signal number, in a case-insensitive manner.
186	/// It supports integers, the short name of the signal (like `INT`, `HUP`, `USR1`, etc), and
187	/// the long name of the signal (like `SIGINT`, `SIGHUP`, `SIGUSR1`, etc).
188	///
189	/// Note that this is entirely accurate only when used on unix targets; on other targets it
190	/// falls back to a hardcoded approximation instead of looking up signal tables (via [`nix`]).
191	///
192	/// ```
193	/// # use watchexec_signals::Signal;
194	/// assert_eq!(Signal::Hangup, Signal::from_unix_str("hup").unwrap());
195	/// assert_eq!(Signal::Interrupt, Signal::from_unix_str("SIGINT").unwrap());
196	/// assert_eq!(Signal::ForceStop, Signal::from_unix_str("Kill").unwrap());
197	/// ```
198	///
199	/// Using [`FromStr`] is recommended for practical use, as it will also parse Windows control
200	/// events, see [`Signal::from_windows_str`].
201	pub fn from_unix_str(s: &str) -> Result<Self, SignalParseError> {
202		Self::from_unix_str_impl(s)
203	}
204
205	#[cfg(unix)]
206	fn from_unix_str_impl(s: &str) -> Result<Self, SignalParseError> {
207		if let Ok(sig) = i32::from_str(s) {
208			if let Ok(sig) = NixSignal::try_from(sig) {
209				return Ok(Self::from_nix(sig));
210			}
211		}
212
213		if let Ok(sig) = NixSignal::from_str(&s.to_ascii_uppercase())
214			.or_else(|_| NixSignal::from_str(&format!("SIG{}", s.to_ascii_uppercase())))
215		{
216			return Ok(Self::from_nix(sig));
217		}
218
219		Err(SignalParseError::new(s, "unsupported signal"))
220	}
221
222	#[cfg(not(unix))]
223	fn from_unix_str_impl(s: &str) -> Result<Self, SignalParseError> {
224		match s.to_ascii_uppercase().as_str() {
225			"KILL" | "SIGKILL" | "9" => Ok(Self::ForceStop),
226			"HUP" | "SIGHUP" | "1" => Ok(Self::Hangup),
227			"INT" | "SIGINT" | "2" => Ok(Self::Interrupt),
228			"QUIT" | "SIGQUIT" | "3" => Ok(Self::Quit),
229			"TERM" | "SIGTERM" | "15" => Ok(Self::Terminate),
230			"USR1" | "SIGUSR1" | "10" => Ok(Self::User1),
231			"USR2" | "SIGUSR2" | "12" => Ok(Self::User2),
232			number => match i32::from_str(number) {
233				Ok(int) => Ok(Self::Custom(int)),
234				Err(_) => Err(SignalParseError::new(s, "unsupported signal")),
235			},
236		}
237	}
238
239	/// Parse the input as a windows control event.
240	///
241	/// This parses the input as a control event name, in a case-insensitive manner.
242	///
243	/// The names matched are mostly made up as there's no standard for them, but should be familiar
244	/// to Windows users. They are mapped to the corresponding unix concepts as follows:
245	///
246	/// - `CTRL-CLOSE`, `CTRL+CLOSE`, or `CLOSE` for a hangup
247	/// - `CTRL-BREAK`, `CTRL+BREAK`, or `BREAK` for a terminate
248	/// - `CTRL-C`, `CTRL+C`, or `C` for an interrupt
249	/// - `STOP`, `FORCE-STOP` for a forced stop. This is also mapped to `KILL` and `SIGKILL`.
250	///
251	/// ```
252	/// # use watchexec_signals::Signal;
253	/// assert_eq!(Signal::Hangup, Signal::from_windows_str("ctrl+close").unwrap());
254	/// assert_eq!(Signal::Interrupt, Signal::from_windows_str("C").unwrap());
255	/// assert_eq!(Signal::ForceStop, Signal::from_windows_str("Stop").unwrap());
256	/// ```
257	///
258	/// Using [`FromStr`] is recommended for practical use, as it will fall back to parsing as a
259	/// unix signal, which can be helpful for portability.
260	pub fn from_windows_str(s: &str) -> Result<Self, SignalParseError> {
261		match s.to_ascii_uppercase().as_str() {
262			"CTRL-CLOSE" | "CTRL+CLOSE" | "CLOSE" => Ok(Self::Hangup),
263			"CTRL-BREAK" | "CTRL+BREAK" | "BREAK" => Ok(Self::Terminate),
264			"CTRL-C" | "CTRL+C" | "C" => Ok(Self::Interrupt),
265			"KILL" | "SIGKILL" | "FORCE-STOP" | "STOP" => Ok(Self::ForceStop),
266			_ => Err(SignalParseError::new(s, "unknown control name")),
267		}
268	}
269}
270
271#[cfg(feature = "fromstr")]
272impl FromStr for Signal {
273	type Err = SignalParseError;
274
275	fn from_str(s: &str) -> Result<Self, Self::Err> {
276		Self::from_windows_str(s).or_else(|err| Self::from_unix_str(s).map_err(|_| err))
277	}
278}
279
280/// Error when parsing a signal from string.
281#[cfg(feature = "fromstr")]
282#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
283#[derive(Debug, thiserror::Error)]
284#[error("invalid signal `{src}`: {err}")]
285pub struct SignalParseError {
286	// The string that was parsed.
287	#[cfg_attr(feature = "miette", source_code)]
288	src: String,
289
290	// The error that occurred.
291	err: String,
292
293	// The span of the source which is in error.
294	#[cfg_attr(feature = "miette", label = "invalid signal")]
295	span: (usize, usize),
296}
297
298#[cfg(feature = "fromstr")]
299impl SignalParseError {
300	#[must_use]
301	pub fn new(src: &str, err: &str) -> Self {
302		Self {
303			src: src.to_owned(),
304			err: err.to_owned(),
305			span: (0, src.len()),
306		}
307	}
308}
309
310impl fmt::Display for Signal {
311	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312		write!(
313			f,
314			"{}",
315			match (self, cfg!(windows)) {
316				(Self::Hangup, false) => "SIGHUP",
317				(Self::Hangup, true) => "CTRL-CLOSE",
318				(Self::ForceStop, false) => "SIGKILL",
319				(Self::ForceStop, true) => "STOP",
320				(Self::Interrupt, false) => "SIGINT",
321				(Self::Interrupt, true) => "CTRL-C",
322				(Self::Quit, _) => "SIGQUIT",
323				(Self::Terminate, false) => "SIGTERM",
324				(Self::Terminate, true) => "CTRL-BREAK",
325				(Self::User1, _) => "SIGUSR1",
326				(Self::User2, _) => "SIGUSR2",
327				(Self::Custom(n), _) => {
328					return write!(f, "{n}");
329				}
330			}
331		)
332	}
333}
334
335#[cfg(feature = "serde")]
336mod serde_support {
337	use super::Signal;
338
339	#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
340	#[serde(untagged)]
341	pub enum SerdeSignal {
342		Named(NamedSignal),
343		Number(i32),
344	}
345
346	#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
347	#[serde(rename_all = "kebab-case")]
348	pub enum NamedSignal {
349		#[serde(rename = "SIGHUP")]
350		Hangup,
351		#[serde(rename = "SIGKILL")]
352		ForceStop,
353		#[serde(rename = "SIGINT")]
354		Interrupt,
355		#[serde(rename = "SIGQUIT")]
356		Quit,
357		#[serde(rename = "SIGTERM")]
358		Terminate,
359		#[serde(rename = "SIGUSR1")]
360		User1,
361		#[serde(rename = "SIGUSR2")]
362		User2,
363	}
364
365	impl From<Signal> for SerdeSignal {
366		fn from(signal: Signal) -> Self {
367			match signal {
368				Signal::Hangup => Self::Named(NamedSignal::Hangup),
369				Signal::Interrupt => Self::Named(NamedSignal::Interrupt),
370				Signal::Quit => Self::Named(NamedSignal::Quit),
371				Signal::Terminate => Self::Named(NamedSignal::Terminate),
372				Signal::User1 => Self::Named(NamedSignal::User1),
373				Signal::User2 => Self::Named(NamedSignal::User2),
374				Signal::ForceStop => Self::Named(NamedSignal::ForceStop),
375				Signal::Custom(number) => Self::Number(number),
376			}
377		}
378	}
379
380	impl From<SerdeSignal> for Signal {
381		fn from(signal: SerdeSignal) -> Self {
382			match signal {
383				SerdeSignal::Named(NamedSignal::Hangup) => Self::Hangup,
384				SerdeSignal::Named(NamedSignal::ForceStop) => Self::ForceStop,
385				SerdeSignal::Named(NamedSignal::Interrupt) => Self::Interrupt,
386				SerdeSignal::Named(NamedSignal::Quit) => Self::Quit,
387				SerdeSignal::Named(NamedSignal::Terminate) => Self::Terminate,
388				SerdeSignal::Named(NamedSignal::User1) => Self::User1,
389				SerdeSignal::Named(NamedSignal::User2) => Self::User2,
390				SerdeSignal::Number(number) => Self::Custom(number),
391			}
392		}
393	}
394}