breakmancer 0.9.0

Drop a breakpoint into any shell.
Documentation
//! Supports managed suppression of interrupt signals.
//!
//! When the user presses ctrl-c when interacting with a CLI, the
//! application receives an interrupt signal (SIGINT). Applications by
//! default do not handle this signal, and therefore are terminated
//! instead.
//!
//! If you want to explicitly handle this signal, you can register a
//! handler, but then ctrl-c will not terminate your application
//! unless your handler explicitly invokes `exit`. This is then the
//! opposite problem of the original one; rather than not being able
//! to handle ctrl-c, you have to always handle it. Failing to do so
//! means the user will not be able to terminate your CLI app without
//! using annoying and non-obvious workarounds such as backgrounding
//! or finding the process ID.
//!
//! The `Manager` struct takes the approach of registering a handler
//! that will `exit(0)` whenever a SIGINT is received unless one or
//! more suppressors are alive. Suppressors can be created and dropped
//! at any time, and multiple may be alive concurrently.
//!
//! # Examples
//!
//! ```
//! use breakmancer::sigint::Manager;
//!
//! let mut sigint_manager = Manager::setup(); // one-time setup
//!
//! let suppress = sigint_manager.suppress();
//! // ...more code here...
//! let interrupt_fired = suppress.release();
//! ```
//!
//! The code that runs while suppression is in effect should be
//! written to avoid any chance of deadlock or indefinite blocking, as
//! that will leave the user with no easy way to terminate a hung
//! application.
//!
//! [`SuppressorHandle::was_signaled`] can be queried at any time to
//! determine whether a SIGINT has occurred since the start of this
//! suppression (or the last call of the function). Additionally, the
//! return value of [`SuppressorHandle::release`] gives a final
//! opportunity to learn whether another SIGINT has occurred.

use std::sync::atomic::{AtomicBool, Ordering::Relaxed};
use std::sync::{Arc, Mutex, Weak};
use std::thread;

use signal_hook::consts::SIGINT;
use signal_hook::iterator::Signals;

/// The core of a [`SuppressorHandle`]. Dropping this releases the
/// suppression.
struct Suppressor {
    /// True when a signal has arrived since the last check.
    new_signal: AtomicBool,
}

impl Suppressor {
    /// Record that a SIGINT has been received.
    fn signal_received(&self) {
        self.new_signal.store(true, Relaxed);
    }
}

/// Keeps ^C from exiting the application.
///
/// Drop or call [`SuppressorHandle::release`] to remove this suppression.
pub struct SuppressorHandle {
    /// We use Arc here to share this with the manager (which only
    /// keeps a Weak Arc.)
    state: Arc<Suppressor>,
}

impl SuppressorHandle {
    /// Create suppression handle. This doesn't do anything until the
    /// manager registers it.
    fn new() -> SuppressorHandle {
        SuppressorHandle {
            state: Arc::new(Suppressor {
                new_signal: AtomicBool::new(false),
            }),
        }
    }

    /// Return whether signal has arrived since previous call.
    pub fn was_signaled(&self) -> bool {
        self.state.new_signal.swap(false, Relaxed)
    }

    /// Releases the suppression (via drop) and returns final signal state.
    ///
    /// Approximately equivalent to calling [`SuppressorHandle::was_signaled`] and then
    /// dropping the handle.
    pub fn release(self) -> bool {
        self.was_signaled()
    }
}

type Suppressors = Arc<Mutex<Vec<Weak<Suppressor>>>>;

/// SIGINT manager.
///
/// Setting up the manager ensures that the interrupt signal will be
/// converted to an application exit, but also that there is a way to
/// suppress this behavior (via [`Manager::suppress`]).
pub struct Manager {
    suppressors: Suppressors,
}

/// Core of [`Manager::setup`].
///
/// Spawns a thread that will monitor for SIGINT and exit if there are
/// no extant suppressors.
fn enable_suppression(suppressors: Suppressors) {
    thread::spawn(move || {
        let mut signals = Signals::new([SIGINT])
            .map_err(|err| format!("Couldn't register interrupt handler: {err}"))
            .unwrap();

        for _signal in signals.forever() {
            let mut suppressors = suppressors.lock().unwrap();
            if suppressors.is_empty() {
                std::process::exit(0)
            } else {
                let mut remaining: Vec<Weak<Suppressor>> = vec![];
                for weak in suppressors.iter() {
                    if let Some(s) = weak.upgrade() {
                        s.signal_received();
                        // Only keep the weak refs that are still valid
                        remaining.push(weak.clone());
                    }
                }
                *suppressors = remaining;
            }
        }
    });
}

impl Manager {
    /// Start handling SIGINT suppression and provide default (exit)
    /// response when no suppressors exist.
    pub fn setup() -> Manager {
        let suppressors = Arc::new(Mutex::new(Vec::<Weak<Suppressor>>::new()));
        enable_suppression(suppressors.clone());
        Manager { suppressors }
    }

    /// Suppress the interrupt handler while the returned handle is alive.
    pub fn suppress(&mut self) -> SuppressorHandle {
        let handle = SuppressorHandle::new();
        let suppressor = handle.state.clone();
        self.suppressors
            .lock()
            .unwrap()
            .push(Arc::downgrade(&suppressor));
        handle
    }
}