//! Cleaning up signals.
//!
//! The routines in this module allow resetting the signals of an application back to defaults.
//! This is intended for the following situation:
//!
//! * A terminal signal (eg. a `SIGTERM`, `SIGINT` or something similar) is received.
//! * The application resets the signal handlers to defaults.
//! * The application proceeds to perform some kind of shutdown (saving data, cleaning up, ...).
//! * If another such signal is received, the application is terminated right away the hard way,
//!   without finishing the shutdown.
//!
//! The alternative of leaving the original signals in place might be problematic in case the
//! shutdown takes a long time or when it gets stuck. In such case the application would appear to
//! ignore the signal and just refuse to die.
//!
//! There are two ways to perform the reset:
//! * Registering the reset as part of the signal handlers. This is more reliable (even in case the
//!   application is already stuck in some kind of infinite loop, it would still work). This is
//!   done by [register].
//! * Manually resetting the handlers just before the shutdown. This is done with [cleanup_signal].

use std::io::Error;
#[cfg(not(windows))]
use std::ptr;

use libc::{c_int, sighandler_t, SIG_ERR};

#[cfg(not(windows))]
use libc::SIG_DFL;
// Unfortunately, not exported on windows :-(. Checked this actually works by tests/default.rs.
#[cfg(windows)]
const SIG_DFL: sighandler_t = 0;

pub use signal_hook_registry::unregister_signal;

use SigId;

/// Resets the signal handler to the default one.
///
/// This is the lowest level wrapper around resetting the signal handler to the OS default. It
/// doesn't remove the hooks (though they will not get called), it doesn't handle errors and it
/// doesn't return any previous chained signals. The hooks will simply stay registered but dormant.
///
/// This function is async-signal-safe. However, you might prefer to use either [cleanup_signal] or
/// [register].
///
/// # Warning
///
/// This action is irreversible, once called, registering more hooks for the same signal will have
/// no effect (neither the old nor the new ones will be active but the registration will appear to
/// have succeeded).
///
/// This behaviour **can change** in future versions without considering it a breaking change.
///
/// In other words, this is expected to be called only before terminating the application and no
/// further manipulation of the given signal is supported in any way. While it won't cause UB, it
/// *will* produce unexpected results.
pub fn cleanup_raw(signal: c_int) -> sighandler_t {
    unsafe { ::libc::signal(signal, SIG_DFL) }
}

/// Resets the signal handler to the default one and removes all its hooks.
///
/// This resets the signal to the OS default. It doesn't revert to calling any previous signal
/// handlers (the ones not handled by `signal-hook`). All the hooks registered for this signal are
/// removed.
///
/// The intended use case is making sure further instances of a terminal signal have immediate
/// effect. If eg. a CTRL+C is pressed, the application removes all signal handling and proceeds to
/// its own shutdown phase. If the shutdown phase takes too long or gets stuck, the user may press
/// CTRL+C again which will then kill the application immediately, by a default signal action.
///
/// # Warning
///
/// This action is *global* (affecting hooks some other library or unrelated part of program
/// registered) and *irreversible*. Once called, registering new hooks for this signal has no
/// further effect (they'll appear to be registered, but they won't be called by the signal). The
/// latter may change in the future and it won't be considered a breaking change.
///
/// In other words, this is expected to be called only once the application enters its terminal
/// state and is not supported otherwise.
///
/// The function is **not** async-signal-safe. See [register] and [cleanup_raw] if you intend to
/// reset the signal directly from inside the signal handler itself.
///
/// # Examples
///
/// ```rust
/// # extern crate libc;
/// # extern crate signal_hook;
/// #
/// # use std::io::Error;
/// # use std::sync::atomic::{AtomicBool, Ordering};
/// # use std::sync::Arc;
/// #
/// # fn keep_processing() { std::thread::sleep(std::time::Duration::from_millis(50)); }
/// # fn app_cleanup() {}
/// use signal_hook::{cleanup, flag, SIGTERM};
///
/// fn main() -> Result<(), Error> {
///     let terminated = Arc::new(AtomicBool::new(false));
///     flag::register(SIGTERM, Arc::clone(&terminated))?;
/// #   unsafe { libc::raise(SIGTERM) };
///
///     while !terminated.load(Ordering::Relaxed) {
///         keep_processing();
///     }
///
///     cleanup::cleanup_signal(SIGTERM)?;
///     app_cleanup();
///     Ok(())
/// }
/// ```
pub fn cleanup_signal(signal: c_int) -> Result<(), Error> {
    // We use `signal` both on unix and windows here. Unlike with regular functions, usage of
    // SIG_DFL is portable and much more convenient to use.
    let result = cleanup_raw(signal);
    // The cast is needed on windows :-|.
    if result == SIG_ERR as _ {
        return Err(Error::last_os_error());
    }
    unregister_signal(signal);
    Ok(())
}

#[cfg(not(windows))]
fn verify_signals_exist(signals: &[c_int]) -> Result<(), Error> {
    signals
        .iter()
        .map(|s| -> Result<(), Error> {
            if unsafe { ::libc::sigaction(*s, ptr::null(), ptr::null_mut()) } == -1 {
                Err(Error::last_os_error())
            } else {
                Ok(())
            }
        })
        .collect()
}

#[cfg(windows)]
fn verify_signals_exist(_: &[c_int]) -> Result<(), Error> {
    // TODO: Do we have a way to check if the signals are valid on windows too?
    Ok(())
}

/// Register a cleanup after receiving a signal.
///
/// Registers an action that, after receiving `signal`, will reset all signals specified in
/// `cleanup` to their OS defaults. The reset is done as part of the signal handler.
///
/// The intended use case is that at CTRL+C (or something similar), the application starts shutting
/// down. This might take some time so by resetting all terminal signals to the defaults at that
/// time makes sure a second CTRL+C results in immediate (hard) termination of the application.
///
/// The hooks are still left inside and any following hooks after the reset are still run. Only the
/// next signal will be affected (and the hooks will be inert).
///
/// # Warning
///
/// The reset as part of the action is *global* and *irreversible*. All signal hooks and all
/// signals registered outside of `signal-hook` are affected and won't be run any more. Registering
/// more hooks for the same signals as cleaned will have no effect.
///
/// The latter part of having no effect may be changed in the future, do not rely on it.
/// Preferably, don't manipulate the signal any longer.
///
/// # Examples
///
/// ```rust
/// # extern crate libc;
/// # extern crate signal_hook;
/// #
/// # use std::io::Error;
/// # use std::sync::atomic::{AtomicBool, Ordering};
/// # use std::sync::Arc;
/// #
/// # fn keep_processing() { std::thread::sleep(std::time::Duration::from_millis(50)); }
/// # fn app_cleanup() {}
/// use signal_hook::{cleanup, flag, SIGINT, SIGTERM};
///
/// fn main() -> Result<(), Error> {
///     let terminated = Arc::new(AtomicBool::new(false));
///     flag::register(SIGTERM, Arc::clone(&terminated))?;
///     cleanup::register(SIGTERM, vec![SIGTERM, SIGINT])?;
/// #   unsafe { libc::raise(SIGTERM) };
///
///     while !terminated.load(Ordering::Relaxed) {
///         keep_processing();
///     }
///
///     app_cleanup();
///     Ok(())
/// }
/// ```
pub fn register(signal: c_int, cleanup: Vec<c_int>) -> Result<SigId, Error> {
    verify_signals_exist(&cleanup)?;
    let hook = move || {
        for sig in &cleanup {
            // Note: we are ignoring the errors here. We have no way to handle them and the only
            // possible ones are invalid signals ‒ which we should have handled by
            // verify_signals_exist above.
            cleanup_raw(*sig);
        }
    };
    unsafe { ::register(signal, hook) }
}
