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/console/application.rs | 63 +++---------------------------- 1 file changed, 5 insertions(+), 58 deletions(-) (limited to 'crates/shirabe/src/console/application.rs') diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs index c0cd072b..197ddfbd 100644 --- a/crates/shirabe/src/console/application.rs +++ b/crates/shirabe/src/console/application.rs @@ -69,7 +69,6 @@ use shirabe_seld_json_lint::ParsingException; use shirabe_symfony_console::application::Application as BaseApplication; use shirabe_symfony_console::command::Command as SymfonyCommand; use shirabe_symfony_console::command::HelpCommand; -use shirabe_symfony_console::command::SignalableCommandInterface; use shirabe_symfony_console::command_loader::CommandLoaderInterface; use shirabe_symfony_console::completion::CompletionInput; use shirabe_symfony_console::completion::CompletionSuggestions; @@ -94,7 +93,6 @@ use shirabe_symfony_console::input::InputOption; use shirabe_symfony_console::output::ConsoleOutput; use shirabe_symfony_console::output::ConsoleOutputInterface; use shirabe_symfony_console::output::{OutputInterface, output_interface}; -use shirabe_symfony_console::signal_registry::SignalRegistry; use shirabe_symfony_console::style::StyleInterface; use shirabe_symfony_console::style::SymfonyStyle; use shirabe_symfony_console::terminal::Terminal; @@ -103,6 +101,10 @@ use shirabe_symfony_process::exception::ProcessTimedOutException; /// The PHP `Composer\Console\Application` and `Symfony\Component\Console\Application` are /// flattened into a single struct. Methods that are overridden by subclass and called via /// `parent::` are prefixed by `base_`. +/// +/// Symfony's signal dispatch — `$signalRegistry`, `$signalsToDispatchEvent`, and the +/// `SignalableCommandInterface` branch of `doRunCommand` — is unported. See +/// `docs/dev/signals.md`. #[derive(Debug)] pub struct Application { commands: IndexMap>>, @@ -117,8 +119,6 @@ pub struct Application { terminal: Terminal, default_command: String, single_command: bool, - signal_registry: Option, - signals_to_dispatch_event: Vec, // $initialized is omitted. See ApplicationHandle::init(). pub(crate) composer: Option, pub(crate) io: std::rc::Rc>, @@ -175,7 +175,7 @@ impl Application { let initial_working_directory = getcwd(); - let mut this = Self { + Self { commands: IndexMap::new(), want_helps: false, running_command: None, @@ -188,8 +188,6 @@ impl Application { terminal: Terminal::new(), default_command: "list".to_string(), single_command: false, - signal_registry: None, - signals_to_dispatch_event: Vec::new(), composer: None, io, has_plugin_commands: false, @@ -199,17 +197,7 @@ impl Application { initial_working_directory, dev_warning_time: composer::COMPOSER_DEV_WARNING_TIME, me: std::rc::Weak::new(), - }; - if defined("SIGINT") && SignalRegistry::is_supported() { - this.signal_registry = Some(SignalRegistry::new()); - this.signals_to_dispatch_event = vec![ - shirabe_php_shim::SIGINT, - shirabe_php_shim::SIGTERM, - shirabe_php_shim::SIGUSR1, - shirabe_php_shim::SIGUSR2, - ]; } - this } /// Returns the shared handle to this application set up by `ApplicationHandle::new`. Proxy @@ -731,18 +719,6 @@ impl Application { self.command_loader = Some(command_loader); } - pub fn get_signal_registry(&self) -> anyhow::Result<&SignalRegistry> { - match &self.signal_registry { - None => Err(ConsoleRuntimeException::new("Signals are not supported. Make sure that the `pcntl` extension is installed and that \"pcntl_*\" functions are not disabled by your php.ini's \"disable_functions\" directive.".to_string()) - .into()), - Some(signal_registry) => Ok(signal_registry), - } - } - - pub fn set_signals_to_dispatch_event(&mut self, signals_to_dispatch_event: Vec) { - self.signals_to_dispatch_event = signals_to_dispatch_event; - } - pub fn set_helper_set(&mut self, helper_set: std::rc::Rc>) { self.helper_set = Some(helper_set); } @@ -2993,35 +2969,6 @@ impl ApplicationHandle { // be reached until dynamic helper registration is restored (see HelperSet). let _ = command.borrow().get_helper_set(); - if !application.borrow().signals_to_dispatch_event.is_empty() { - // $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [] - // TODO(phase-c): SymfonyCommand is not a SignalableCommandInterface here; downcast needed. - let command_signals: Vec = Vec::new(); - let _ = std::marker::PhantomData::; - - if !command_signals.is_empty() { - if application.borrow().signal_registry.is_none() { - return Err(ConsoleRuntimeException::new("Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that \"pcntl_*\" functions are not disabled by your php.ini's \"disable_functions\" directive.".to_string()) - .into()); - } - - if Terminal::has_stty_available() { - // TODO(phase-c): registers SIGINT/SIGTERM handlers that restore the stty mode via - // shell_exec('stty ...'). pcntl signal handlers have no faithful Rust - // equivalent yet. - let _stty_mode = shirabe_php_shim::shell_exec("stty -g"); - for _signal in [shirabe_php_shim::SIGINT, shirabe_php_shim::SIGTERM] { - todo!("register signal handler to restore stty mode"); - } - } - } - - for _signal in command_signals { - // $this->signalRegistry->register($signal, [$command, 'handleSignal']); - todo!("register command->handle_signal as signal handler"); - } - } - command .borrow() .run(input.clone(), output.clone()) -- cgit v1.3.1-4-g156e