aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-symfony-console/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-11 12:19:31 +0900
committernsfisis <nsfisis@gmail.com>2026-08-11 12:19:31 +0900
commit9539fea16e0d8d07faf7edf50d480e0a80c4ccfc (patch)
treec989aae95701e24aace29fec25c9d33c4c065e6b /crates/shirabe-symfony-console/src
parent144d059b725e2d178d7f4a6403cde9474fe65dcc (diff)
downloadphp-shirabe-9539fea16e0d8d07faf7edf50d480e0a80c4ccfc.tar.gz
php-shirabe-9539fea16e0d8d07faf7edf50d480e0a80c4ccfc.tar.zst
php-shirabe-9539fea16e0d8d07faf7edf50d480e0a80c4ccfc.zip
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) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-symfony-console/src')
-rw-r--r--crates/shirabe-symfony-console/src/command.rs2
-rw-r--r--crates/shirabe-symfony-console/src/command/signalable_command_interface.rs10
-rw-r--r--crates/shirabe-symfony-console/src/lib.rs2
-rw-r--r--crates/shirabe-symfony-console/src/signal_registry.rs3
-rw-r--r--crates/shirabe-symfony-console/src/signal_registry/signal_registry.rs102
5 files changed, 0 insertions, 119 deletions
diff --git a/crates/shirabe-symfony-console/src/command.rs b/crates/shirabe-symfony-console/src/command.rs
index 5ab97400..7efaf401 100644
--- a/crates/shirabe-symfony-console/src/command.rs
+++ b/crates/shirabe-symfony-console/src/command.rs
@@ -3,11 +3,9 @@ mod complete_command;
mod dump_completion_command;
mod help_command;
mod list_command;
-mod signalable_command_interface;
pub use command::*;
pub use complete_command::*;
pub use dump_completion_command::*;
pub use help_command::*;
pub use list_command::*;
-pub use signalable_command_interface::*;
diff --git a/crates/shirabe-symfony-console/src/command/signalable_command_interface.rs b/crates/shirabe-symfony-console/src/command/signalable_command_interface.rs
deleted file mode 100644
index 8777eb5e..00000000
--- a/crates/shirabe-symfony-console/src/command/signalable_command_interface.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-//! ref: composer/vendor/symfony/console/Command/SignalableCommandInterface.php
-
-/// Interface for command reacting to signal.
-pub trait SignalableCommandInterface {
- /// Returns the list of signals to subscribe.
- fn get_subscribed_signals(&self) -> Vec<i64>;
-
- /// The method will be called when the application is signaled.
- fn handle_signal(&mut self, signal: i64);
-}
diff --git a/crates/shirabe-symfony-console/src/lib.rs b/crates/shirabe-symfony-console/src/lib.rs
index 814e53a3..4a5a4ceb 100644
--- a/crates/shirabe-symfony-console/src/lib.rs
+++ b/crates/shirabe-symfony-console/src/lib.rs
@@ -12,7 +12,6 @@ pub mod helper;
pub mod input;
pub mod output;
pub mod question;
-pub mod signal_registry;
pub mod style;
pub mod terminal;
pub mod tester;
@@ -31,6 +30,5 @@ pub use helper::*;
pub use input::*;
pub use output::*;
pub use question::*;
-pub use signal_registry::*;
pub use style::*;
pub use terminal::*;
diff --git a/crates/shirabe-symfony-console/src/signal_registry.rs b/crates/shirabe-symfony-console/src/signal_registry.rs
deleted file mode 100644
index ab48848e..00000000
--- a/crates/shirabe-symfony-console/src/signal_registry.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-mod signal_registry;
-
-pub use signal_registry::*;
diff --git a/crates/shirabe-symfony-console/src/signal_registry/signal_registry.rs b/crates/shirabe-symfony-console/src/signal_registry/signal_registry.rs
deleted file mode 100644
index f1f67d09..00000000
--- a/crates/shirabe-symfony-console/src/signal_registry/signal_registry.rs
+++ /dev/null
@@ -1,102 +0,0 @@
-//! ref: composer/vendor/symfony/console/SignalRegistry/SignalRegistry.php
-
-use indexmap::IndexMap;
-
-/// A signal handler receives the signal number and whether a further handler follows.
-pub type SignalHandler = Box<dyn Fn(i64, bool)>;
-
-pub struct SignalRegistry {
- // signal number => list of handlers
- signal_handlers: IndexMap<i64, Vec<SignalHandler>>,
-}
-
-impl std::fmt::Debug for SignalRegistry {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("SignalRegistry")
- .field("signal_handlers", &self.signal_handlers.keys())
- .finish_non_exhaustive()
- }
-}
-
-impl Default for SignalRegistry {
- fn default() -> Self {
- Self::new()
- }
-}
-
-impl SignalRegistry {
- pub fn new() -> Self {
- if shirabe_php_shim::function_exists("pcntl_async_signals") {
- shirabe_php_shim::pcntl_async_signals(true);
- }
-
- Self {
- signal_handlers: IndexMap::new(),
- }
- }
-
- pub fn register(&mut self, signal: i64, signal_handler: SignalHandler) {
- if !self.signal_handlers.contains_key(&signal) {
- let previous_callback = shirabe_php_shim::pcntl_signal_get_handler(signal);
-
- if shirabe_php_shim::is_callable(&previous_callback) {
- // $this->signalHandlers[$signal][] = $previousCallback;
- // The previous handler is an opaque PHP callable obtained from pcntl;
- // it is invoked through the runtime callable mechanism.
- self.signal_handlers
- .entry(signal)
- .or_default()
- .push(Box::new(move |signal, has_next| {
- shirabe_php_shim::call_php_callable(
- &previous_callback,
- &[
- shirabe_php_shim::PhpMixed::Int(signal),
- shirabe_php_shim::PhpMixed::Bool(has_next),
- ],
- );
- }));
- }
- }
-
- self.signal_handlers
- .entry(signal)
- .or_default()
- .push(signal_handler);
-
- // pcntl_signal($signal, [$this, 'handle'])
- // TODO(plugin): the PHP callback `[$this, 'handle']` captures the registry
- // instance. Wiring this object method as a C-level signal handler requires the
- // runtime callable mechanism; see review notes.
- shirabe_php_shim::pcntl_signal(signal, shirabe_php_shim::PhpMixed::Null);
- }
-
- pub fn is_supported() -> bool {
- if !shirabe_php_shim::function_exists("pcntl_signal") {
- return false;
- }
-
- if shirabe_php_shim::explode(
- ",",
- &shirabe_php_shim::ini_get("disable_functions").unwrap_or_default(),
- )
- .contains(&"pcntl_signal".to_string())
- {
- return false;
- }
-
- true
- }
-
- pub fn handle(&self, signal: i64) {
- let handlers = match self.signal_handlers.get(&signal) {
- Some(handlers) => handlers,
- None => return,
- };
- let count = handlers.len();
-
- for (i, signal_handler) in handlers.iter().enumerate() {
- let has_next = i != count - 1;
- signal_handler(signal, has_next);
- }
- }
-}