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 /crates/shirabe/src/command/create_project_command.rs | |
| 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 'crates/shirabe/src/command/create_project_command.rs')
| -rw-r--r-- | crates/shirabe/src/command/create_project_command.rs | 61 |
1 files changed, 33 insertions, 28 deletions
diff --git a/crates/shirabe/src/command/create_project_command.rs b/crates/shirabe/src/command/create_project_command.rs index 787e31d7..c7768f11 100644 --- a/crates/shirabe/src/command/create_project_command.rs +++ b/crates/shirabe/src/command/create_project_command.rs @@ -32,6 +32,7 @@ use crate::repository::PlatformRepository; use crate::repository::RepositoryFactory; use crate::repository::RepositorySet; use crate::script::ScriptEvents; +use crate::signal::SignalSubscription; use crate::util::Filesystem; use crate::util::Platform; use crate::util::ProcessExecutor; @@ -43,7 +44,6 @@ use shirabe_php_shim::{ chdir, explode_with_limit, file_exists, getcwd, impl_php_class, implode, is_dir, is_file, mkdir, realpath, rtrim, 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; @@ -731,28 +731,23 @@ impl CreateProjectCommand { // handler Ctrl+C aborts gracefully let _ = mkdir(&directory, 0o777, true); - let mut signal_handler: Option<SignalHandler> = None; - if let Some(real_dir) = realpath(&directory) { - let real_dir_clone = real_dir; - let io_for_signal = io.clone(); - signal_handler = Some(SignalHandler::create( - vec![ - SignalHandler::SIGINT.to_string(), - SignalHandler::SIGTERM.to_string(), - SignalHandler::SIGHUP.to_string(), - ], - Box::new(move |signal: String, handler: &SignalHandler| { - io_for_signal.write_error3( - &format!("Received {}, aborting", signal), - true, - crate::io::DEBUG, - ); - let mut fs = Filesystem::new(None); - fs.remove_directory(&real_dir_clone).ok(); - handler.exit_with_last_signal(); - }), - )); - } + let real_dir = realpath(&directory); + let signals = real_dir.as_ref().map(|_| SignalSubscription::new()); + let abort_on_signal = |signals: &SignalSubscription| { + io.write_error3( + &format!("Received {}, aborting", signals.last_signal().as_str()), + true, + crate::io::DEBUG, + ); + let mut fs = Filesystem::new(None); + fs.remove_directory( + real_dir + .as_ref() + .expect("subscribed only when realpath succeeded"), + ) + .ok(); + signals.exit_with_last_signal(); + }; // avoid displaying 9999999-dev as version if default-branch was selected if let Some(alias) = package.as_alias() @@ -791,13 +786,19 @@ impl CreateProjectCommand { ); // A shared borrow: plugin registration inside execute re-enters this manager handle // through the Composer graph. - installation_manager.borrow().execute( + let executed = installation_manager.borrow().execute( &installed_repo, vec![InstallOperation::new(package.clone()).into()], true, true, false, - )?; + ); + if let Some(signals) = &signals + && signals.is_triggered() + { + abort_on_signal(signals); + } + executed?; installation_manager .borrow_mut() .notify_installs(io.clone()); @@ -826,11 +827,15 @@ impl CreateProjectCommand { Platform::put_env("COMPOSER_ROOT_VERSION", &package.get_pretty_version()); - // once the root project is fully initialized, we do not need to wipe everything on user abort anymore even if it happens during deps install - if let Some(handler) = signal_handler { - handler.unregister(); + if let Some(signals) = &signals + && signals.is_triggered() + { + abort_on_signal(signals); } + // once the root project is fully initialized, we do not need to wipe everything on user abort anymore even if it happens during deps install + drop(signals); + Ok(installed_from_vcs) } } |
