From a07f9ffba98fe5472a3004b56299ca99e22dc6c9 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 22 May 2020 13:24:30 +0200 Subject: [PATCH] Catch SIGINT/Ctrl+C during installs and run cleanups on all packages before exiting --- .../Installer/InstallationManager.php | 137 ++++++++++++------ 1 file changed, 90 insertions(+), 47 deletions(-) diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 4b8db7877..a8266bfe7 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -171,34 +171,89 @@ class InstallationManager public function execute(RepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true) { $promises = array(); - - foreach ($operations as $operation) { - $opType = $operation->getOperationType(); - $promise = null; - - if ($opType === 'install') { - $package = $operation->getPackage(); - $installer = $this->getInstaller($package->getType()); - $promise = $installer->download($package); - } elseif ($opType === 'update') { - $target = $operation->getTargetPackage(); - $targetType = $target->getType(); - $installer = $this->getInstaller($targetType); - $promise = $installer->download($target, $operation->getInitialPackage()); - } - - if ($promise) { - $promises[] = $promise; - } - } - - if (!empty($promises)) { - $this->loop->wait($promises); - } - $cleanupPromises = array(); + + $loop = $this->loop; + $runCleanup = function () use (&$cleanupPromises, $loop) { + $promises = array(); + + foreach ($cleanupPromises as $cleanup) { + $promises[] = new \React\Promise\Promise(function ($resolve, $reject) use ($cleanup) { + $promise = $cleanup(); + if (null === $promise) { + $resolve(); + } else { + $promise->then(function () use ($resolve) { + $resolve(); + }); + } + }); + } + + if (!empty($promises)) { + $loop->wait($promises); + } + }; + + // handler Ctrl+C for unix-like systems + $handleInterrupts = function_exists('pcntl_async_signals') && function_exists('pcntl_signal'); + $prevHandler = null; + if ($handleInterrupts) { + pcntl_async_signals(true); + $prevHandler = pcntl_signal_get_handler(SIGINT); + pcntl_signal(SIGINT, function ($sig) use ($runCleanup, $prevHandler) { + $runCleanup(); + + if (!in_array($prevHandler, array(SIG_DFL, SIG_IGN), true)) { + call_user_func($prevHandler, $sig); + } + + exit(130); + }); + } + try { - foreach ($operations as $operation) { + foreach ($operations as $index => $operation) { + $opType = $operation->getOperationType(); + + // ignoring alias ops as they don't need to execute anything at this stage + if (!in_array($opType, array('update', 'install', 'uninstall'))) { + continue; + } + + if ($opType === 'update') { + $package = $operation->getTargetPackage(); + $initialPackage = $operation->getInitialPackage(); + } else { + $package = $operation->getPackage(); + $initialPackage = null; + } + $installer = $this->getInstaller($package->getType()); + + $cleanupPromises[$index] = function () use ($opType, $installer, $package, $initialPackage) { + // avoid calling cleanup if the download was not even initialized for a package + // as without installation source configured nothing will work + if (!$package->getInstallationSource()) { + return; + } + + return $installer->cleanup($opType, $package, $initialPackage); + }; + + if ($opType !== 'uninstall') { + $promise = $installer->download($package, $initialPackage); + if ($promise) { + $promises[] = $promise; + } + } + } + + // execute all downloads first + if (!empty($promises)) { + $this->loop->wait($promises); + } + + foreach ($operations as $index => $operation) { $opType = $operation->getOperationType(); // ignoring alias ops as they don't need to execute anything @@ -236,13 +291,9 @@ class InstallationManager $promise = new \React\Promise\Promise(function ($resolve, $reject) { $resolve(); }); } - $cleanupPromise = function () use ($opType, $installer, $package, $initialPackage) { - return $installer->cleanup($opType, $package, $initialPackage); - }; - $promise = $promise->then(function () use ($opType, $installManager, $repo, $operation) { return $installManager->$opType($repo, $operation); - })->then($cleanupPromise) + })->then($cleanupPromises[$index]) ->then(function () use ($opType, $runScripts, $dispatcher, $installManager, $devMode, $repo, $operations, $operation) { $repo->write($devMode, $installManager); @@ -256,35 +307,27 @@ class InstallationManager throw $e; }); - $cleanupPromises[] = $cleanupPromise; $promises[] = $promise; } + // execute all prepare => installs/updates/removes => cleanup steps if (!empty($promises)) { $this->loop->wait($promises); } } catch (\Exception $e) { - $promises = array(); - foreach ($cleanupPromises as $cleanup) { - $promises[] = new \React\Promise\Promise(function ($resolve, $reject) use ($cleanup) { - $promise = $cleanup(); - if (null === $promise) { - $resolve(); - } else { - $promise->then(function () use ($resolve) { - $resolve(); - }); - } - }); - } + $runCleanup(); - if (!empty($promises)) { - $this->loop->wait($promises); + if ($handleInterrupts) { + pcntl_signal(SIGINT, $prevHandler); } throw $e; } + if ($handleInterrupts) { + pcntl_signal(SIGINT, $prevHandler); + } + // do a last write so that we write the repository even if nothing changed // as that can trigger an update of some files like InstalledVersions.php if // running a new composer version