From 9539fea16e0d8d07faf7edf50d480e0a80c4ccfc Mon Sep 17 00:00:00 2001 From: nsfisis Date: Tue, 11 Aug 2026 12:19:31 +0900 Subject: feat(signal): abort on SIGINT, SIGTERM and SIGHUP at checkpoints The SignalHandler port was a no-op stub, so all four of Composer's abort paths were dead code: nothing removed a half-created project, reverted composer.json, or cleaned up half-installed packages. Composer runs those handlers from pcntl callbacks, which a Rust signal handler cannot do -- it may touch nothing beyond atomics. SignalSubscription records the signal instead, and the abort runs from checkpoints on the normal call stack, where the clean-up can borrow the state it needs. That also resolves the closure-capture TODO(phase-c)s in RequireCommand and InstallationManager, and replaces exit_with_last_signal's exit(0) with the restore-and-re-raise Seld\Signal does. A subscription is live only inside the four abort regions, so elsewhere the signals keep their default disposition and kill the process at once. It is installed without SA_RESTART so a signal interrupts an interactive prompt rather than resuming the read. A signal reaches only the innermost subscription, reproducing SignalHandler's single-stack dispatch. Drop SignalRegistry, SignalableCommandInterface and the Application wiring for them: nothing in Composer reaches that path, and SignalHandler discards whatever they register. Signal handling from plugins and scripts is undefined behavior; see docs/dev/signals.md. Co-Authored-By: Claude Opus 5 (1M context) --- crates/shirabe/src/util/process_executor.rs | 37 +++++++++++++---------------- 1 file changed, 16 insertions(+), 21 deletions(-) (limited to 'crates/shirabe/src/util/process_executor.rs') diff --git a/crates/shirabe/src/util/process_executor.rs b/crates/shirabe/src/util/process_executor.rs index 97f4ed3d..4808d0be 100644 --- a/crates/shirabe/src/util/process_executor.rs +++ b/crates/shirabe/src/util/process_executor.rs @@ -3,6 +3,7 @@ use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::io::io_interface; +use crate::signal::SignalSubscription; use crate::util::GitHub; use crate::util::Platform; use indexmap::IndexMap; @@ -14,7 +15,6 @@ use shirabe_php_shim::{ php_regex, rtrim, str_replace, strcspn, strlen, strpbrk, strtolower, strtr_array, substr_replace, trim, }; -use shirabe_seld_signal::SignalHandler; use shirabe_symfony_process::ExecutableFinder; use shirabe_symfony_process::Process; use shirabe_symfony_process::ProcessMock; @@ -267,22 +267,7 @@ impl ProcessExecutor { // ignore TTY enabling errors } - let io_for_signal = self.io.clone(); - let signal_handler = SignalHandler::create( - vec![ - SignalHandler::SIGINT.to_string(), - SignalHandler::SIGTERM.to_string(), - SignalHandler::SIGHUP.to_string(), - ], - Box::new(move |signal: String, _h: &SignalHandler| { - if let Some(io) = &io_for_signal { - io.write_error(&format!( - "Received {}, aborting when child process is done", - signal - )); - } - }), - ); + let signals = SignalSubscription::new(); let result: anyhow::Result<()> = (|| -> anyhow::Result<()> { match output.to_callback() { @@ -306,23 +291,33 @@ impl ProcessExecutor { self.error_output = process.get_error_output()?; Ok(()) })(); + if signals.is_triggered() + && let Some(io) = &self.io + { + io.write_error3( + &format!( + "Received {}, aborting when child process is done", + signals.last_signal().as_str() + ), + true, + io_interface::DEBUG, + ); + } let final_result: anyhow::Result<()> = match result { Ok(()) => Ok(()), Err(e) => { if let Some(pse) = e.catch::() { - if signal_handler.is_triggered() { + if signals.is_triggered() { // exiting as we were signaled and the child process exited too due to the signal - signal_handler.exit_with_last_signal(); + signals.exit_with_last_signal(); } let _ = pse; Ok(()) } else { - signal_handler.unregister(); return Err(e); } } }; - signal_handler.unregister(); final_result?; Ok(process.get_exit_code()) -- cgit v1.3.1-4-g156e