From a7f4cd3c02ad040b11215e2ecbf89612e79afdb5 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 20 Jul 2022 22:08:44 +0200 Subject: [PATCH] Capture signals and wait until child process exits to also exit, fixes #6059 --- src/Composer/Console/Application.php | 11 ++++++-- .../ScriptExecutionException.php | 2 ++ src/Composer/Util/ProcessExecutor.php | 27 +++++++++++++++---- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 07f9108b335b..315379a2c505 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -17,6 +17,7 @@ use Composer\Util\Platform; use Composer\Util\Silencer; use LogicException; +use Seld\Signal\SignalHandler; use Symfony\Component\Console\Application as BaseApplication; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Helper\HelperSet; @@ -93,7 +94,15 @@ public function __construct() date_default_timezone_set(Silencer::call('date_default_timezone_get')); } + $this->io = new NullIO(); + if (!$shutdownRegistered) { + $signalHandler = SignalHandler::create([SignalHandler::SIGINT, SignalHandler::SIGTERM, SignalHandler::SIGHUP], function (string $signal, SignalHandler $handler) { + $this->io->writeError('Received '.$signal.', aborting', true, IOInterface::DEBUG); + + $handler->exitWithLastSignal(); + }); + $shutdownRegistered = true; register_shutdown_function(static function (): void { @@ -107,8 +116,6 @@ public function __construct() }); } - $this->io = new NullIO(); - $this->initialWorkingDirectory = getcwd(); parent::__construct('Composer', Composer::getVersion()); diff --git a/src/Composer/EventDispatcher/ScriptExecutionException.php b/src/Composer/EventDispatcher/ScriptExecutionException.php index 3b05a8831f7d..72a4aa273156 100644 --- a/src/Composer/EventDispatcher/ScriptExecutionException.php +++ b/src/Composer/EventDispatcher/ScriptExecutionException.php @@ -13,6 +13,8 @@ namespace Composer\EventDispatcher; /** + * Thrown when a script running an external process exits with a non-0 status code + * * @author Jordi Boggiano */ class ScriptExecutionException extends \RuntimeException diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index 768acbc217ff..3f57239bf0a9 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -14,6 +14,8 @@ use Composer\IO\IOInterface; use Composer\Pcre\Preg; +use Seld\Signal\SignalHandler; +use Symfony\Component\Process\Exception\ProcessSignaledException; use Symfony\Component\Process\Process; use Symfony\Component\Process\Exception\RuntimeException; use React\Promise\Promise; @@ -125,13 +127,28 @@ private function doExecute($command, ?string $cwd, bool $tty, &$output = null): $this->outputHandler($type, $buffer); }; - $process->run($callback); + $signalHandler = SignalHandler::create([SignalHandler::SIGINT, SignalHandler::SIGTERM, SignalHandler::SIGHUP], function (string $signal) { + if ($this->io !== null) { + $this->io->writeError('Received '.$signal.', aborting when child process is done', true, IOInterface::DEBUG); + } + }); - if ($this->captureOutput && !is_callable($output)) { - $output = $process->getOutput(); - } + try { + $process->run($callback); - $this->errorOutput = $process->getErrorOutput(); + if ($this->captureOutput && !is_callable($output)) { + $output = $process->getOutput(); + } + + $this->errorOutput = $process->getErrorOutput(); + } catch (ProcessSignaledException $e) { + if ($signalHandler->isTriggered()) { + // exiting as we were signaled and the child process exited too due to the signal + $signalHandler->exitWithLastSignal(); + } + } finally { + $signalHandler->unregister(); + } return $process->getExitCode(); }