aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/command/require_command.rs
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/src/command/require_command.rs
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/src/command/require_command.rs')
-rw-r--r--crates/shirabe/src/command/require_command.rs47
1 files changed, 26 insertions, 21 deletions
diff --git a/crates/shirabe/src/command/require_command.rs b/crates/shirabe/src/command/require_command.rs
index d4295a50..f06c4d84 100644
--- a/crates/shirabe/src/command/require_command.rs
+++ b/crates/shirabe/src/command/require_command.rs
@@ -28,6 +28,7 @@ use crate::repository::CompositeRepository;
use crate::repository::PlatformRepository;
use crate::repository::PlatformRepositoryHandle;
use crate::repository::RepositorySet;
+use crate::signal::SignalSubscription;
use crate::util::Filesystem;
use crate::util::PackageSorter;
use crate::util::Silencer;
@@ -37,7 +38,6 @@ use shirabe_php_shim::{
array_merge, array_unique, empty, file_exists, file_get_contents, file_put_contents, filesize,
impl_php_class, implode, is_writable, strtolower, unlink,
};
-use shirabe_seld_signal::SignalHandler;
use shirabe_symfony_console::command::Command;
use shirabe_symfony_console::input::InputInterface;
use shirabe_symfony_console::output::OutputInterface;
@@ -839,25 +839,16 @@ impl Command for RequireCommand {
None
};
- // PHP: function ($signal, $handler) use ($io, $self) {
- // $io->writeError('Received '.$signal.', aborting', true, IOInterface::DEBUG);
- // $self->revertComposerFile(); $handler->exitWithLastSignal(); }
- // TODO(phase-c): SignalHandler::create takes a `Box<dyn Fn> + 'static` handler that cannot
- // borrow &self, but the body must call self.revert_composer_file() (which mutates the
- // command's composer.json backup state) and self.get_io(). Faithfully wiring this needs the
- // revert state + io shared into the closure (Rc<RefCell<...>>), i.e. the shared-ownership
- // rework of the command — the same pattern as InstallationManager::execute's signal handler.
- let signal_handler = SignalHandler::create(
- vec![
- SignalHandler::SIGINT.to_string(),
- SignalHandler::SIGTERM.to_string(),
- SignalHandler::SIGHUP.to_string(),
- ],
- Box::new(move |signal: String, handler: &SignalHandler| {
- let _ = signal;
- handler.exit_with_last_signal();
- }),
- );
+ let signals = SignalSubscription::new();
+ let abort_on_signal = |signals: &SignalSubscription| {
+ self.get_io().write_error3(
+ &format!("Received {}, aborting", signals.last_signal().as_str()),
+ true,
+ io_interface::DEBUG,
+ );
+ self.revert_composer_file();
+ signals.exit_with_last_signal();
+ };
// check for writability by writing to the file as is_writable can not be trusted on network-mounts
// see https://github.com/composer/composer/issues/8231 and https://bugs.php.net/bug.php?id=68926
@@ -971,6 +962,10 @@ impl Command for RequireCommand {
fixed,
);
+ if signals.is_triggered() {
+ abort_on_signal(&signals);
+ }
+
let requirements = match requirements_result {
Ok(r) => r,
Err(e) => {
@@ -1192,6 +1187,10 @@ impl Command for RequireCommand {
self.update_file(&json, &requirements, require_key, remove_key, sort_packages);
}
+ if signals.is_triggered() {
+ abort_on_signal(&signals);
+ }
+
let updated_msg = format!(
"<info>{} has been {}</info>",
file,
@@ -1227,6 +1226,9 @@ impl Command for RequireCommand {
require_key,
remove_key,
);
+ if signals.is_triggered() {
+ abort_on_signal(&signals);
+ }
let dry_run = input
.borrow()
.get_option("dry-run")?
@@ -1262,12 +1264,15 @@ impl Command for RequireCommand {
}
};
+ if signals.is_triggered() {
+ abort_on_signal(&signals);
+ }
+
// finally
if dry_run && self.newly_created.get() {
// @unlink($this->json->getPath());
unlink(json.borrow().get_path());
}
- signal_handler.unregister();
result
}