use std::sync::atomic::{AtomicBool, Ordering};
#[allow(unused_imports)]
use tracing::{debug, info, warn};
use crate::ShutdownReason;
use crate::error::{Error, Result};
use crate::shutdown::ShutdownCoordinator;
#[derive(Debug)]
pub struct SignalHandler {
#[allow(dead_code)]
shutdown_coordinator: ShutdownCoordinator,
handling_signals: AtomicBool,
}
impl SignalHandler {
#[must_use]
pub const fn new(shutdown_coordinator: ShutdownCoordinator) -> Self {
Self {
shutdown_coordinator,
handling_signals: AtomicBool::new(false),
}
}
pub async fn handle_signals(&self) -> Result<()> {
if self
.handling_signals
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
return Err(Error::invalid_state("Signal handling already started"));
}
info!("Starting signal handler");
#[cfg(unix)]
{
return self.handle_unix_signals().await;
}
#[cfg(windows)]
{
return self.handle_windows_signals().await;
}
}
pub fn stop(&self) {
self.handling_signals.store(false, Ordering::Release);
debug!("Signal handling stopped");
}
pub fn is_handling(&self) -> bool {
self.handling_signals.load(Ordering::Acquire)
}
}
#[cfg(unix)]
impl SignalHandler {
async fn handle_unix_signals(&self) -> Result<()> {
#[cfg(all(feature = "tokio", not(feature = "async-std")))]
{
return self.handle_unix_signals_tokio().await;
}
#[cfg(all(feature = "async-std", not(feature = "tokio")))]
{
return self.handle_unix_signals_async_std().await;
}
#[cfg(not(any(feature = "tokio", feature = "async-std")))]
{
return Err(Error::runtime_with_code(
crate::error::ErrorCode::MissingRuntime,
"No runtime available for signal handling",
));
}
#[cfg(all(feature = "tokio", feature = "async-std"))]
{
return self.handle_unix_signals_tokio().await;
}
}
#[cfg(feature = "tokio")]
async fn handle_unix_signals_tokio(&self) -> Result<()> {
use tokio::signal::unix::{signal, SignalKind};
let mut sigterm = signal(SignalKind::terminate()).map_err(|e| {
Error::signal_with_number(format!("Failed to register SIGTERM handler: {e}"), 15)
})?;
let mut sigint = signal(SignalKind::interrupt()).map_err(|e| {
Error::signal_with_number(format!("Failed to register SIGINT handler: {e}"), 2)
})?;
let mut sigquit = signal(SignalKind::quit()).map_err(|e| {
Error::signal_with_number(format!("Failed to register SIGQUIT handler: {e}"), 3)
})?;
let mut sighup = signal(SignalKind::hangup()).map_err(|e| {
Error::signal_with_number(format!("Failed to register SIGHUP handler: {e}"), 1)
})?;
info!("Unix signal handlers registered (SIGTERM, SIGINT, SIGQUIT, SIGHUP)");
loop {
tokio::select! {
_ = sigterm.recv() => {
info!("Received SIGTERM, initiating graceful shutdown");
if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(15)) {
break;
}
}
_ = sigint.recv() => {
info!("Received SIGINT (Ctrl+C), initiating graceful shutdown");
if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(2)) {
break;
}
}
_ = sigquit.recv() => {
warn!("Received SIGQUIT, initiating immediate shutdown");
if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(3)) {
break;
}
}
_ = sighup.recv() => {
info!("Received SIGHUP, could be used for config reload (initiating shutdown for now)");
if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(1)) {
break;
}
}
}
if !self.is_handling() {
debug!("Signal handling stopped by request");
break;
}
}
Ok(())
}
#[cfg(all(feature = "async-std", not(feature = "tokio")))]
async fn handle_unix_signals_async_std(&self) -> Result<()> {
use futures::stream::StreamExt;
use signal_hook::consts::{SIGHUP, SIGINT, SIGQUIT, SIGTERM};
use signal_hook_async_std::Signals;
let mut signals = Signals::new([SIGTERM, SIGINT, SIGQUIT, SIGHUP])
.map_err(|e| Error::signal(format!("Failed to register Unix signal handlers: {e}")))?;
info!("Unix signal handlers registered (SIGTERM, SIGINT, SIGQUIT, SIGHUP)");
while self.is_handling() {
if let Some(signal) = signals.next().await {
match signal {
SIGTERM => {
info!("Received SIGTERM, initiating graceful shutdown");
if self
.shutdown_coordinator
.initiate_shutdown(ShutdownReason::Signal(15))
{
break;
}
}
SIGINT => {
info!("Received SIGINT (Ctrl+C), initiating graceful shutdown");
if self
.shutdown_coordinator
.initiate_shutdown(ShutdownReason::Signal(2))
{
break;
}
}
SIGQUIT => {
warn!("Received SIGQUIT, initiating immediate shutdown");
if self
.shutdown_coordinator
.initiate_shutdown(ShutdownReason::Signal(3))
{
break;
}
}
SIGHUP => {
info!("Received SIGHUP, could be used for config reload (initiating shutdown for now)");
if self
.shutdown_coordinator
.initiate_shutdown(ShutdownReason::Signal(1))
{
break;
}
}
_ => {}
}
}
}
Ok(())
}
}
#[cfg(windows)]
impl SignalHandler {
async fn handle_windows_signals(&self) -> Result<()> {
#[cfg(feature = "tokio")]
{
self.handle_windows_signals_tokio().await
}
#[cfg(all(feature = "async-std", not(feature = "tokio")))]
{
self.handle_windows_signals_async_std().await
}
}
#[cfg(feature = "tokio")]
async fn handle_windows_signals_tokio(&self) -> Result<()> {
use tokio::signal::windows::{ctrl_break, ctrl_c, ctrl_close, ctrl_shutdown};
let mut ctrl_c_stream = ctrl_c()
.map_err(|e| Error::signal(format!("Failed to register Ctrl+C handler: {e}")))?;
let mut ctrl_break_stream = ctrl_break()
.map_err(|e| Error::signal(format!("Failed to register Ctrl+Break handler: {e}")))?;
let mut ctrl_close_stream = ctrl_close()
.map_err(|e| Error::signal(format!("Failed to register Ctrl+Close handler: {e}")))?;
let mut ctrl_shutdown_stream = ctrl_shutdown()
.map_err(|e| Error::signal(format!("Failed to register shutdown handler: {e}")))?;
info!("Windows console event handlers registered");
loop {
tokio::select! {
_ = ctrl_c_stream.recv() => {
info!("Received Ctrl+C, initiating graceful shutdown");
if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(2)) {
break;
}
}
_ = ctrl_break_stream.recv() => {
info!("Received Ctrl+Break, initiating graceful shutdown");
if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(3)) {
break;
}
}
_ = ctrl_close_stream.recv() => {
warn!("Received console close event, initiating immediate shutdown");
if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(1)) {
break;
}
}
_ = ctrl_shutdown_stream.recv() => {
warn!("Received system shutdown event, initiating immediate shutdown");
if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(6)) {
break;
}
}
}
if !self.is_handling() {
debug!("Signal handling stopped by request");
break;
}
}
Ok(())
}
#[cfg(all(feature = "async-std", not(feature = "tokio")))]
async fn handle_windows_signals_async_std(&self) -> Result<()> {
use std::sync::atomic::{AtomicBool, Ordering};
let shutdown_flag = Arc::new(AtomicBool::new(false));
let shutdown_flag_clone = Arc::clone(&shutdown_flag);
ctrlc::set_handler(move || {
shutdown_flag_clone.store(true, Ordering::Release);
})
.map_err(|e| Error::signal(format!("Failed to set Ctrl+C handler: {}", e)))?;
info!("Windows Ctrl+C handler registered");
while self.is_handling() && !shutdown_flag.load(Ordering::Acquire) {
async_std::task::sleep(std::time::Duration::from_millis(100)).await;
}
if shutdown_flag.load(Ordering::Acquire) {
info!("Received Windows console event, initiating graceful shutdown");
self.shutdown_coordinator
.initiate_shutdown(ShutdownReason::Signal(2));
}
Ok(())
}
}
#[cfg(all(unix, feature = "async-std", not(feature = "tokio")))]
#[allow(dead_code)]
#[allow(clippy::missing_const_for_fn)]
extern "C" fn handle_signal(_signal: libc::c_int) {
}
#[must_use]
pub const fn signal_description(signal: i32) -> &'static str {
match signal {
1 => "SIGHUP (Hangup)",
2 => "SIGINT (Interrupt/Ctrl+C)",
3 => "SIGQUIT (Quit)",
6 => "SIGABRT (Abort)",
9 => "SIGKILL (Kill - non-catchable)",
15 => "SIGTERM (Terminate)",
_ => "Unknown signal",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SignalHandling {
Enabled,
#[default]
Disabled,
}
impl From<bool> for SignalHandling {
fn from(value: bool) -> Self {
if value {
Self::Enabled
} else {
Self::Disabled
}
}
}
impl From<SignalHandling> for bool {
fn from(value: SignalHandling) -> Self {
match value {
SignalHandling::Enabled => true,
SignalHandling::Disabled => false,
}
}
}
#[derive(Debug, Clone)]
pub struct SignalConfig {
pub term: SignalHandling,
pub interrupt: SignalHandling,
pub quit: SignalHandling,
pub hangup: SignalHandling,
pub user1: SignalHandling,
pub user2: SignalHandling,
pub custom_handlers: Vec<(i32, String)>,
}
impl Default for SignalConfig {
fn default() -> Self {
Self {
term: SignalHandling::Enabled,
interrupt: SignalHandling::Enabled,
quit: SignalHandling::Enabled,
hangup: SignalHandling::Disabled, user1: SignalHandling::Disabled,
user2: SignalHandling::Disabled,
custom_handlers: Vec::with_capacity(4),
}
}
}
impl SignalConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn with_sighup(mut self) -> Self {
self.hangup = SignalHandling::Enabled;
self
}
#[must_use]
pub const fn with_sigusr1(mut self) -> Self {
self.user1 = SignalHandling::Enabled;
self
}
#[must_use]
pub const fn with_sigusr2(mut self) -> Self {
self.user2 = SignalHandling::Enabled;
self
}
#[must_use]
pub fn with_custom_handler<S: Into<String>>(mut self, signal: i32, description: S) -> Self {
self.custom_handlers.push((signal, description.into()));
self
}
#[must_use]
pub const fn without_sigint(mut self) -> Self {
self.interrupt = SignalHandling::Disabled;
self
}
#[must_use]
pub const fn without_sigterm(mut self) -> Self {
self.term = SignalHandling::Disabled;
self
}
#[must_use]
pub const fn without_sigquit(mut self) -> Self {
self.quit = SignalHandling::Disabled;
self
}
}
#[derive(Debug)]
pub struct ConfigurableSignalHandler {
#[allow(dead_code)]
shutdown_coordinator: ShutdownCoordinator,
config: SignalConfig,
handling_signals: AtomicBool,
}
impl ConfigurableSignalHandler {
#[must_use]
pub const fn new(shutdown_coordinator: ShutdownCoordinator, config: SignalConfig) -> Self {
Self {
shutdown_coordinator,
config,
handling_signals: AtomicBool::new(false),
}
}
pub async fn handle_signals(&self) -> Result<()> {
if self
.handling_signals
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
return Err(Error::invalid_state("Signal handling already started"));
}
info!("Starting configurable signal handler");
let mut handled_signals = Vec::with_capacity(6); if bool::from(self.config.term) {
handled_signals.push("SIGTERM");
}
if bool::from(self.config.interrupt) {
handled_signals.push("SIGINT");
}
if bool::from(self.config.quit) {
handled_signals.push("SIGQUIT");
}
if bool::from(self.config.hangup) {
handled_signals.push("SIGHUP");
}
if bool::from(self.config.user1) {
handled_signals.push("SIGUSR1");
}
if bool::from(self.config.user2) {
handled_signals.push("SIGUSR2");
}
info!("Handling signals: {:?}", handled_signals);
#[cfg(unix)]
{
self.handle_configured_unix_signals().await
}
#[cfg(windows)]
{
self.handle_configured_windows_signals().await
}
}
#[cfg(unix)]
async fn handle_configured_unix_signals(&self) -> Result<()> {
#[cfg(feature = "tokio")]
{
use tokio::signal::unix::{signal, SignalKind};
if bool::from(self.config.term) || bool::from(self.config.interrupt) {
let mut sigterm = signal(SignalKind::terminate())?;
let mut sigint = signal(SignalKind::interrupt())?;
loop {
tokio::select! {
_ = sigterm.recv(), if bool::from(self.config.term) => {
info!("Received SIGTERM, initiating graceful shutdown");
if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(15)) {
break;
}
}
_ = sigint.recv(), if bool::from(self.config.interrupt) => {
info!("Received SIGINT, initiating graceful shutdown");
if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(2)) {
break;
}
}
}
if !self.handling_signals.load(Ordering::Acquire) {
break;
}
}
}
}
#[cfg(all(feature = "async-std", not(feature = "tokio")))]
{
while self.handling_signals.load(Ordering::Acquire) {
async_std::task::sleep(std::time::Duration::from_millis(100)).await;
}
}
Ok(())
}
#[cfg(windows)]
async fn handle_configured_windows_signals(&self) -> Result<()> {
#[cfg(feature = "tokio")]
{
use tokio::signal::windows::{ctrl_break, ctrl_c};
let mut ctrl_c_stream = ctrl_c()?;
let mut ctrl_break_stream = ctrl_break()?;
loop {
tokio::select! {
_ = ctrl_c_stream.recv() => {
info!("Received Ctrl+C, initiating graceful shutdown");
if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(2)) {
break;
}
}
_ = ctrl_break_stream.recv() => {
info!("Received Ctrl+Break, initiating graceful shutdown");
if self.shutdown_coordinator.initiate_shutdown(ShutdownReason::Signal(3)) {
break;
}
}
}
if !self.handling_signals.load(Ordering::Acquire) {
break;
}
}
}
#[cfg(all(feature = "async-std", not(feature = "tokio")))]
{
while self.handling_signals.load(Ordering::Acquire) {
async_std::task::sleep(std::time::Duration::from_millis(100)).await;
}
}
Ok(())
}
pub fn stop(&self) {
self.handling_signals.store(false, Ordering::Release);
debug!("Configurable signal handling stopped");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shutdown::ShutdownCoordinator;
use std::time::Duration;
#[test]
fn test_signal_description() {
assert_eq!(signal_description(15), "SIGTERM (Terminate)");
assert_eq!(signal_description(2), "SIGINT (Interrupt/Ctrl+C)");
assert_eq!(signal_description(999), "Unknown signal");
}
#[test]
fn test_signal_config() {
let config = SignalConfig::new()
.with_sighup()
.with_custom_handler(12, "Custom signal")
.without_sigint();
assert_eq!(config.interrupt, SignalHandling::Disabled);
assert_eq!(config.term, SignalHandling::Enabled);
assert_eq!(config.hangup, SignalHandling::Enabled);
assert_eq!(config.custom_handlers.len(), 1);
assert_eq!(config.custom_handlers[0].0, 12);
}
#[cfg(feature = "tokio")]
#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn test_signal_handler_creation() {
let test_result = tokio::time::timeout(Duration::from_secs(5), async {
let coordinator = ShutdownCoordinator::new(5000, 10000, 15000);
let handler = SignalHandler::new(coordinator);
assert!(!handler.is_handling());
})
.await;
assert!(test_result.is_ok(), "Test timed out after 5 seconds");
}
#[cfg(all(feature = "async-std", not(feature = "tokio")))]
#[async_std::test]
async fn test_signal_handler_creation() {
let test_result = async_std::future::timeout(Duration::from_secs(5), async {
let coordinator = ShutdownCoordinator::new(5000, 10000, 15000);
let handler = SignalHandler::new(coordinator);
assert!(!handler.is_handling());
})
.await;
assert!(test_result.is_ok(), "Test timed out after 5 seconds");
}
}