From 8a6cdc83d160c9133f37f54fc3310f643d2e0b7f Mon Sep 17 00:00:00 2001 From: Sander Date: Wed, 17 Sep 2025 22:54:32 +0200 Subject: [PATCH 1/2] tasks: shut down processes faster Wait on child processes instead of sleeping. This is important to properly reap child processes. Fixes an issue where process-compose would fail to restart the task runner. --- devenv-tasks/src/signal_handler.rs | 42 +++++++++--------------------- devenv-tasks/src/task_state.rs | 25 +++++------------- 2 files changed, 20 insertions(+), 47 deletions(-) diff --git a/devenv-tasks/src/signal_handler.rs b/devenv-tasks/src/signal_handler.rs index ccbb070f6..16bd99b1f 100644 --- a/devenv-tasks/src/signal_handler.rs +++ b/devenv-tasks/src/signal_handler.rs @@ -1,4 +1,4 @@ -use tokio::signal; +use tokio::signal::unix::{signal, SignalKind}; use tokio_util::sync::CancellationToken; use tracing::debug; @@ -17,36 +17,20 @@ impl SignalHandler { let cancellation_token = CancellationToken::new(); let token_clone = cancellation_token.clone(); + let mut sigint = signal(SignalKind::interrupt()).expect("Failed to install SIGINT handler"); + let mut sigterm = signal(SignalKind::terminate()).expect("Failed to install SIGTERM handler"); + let handle = tokio::spawn(async move { - let ctrl_c = signal::ctrl_c(); - - #[cfg(unix)] - { - let mut sigterm = signal::unix::signal(signal::unix::SignalKind::terminate()) - .expect("Failed to install SIGTERM handler"); - - tokio::select! { - _ = ctrl_c => { - debug!("Received SIGINT (Ctrl+C), triggering shutdown..."); - eprintln!("Received SIGINT (Ctrl+C), shutting down gracefully..."); - token_clone.cancel(); - } - _ = sigterm.recv() => { - debug!("Received SIGTERM, triggering shutdown..."); - eprintln!("Received SIGTERM, shutting down gracefully..."); - token_clone.cancel(); - } + tokio::select! { + _ = sigint.recv() => { + debug!("Received SIGINT (Ctrl+C), triggering shutdown..."); + eprintln!("Received SIGINT (Ctrl+C), shutting down gracefully..."); + token_clone.cancel(); } - } - - #[cfg(not(unix))] - { - tokio::select! { - _ = ctrl_c => { - debug!("Received SIGINT (Ctrl+C), triggering shutdown..."); - eprintln!("Received SIGINT (Ctrl+C), shutting down gracefully..."); - token_clone.cancel(); - } + _ = sigterm.recv() => { + debug!("Received SIGTERM, triggering shutdown..."); + eprintln!("Received SIGTERM, shutting down gracefully..."); + token_clone.cancel(); } } }); diff --git a/devenv-tasks/src/task_state.rs b/devenv-tasks/src/task_state.rs index 572a5bfbd..279638ef6 100644 --- a/devenv-tasks/src/task_state.rs +++ b/devenv-tasks/src/task_state.rs @@ -81,10 +81,7 @@ impl TaskState { // Create a new process group for better signal handling // This ensures that signals sent to the parent are propagated to all children - #[cfg(unix)] - { - command.process_group(0); - } + command.process_group(0); // Set DEVENV_TASK_INPUTS if let Some(inputs) = &self.task.inputs { @@ -331,28 +328,20 @@ impl TaskState { eprintln!("Task {} received shutdown signal, terminating child process", self.task.name); // Kill the child process and its process group - #[cfg(unix)] if let Some(pid) = child.id() { use ::nix::sys::signal::{self, Signal}; use ::nix::unistd::Pid; // Send SIGTERM to the process group first for graceful shutdown - let _ = signal::killpg(Pid::from_raw(pid as i32), Signal::SIGTERM); - - // Wait a bit for graceful shutdown - tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; - if child.try_wait().unwrap_or(None).is_none() { - // Force kill if still running - let _ = signal::killpg(Pid::from_raw(pid as i32), Signal::SIGKILL); + signal::killpg(Pid::from_raw(pid as i32), Signal::SIGTERM).ok(); + tokio::select! { + _ = child.wait() => {} + _ = tokio::time::sleep(std::time::Duration::from_secs(5)) => { + child.kill().await.ok(); + } } } - #[cfg(not(unix))] - { - // On non-Unix systems, try to kill the child process directly - let _ = child.kill().await; - } - return Ok(TaskCompleted::Cancelled(now.elapsed())); } result = stdout_reader.next_line(), if !stdout_closed => { From 13cde6765c227fcd74323ed5f54d1f77e9843172 Mon Sep 17 00:00:00 2001 From: Sander Date: Wed, 17 Sep 2025 22:54:32 +0200 Subject: [PATCH 2/2] tasks: implement textbook unix signal handling Do the thing where we re-trigger the default handler to get the correct signal behaviour. --- devenv-tasks/src/main.rs | 9 ++++--- devenv-tasks/src/signal_handler.rs | 42 +++++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/devenv-tasks/src/main.rs b/devenv-tasks/src/main.rs index 6a82a96b2..dddb24d83 100644 --- a/devenv-tasks/src/main.rs +++ b/devenv-tasks/src/main.rs @@ -62,16 +62,19 @@ async fn main() -> Result<(), Box> { run_mode: mode, }; - // Create shared signal handler + // Create a global signal handler let signal_handler = SignalHandler::start(); - let cancellation_token = signal_handler.cancellation_token(); let mut tasks_ui = TasksUi::builder(config, verbosity) - .with_cancellation_token(cancellation_token) + .with_cancellation_token(signal_handler.cancellation_token()) .build() .await?; let (status, _outputs) = tasks_ui.run().await?; + if signal_handler.last_signal().is_some() { + signal_handler.exit_process(); + } + if status.failed + status.dependency_failed > 0 { std::process::exit(1); } diff --git a/devenv-tasks/src/signal_handler.rs b/devenv-tasks/src/signal_handler.rs index 16bd99b1f..d439dfaa4 100644 --- a/devenv-tasks/src/signal_handler.rs +++ b/devenv-tasks/src/signal_handler.rs @@ -1,11 +1,15 @@ -use tokio::signal::unix::{signal, SignalKind}; +use std::sync::atomic::{AtomicI32, Ordering}; +use std::sync::Arc; +use tokio::signal::unix::signal; use tokio_util::sync::CancellationToken; use tracing::debug; +use nix::libc; /// A shared signal handler service that manages signal handling across the entire application. /// This replaces per-task signal handlers with a single, efficient, centralized handler. pub struct SignalHandler { cancellation_token: CancellationToken, + last_signal: Arc, _handle: tokio::task::JoinHandle<()>, } @@ -16,20 +20,31 @@ impl SignalHandler { pub fn start() -> Self { let cancellation_token = CancellationToken::new(); let token_clone = cancellation_token.clone(); + let last_signal = Arc::new(AtomicI32::new(0)); + let last_signal_clone = Arc::clone(&last_signal); - let mut sigint = signal(SignalKind::interrupt()).expect("Failed to install SIGINT handler"); - let mut sigterm = signal(SignalKind::terminate()).expect("Failed to install SIGTERM handler"); + let mut sigint = signal(libc::SIGINT.into()).expect("Failed to install SIGINT handler"); + let mut sigterm = signal(libc::SIGTERM.into()).expect("Failed to install SIGTERM handler"); + let mut sighup = signal(libc::SIGHUP.into()).expect("Failed to install SIGHUP handler"); let handle = tokio::spawn(async move { tokio::select! { _ = sigint.recv() => { debug!("Received SIGINT (Ctrl+C), triggering shutdown..."); eprintln!("Received SIGINT (Ctrl+C), shutting down gracefully..."); + last_signal_clone.store(libc::SIGINT, Ordering::Relaxed); token_clone.cancel(); } _ = sigterm.recv() => { debug!("Received SIGTERM, triggering shutdown..."); eprintln!("Received SIGTERM, shutting down gracefully..."); + last_signal_clone.store(libc::SIGTERM, Ordering::Relaxed); + token_clone.cancel(); + } + _ = sighup.recv() => { + debug!("Received SIGHUP, triggering shutdown..."); + eprintln!("Received SIGHUP, shutting down gracefully..."); + last_signal_clone.store(libc::SIGHUP, Ordering::Relaxed); token_clone.cancel(); } } @@ -37,6 +52,7 @@ impl SignalHandler { Self { cancellation_token, + last_signal, _handle: handle, } } @@ -51,6 +67,26 @@ impl SignalHandler { pub fn is_cancelled(&self) -> bool { self.cancellation_token.is_cancelled() } + + /// Get the last signal that was received, if any. + pub fn last_signal(&self) -> Option { + match self.last_signal.load(Ordering::Relaxed) { + 0 => None, + i => Some(i) + } + } + + /// Restore the default handler for the last received signal and re-raise the signal to terminate with the correct exit code. + pub fn exit_process(&self) -> ! { + let signal = self.last_signal().unwrap_or(libc::SIGTERM); + unsafe { + libc::signal(signal, libc::SIG_DFL); + libc::kill(libc::getpid(), signal); + } + + // Unreachable: something went wrong + std::process::exit(1); + } } impl Drop for SignalHandler {