//! Shared signal handling utilities //! //! This module provides unified signal handling for background services, //! eliminating duplicate signal handling code across different services. use tokio::signal; use tracing::{debug, error, info}; /// Create a unified shutdown signal handler that listens to multiple sources /// /// This function provides consistent signal handling across all services: /// - Ctrl+C (SIGINT) /// - SIGTERM (on Unix platforms) /// - Manual shutdown signals /// /// # Example /// ```rust,no_run /// use voice_cli::utils::signal_handling::create_shutdown_signal; /// use tracing::info; /// /// # async fn example() { /// let shutdown_signal = create_shutdown_signal(); /// tokio::select! { /// _ = async { /* service_work */ } => {}, /// _ = shutdown_signal => { /// info!("Received shutdown signal, stopping service"); /// } /// } /// # } /// ``` pub async fn create_shutdown_signal() { handle_system_signals().await } /// Handle system signals for graceful shutdown /// /// This provides the core signal handling logic used by all services. /// Separated into its own function for reusability and testing. pub async fn handle_system_signals() { let ctrl_c = async { if let Err(e) = signal::ctrl_c().await { error!("Failed to listen for Ctrl+C: {}", e); } else { info!("Received Ctrl+C signal"); } }; #[cfg(unix)] let terminate = async { use tokio::signal::unix::{SignalKind, signal}; match signal(SignalKind::terminate()) { Ok(mut stream) => { stream.recv().await; info!("Received SIGTERM signal"); } Err(e) => { error!("Failed to listen for SIGTERM: {}", e); } } }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => debug!("Ctrl+C signal handled"), _ = terminate => debug!("SIGTERM signal handled"), } } /// Create a combined shutdown signal that listens to both system signals and manual channels /// /// This is useful for services that need to respond to both system signals (Ctrl+C, SIGTERM) /// and programmatic shutdown requests via channels. /// /// # Arguments /// * `manual_shutdown` - A future that completes when manual shutdown is requested /// /// # Example /// ```rust,no_run /// use voice_cli::utils::signal_handling::create_combined_shutdown_signal; /// use tracing::info; /// /// # async fn example() { /// let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>(); /// let shutdown_signal = create_combined_shutdown_signal(async { /// shutdown_rx.await.ok(); /// }); /// /// tokio::select! { /// _ = async { /* service_work */ } => {}, /// _ = shutdown_signal => { /// info!("Received shutdown signal"); /// } /// } /// # } /// ``` pub async fn create_combined_shutdown_signal(manual_shutdown: F) where F: std::future::Future, { tokio::select! { _ = handle_system_signals() => { info!("Received system shutdown signal"); } _ = manual_shutdown => { info!("Received manual shutdown signal"); } } } /// Create a shutdown signal with service-specific logging /// /// This provides service-specific logging messages for better debugging. /// /// # Arguments /// * `service_name` - Name of the service for logging context /// /// # Example /// ```rust,no_run /// use voice_cli::utils::signal_handling::create_service_shutdown_signal; /// /// # async fn example() { /// let shutdown_signal = create_service_shutdown_signal("http-server"); /// tokio::select! { /// _ = async { /* service_work */ } => {}, /// _ = shutdown_signal => {} /// } /// # } /// ``` pub async fn create_service_shutdown_signal(service_name: &str) { let ctrl_c = async { if let Err(e) = signal::ctrl_c().await { error!("Failed to listen for Ctrl+C in {}: {}", service_name, e); } else { info!("{} received Ctrl+C signal", service_name); } }; #[cfg(unix)] let terminate = async { use tokio::signal::unix::{SignalKind, signal}; match signal(SignalKind::terminate()) { Ok(mut stream) => { stream.recv().await; info!("{} received SIGTERM signal", service_name); } Err(e) => { error!("Failed to listen for SIGTERM in {}: {}", service_name, e); } } }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => debug!("{} handled Ctrl+C signal", service_name), _ = terminate => debug!("{} handled SIGTERM signal", service_name), } } #[cfg(test)] mod tests { use super::*; use tokio::time::{Duration, timeout}; #[tokio::test] async fn test_signal_handling_timeout() { // Test that the signal handlers don't hang indefinitely let result = timeout(Duration::from_millis(100), create_shutdown_signal()).await; // Should timeout since no signals are sent assert!(result.is_err()); } #[tokio::test] async fn test_combined_shutdown_manual() { let (tx, rx) = tokio::sync::oneshot::channel::<()>(); // Start the combined signal handler let signal_future = create_combined_shutdown_signal(async move { rx.await.ok(); }); // Send manual shutdown signal let _ = tx.send(()); // Should complete quickly due to manual signal let result = timeout(Duration::from_millis(100), signal_future).await; assert!(result.is_ok()); } #[tokio::test] async fn test_service_shutdown_signal_timeout() { // Test service-specific signal handling let result = timeout( Duration::from_millis(100), create_service_shutdown_signal("test-service"), ) .await; // Should timeout since no signals are sent assert!(result.is_err()); } }