aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/installer
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/installer
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/installer')
-rw-r--r--crates/shirabe/src/installer/installation_manager.rs71
1 files changed, 54 insertions, 17 deletions
diff --git a/crates/shirabe/src/installer/installation_manager.rs b/crates/shirabe/src/installer/installation_manager.rs
index 24960d28..7fdec9e1 100644
--- a/crates/shirabe/src/installer/installation_manager.rs
+++ b/crates/shirabe/src/installer/installation_manager.rs
@@ -17,6 +17,7 @@ use crate::io::io_interface;
use crate::package::PackageInterfaceHandle;
use crate::repository::InstalledRepositoryInterface;
use crate::repository::InstalledRepositoryInterfaceHandle;
+use crate::signal::SignalSubscription;
use crate::util::Platform;
use crate::util::r#loop::Loop;
use crate::util::sync_executor;
@@ -25,7 +26,6 @@ use shirabe_php_shim::{
InvalidArgumentException, PhpMixed, array_splice, array_unshift, http_build_query, json_encode,
str_contains, str_replace, strpos, strtolower,
};
-use shirabe_seld_signal::SignalHandler;
/// Package operation manager.
#[derive(Debug)]
@@ -309,19 +309,7 @@ impl InstallationManager {
>,
> = IndexMap::new();
- let signal_handler = SignalHandler::create(
- vec![
- SignalHandler::SIGINT.to_string(),
- SignalHandler::SIGTERM.to_string(),
- SignalHandler::SIGHUP.to_string(),
- ],
- // TODO(phase-c): closure captures &mut self via &mut cleanup_promises
- Box::new(move |signal: String, handler: &SignalHandler| {
- // TODO(phase-c): self.io.write_error(...); self.run_cleanup(&cleanup_promises);
- let _ = signal;
- handler.exit_with_last_signal();
- }),
- );
+ let signals = SignalSubscription::new();
// Shared rather than owned so that one operation reaches a plugin as one object, both
// through the whole batch pipeline and through its pre- and post-event.
@@ -367,6 +355,9 @@ impl InstallationManager {
}
for batch_to_execute in batches {
+ if signals.is_triggered() {
+ sync_executor::block_on(self.abort_on_signal(&signals, &cleanup_promises));
+ }
sync_executor::block_on(self.download_and_execute_batch(
repo,
batch_to_execute,
@@ -375,23 +366,26 @@ impl InstallationManager {
run_scripts,
download_only,
all_operations.clone(),
+ &signals,
))?;
}
Ok(())
})();
- // finally
- signal_handler.unregister();
-
match result {
Ok(()) => {}
Err(e) => {
+ if signals.is_triggered() {
+ sync_executor::block_on(self.abort_on_signal(&signals, &cleanup_promises));
+ }
sync_executor::block_on(self.run_cleanup(&cleanup_promises));
return Err(e);
}
}
+ drop(signals);
+
if download_only {
return Ok(());
}
@@ -421,12 +415,16 @@ impl InstallationManager {
run_scripts: bool,
download_only: bool,
all_operations: Vec<std::rc::Rc<AnyOperation>>,
+ signals: &SignalSubscription,
) -> anyhow::Result<()> {
let mut promises: Vec<
std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<()>>>>,
> = vec![];
for (index, operation) in &operations {
+ if signals.is_triggered() {
+ self.abort_on_signal(signals, cleanup_promises).await;
+ }
let op_type = operation.get_operation_type();
// ignoring alias ops as they don't need to execute anything at this stage
@@ -496,6 +494,10 @@ impl InstallationManager {
self.wait_on_promises(promises).await?;
}
+ if signals.is_triggered() {
+ self.abort_on_signal(signals, cleanup_promises).await;
+ }
+
if download_only {
self.run_cleanup(cleanup_promises).await;
@@ -533,6 +535,9 @@ impl InstallationManager {
}
for batch_to_execute in batches {
+ if signals.is_triggered() {
+ self.abort_on_signal(signals, cleanup_promises).await;
+ }
self.execute_batch(
repo,
batch_to_execute,
@@ -540,6 +545,7 @@ impl InstallationManager {
dev_mode,
run_scripts,
&all_operations,
+ signals,
)
.await?;
}
@@ -547,6 +553,7 @@ impl InstallationManager {
Ok(())
}
+ #[allow(clippy::too_many_arguments, reason = "to keep PHP signature")]
async fn execute_batch(
&self,
repo: &InstalledRepositoryInterfaceHandle,
@@ -562,6 +569,7 @@ impl InstallationManager {
dev_mode: bool,
run_scripts: bool,
all_operations: &[std::rc::Rc<AnyOperation>],
+ signals: &SignalSubscription,
) -> anyhow::Result<()> {
let mut promises: Vec<
std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<()>> + '_>>,
@@ -570,6 +578,9 @@ impl InstallationManager {
let mut post_exec_callbacks: Vec<Box<dyn Fn() -> anyhow::Result<()>>> = vec![];
for (index, operation) in operations {
+ if signals.is_triggered() {
+ self.abort_on_signal(signals, cleanup_promises).await;
+ }
let op_type = operation.get_operation_type();
// ignoring alias ops as they don't need to execute anything
@@ -704,6 +715,10 @@ impl InstallationManager {
self.wait_on_promises(promises).await?;
}
+ if signals.is_triggered() {
+ self.abort_on_signal(signals, cleanup_promises).await;
+ }
+
Platform::workaround_filesystem_issues();
for cb in post_exec_callbacks {
@@ -1022,6 +1037,28 @@ impl InstallationManager {
let _ = self.loop_.borrow_mut().wait(promises, None).await;
}
}
+
+ /// Cleans up after the packages installed so far and terminates the process. Never returns.
+ async fn abort_on_signal(
+ &self,
+ signals: &SignalSubscription,
+ cleanup_promises: &IndexMap<
+ i64,
+ Box<
+ dyn Fn() -> Option<
+ std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<()>>>>,
+ >,
+ >,
+ >,
+ ) {
+ self.io.write_error3(
+ &format!("Received {}, aborting", signals.last_signal().as_str()),
+ true,
+ io_interface::DEBUG,
+ );
+ self.run_cleanup(cleanup_promises).await;
+ signals.exit_with_last_signal();
+ }
}
// Composer's PartialComposer::setInstallationManager() accepts any InstallationManager subclass, so