radicle_signals/
lib.rs

1use std::io;
2
3use crossbeam_channel as chan;
4use signals_receipts::channel_notify_facility::{
5    self, FinishError, InstallError, SendError, SignalsChannel as _, UninstallError,
6};
7use signals_receipts::SignalNumber;
8
9use crate::channel_notify_facility_premade::SignalsChannel;
10
11/// Operating system signal.
12#[derive(Debug, Copy, Clone, PartialEq, Eq)]
13pub enum Signal {
14    /// `SIGINT`.
15    Interrupt,
16    /// `SIGTERM`.
17    Terminate,
18    /// `SIGHUP`.
19    Hangup,
20    /// `SIGWINCH`.
21    WindowChanged,
22}
23
24impl TryFrom<SignalNumber> for Signal {
25    type Error = SignalNumber;
26
27    fn try_from(value: SignalNumber) -> Result<Self, Self::Error> {
28        match value {
29            libc::SIGTERM => Ok(Self::Terminate),
30            libc::SIGINT => Ok(Self::Interrupt),
31            libc::SIGWINCH => Ok(Self::WindowChanged),
32            libc::SIGHUP => Ok(Self::Hangup),
33            _ => Err(value),
34        }
35    }
36}
37
38// The signals of interest to handle.
39signals_receipts::channel_notify_facility! {
40    SIGINT
41    SIGTERM
42    SIGHUP
43    SIGWINCH
44}
45
46/// Install global signal handlers, with notifications sent to the given
47/// `notify` channel.
48pub fn install(notify: chan::Sender<Signal>) -> io::Result<()> {
49    /// The sender type must implement the facility's trait.
50    #[derive(Debug)]
51    struct ChanSender(chan::Sender<Signal>);
52
53    /// This also does our desired conversion from signal numbers to our
54    /// `Signal` representation.
55    impl channel_notify_facility::Sender for ChanSender {
56        fn send(&self, sig_num: SignalNumber) -> Result<(), SendError> {
57            if let Ok(sig) = sig_num.try_into() {
58                self.0.send(sig).or(Err(SendError::Disconnected))
59            } else {
60                debug_assert!(false, "only called for recognized signal numbers");
61                // Unrecognized signal numbers would be ignored, but
62                // this never occurs.
63                Err(SendError::Ignored)
64            }
65        }
66    }
67
68    SignalsChannel::install_with_outside_channel(ChanSender(notify)).map_err(|e| match e {
69        InstallError::AlreadyInstalled { unused_notify: _ } => io::Error::new(
70            io::ErrorKind::AlreadyExists,
71            "signal handling is already installed",
72        ),
73        _ => io::Error::other(e), // The error type is non-exhaustive.
74    })
75}
76
77/// Uninstall global signal handlers.
78///
79/// The caller must ensure that all `Receiver`s for the other end of the
80/// channel are dropped to disconnect the channel, to ensure that the
81/// internal "signals-receipt" thread wakes to clean-up (in case it's
82/// blocked on sending on the channel).  Such dropping usually occurs
83/// naturally when uninstalling, since the other end is no longer needed
84/// then, and may be done after calling this.  Not doing so might
85/// deadlock the "signals-receipt" thread.
86pub fn uninstall() -> io::Result<()> {
87    SignalsChannel::uninstall_with_outside_channel().map_err(|e| match e {
88        UninstallError::AlreadyUninstalled => io::Error::new(
89            io::ErrorKind::NotFound,
90            "signal handling is already uninstalled",
91        ),
92        #[allow(clippy::unreachable)]
93        UninstallError::WrongMethod => {
94            // SAFETY: Impossible, because `SignalsChannel` is private
95            // and so `SignalsChannel::install()` is never done.
96            unreachable!()
97        }
98        _ => io::Error::other(e), // The error type is non-exhaustive.
99    })
100}
101
102/// Do [`uninstall()`], terminate the internal "signals-receipt"
103/// thread, and wait for that thread to finish.
104///
105/// This is provided in case it's ever needed to completely clean-up the
106/// facility to be like it hadn't been installed before.  It's
107/// unnecessary to use this, just to uninstall the handling.  Usually,
108/// only using `uninstall` is desirable so that the "signals-receipt"
109/// thread is kept alive for faster reuse when re-installing the
110/// handling.
111///
112/// The caller must ensure that all `Receiver`s for the other end of the
113/// channel have **already** been dropped to disconnect the channel,
114/// before calling this, to ensure that the "signals-receipt" thread
115/// wakes (in case it's blocked on sending on the channel) to see that
116/// it must finish.  If this is not done, this might deadlock.
117pub fn finish() -> io::Result<()> {
118    SignalsChannel::finish_with_outside_channel().map_err(|e| match e {
119        FinishError::AlreadyFinished => io::Error::new(
120            io::ErrorKind::NotFound,
121            "signal-handling facility is already finished",
122        ),
123        #[allow(clippy::unreachable)]
124        FinishError::WrongMethod => {
125            // SAFETY: Impossible, because `SignalsChannel` is private
126            // and so `SignalsChannel::install()` is never done.
127            unreachable!()
128        }
129        _ => io::Error::other(e), // The error type is non-exhaustive.
130    })
131}