diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-11 12:19:31 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-11 12:19:31 +0900 |
| commit | 9539fea16e0d8d07faf7edf50d480e0a80c4ccfc (patch) | |
| tree | c989aae95701e24aace29fec25c9d33c4c065e6b /docs | |
| parent | 144d059b725e2d178d7f4a6403cde9474fe65dcc (diff) | |
| download | php-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 'docs')
| -rw-r--r-- | docs/dev/signals.md | 46 | ||||
| -rw-r--r-- | docs/known-incompatibilities.md | 10 |
2 files changed, 56 insertions, 0 deletions
diff --git a/docs/dev/signals.md b/docs/dev/signals.md new file mode 100644 index 00000000..9616f0ee --- /dev/null +++ b/docs/dev/signals.md @@ -0,0 +1,46 @@ +# Signals + +## In Composer core + +Composer has three routes for handling signals: + +* `Seld\Signal\SignalHandler` +* `Symfony\Component\Console\SignalRegistry\SignalRegistry` and `SignalableCommandInterface` +* PHP builtins (`pcntl_signal()`, etc.) + +Of these, upstream Composer itself only ever uses `SignalHandler`, and Shirabe +behaves roughly the same way. The difference is when the interruption takes +effect, as described in [known incompatibilities](../known-incompatibilities.md). + +> Composer runs its abort handler almost immediately after the signal arrives. +> Shirabe, however, runs it at the next checkpoint instead, so stopping `shirabe` +> command by `Ctrl+C` may take more time than Composer. + +In PHP, the VM checks for a pending signal at the end of a loop and on a +function call, which gets the signal handler run almost immediately after the +signal arrives. Rust has no such mechanism, so we place the checkpoints by +hand; grep for `signals.is_triggered()` to find them. They are not as +fine-grained as a function call, so more work runs between receiving the signal +and starting the abort than Composer would let through, and it takes longer. + +## In plugins and scripts + +Shirabe treats signal handling in plugins and scripts as undefined behavior, +for two broad reasons. + +The first is that Shirabe consists of a Rust core process and a PHP worker +process that runs the plugins and scripts, which makes faithful reproduction +difficult. + +The second is that Composer itself does not fully account for plugins and +scripts subscribing to signals either. The `SignalHandler` that Composer uses +to handle signals overwrites an existing signal handler unconditionally. So +even when a plugin or script installs a handler through Symfony Console or a +PHP builtin, that handler is lost the moment execution reaches a place where +Composer uses `SignalHandler`. + +For these two reasons, Shirabe today neither restricts plugins and scripts from +installing signal handlers nor does anything special about it. What happens +when they do is not guaranteed. + +This stance may be withdrawn if a legitimate use case turns up. diff --git a/docs/known-incompatibilities.md b/docs/known-incompatibilities.md index daaa8bcd..97e4dad7 100644 --- a/docs/known-incompatibilities.md +++ b/docs/known-incompatibilities.md @@ -39,6 +39,16 @@ behavior is slightly different. See [docs/dev/xdebug.md](./dev/xdebug.md) for details. +## Signals + +Composer runs its abort handler almost immediately after the signal arrives. +Shirabe, however, runs it at the next checkpoint instead, so stopping `shirabe` +command by `Ctrl+C` may take more time than Composer. + +Signal handling in plugins and scripts is undefined behavior: it may or may not +work. See [docs/dev/signals.md](./dev/signals.md) for details. + + ## Plugins ### Reflection |
