From 31606a8fe1964d6c5af47fe24173dcdaab84dc59 Mon Sep 17 00:00:00 2001 From: David Yamnitsky Date: Wed, 13 Aug 2025 10:11:40 -0700 Subject: [PATCH] Kill background jobs on interrupt (#16285) # Description This PR kills all background jobs on interrupt, as a fix for https://github.com/nushell/nushell/issues/15947. # User-Facing Changes If you run the following: `job spawn { print "job spawned"; ^sleep infinity }; ^sleep infinity`, then hit ctrl-c, the current behavior is that the `sleep` process from the job will not be killed, it will reparented to init. With this change, the process will be killed on ctrl-c. # Tests + Formatting I was unsure of the best way to write a test for this. # After Submitting --------- Co-authored-by: 132ikl <132@ikl.sh> --- crates/nu-protocol/src/pipeline/handlers.rs | 13 +++++++++++++ src/signals.rs | 15 +++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/crates/nu-protocol/src/pipeline/handlers.rs b/crates/nu-protocol/src/pipeline/handlers.rs index 202eb0a12e..75df29c488 100644 --- a/crates/nu-protocol/src/pipeline/handlers.rs +++ b/crates/nu-protocol/src/pipeline/handlers.rs @@ -68,6 +68,19 @@ impl Handlers { }) } + /// Registers a new handler which persists for the entire process lifetime. + /// + /// Only use this for handlers which should exist for the lifetime of the program. + /// You should prefer to use `register` with a `HandlerGuard` when possible. + pub fn register_unguarded(&self, handler: Handler) -> Result<(), ShellError> { + let id = self.next_id.next()?; + if let Ok(mut handlers) = self.handlers.lock() { + handlers.push((id, handler)); + } + + Ok(()) + } + /// Runs all registered handlers. pub fn run(&self, action: SignalAction) { if let Ok(handlers) = self.handlers.lock() { diff --git a/src/signals.rs b/src/signals.rs index 323231e14e..c6b8c8190a 100644 --- a/src/signals.rs +++ b/src/signals.rs @@ -9,6 +9,21 @@ pub(crate) fn ctrlc_protection(engine_state: &mut EngineState) { engine_state.set_signals(Signals::new(interrupt.clone())); let signal_handlers = Handlers::new(); + + // Register a handler to kill all background jobs on interrupt. + signal_handlers + .register_unguarded({ + let jobs = engine_state.jobs.clone(); + Box::new(move |action| { + if action == SignalAction::Interrupt { + if let Ok(mut jobs) = jobs.lock() { + let _ = jobs.kill_all(); + } + } + }) + }) + .expect("Failed to register interrupt signal handler"); + engine_state.signal_handlers = Some(signal_handlers.clone()); ctrlc::set_handler(move || {