Skip to content

Commit

Permalink
Merge pull request #10958 from Seldaek/signal_fixes
Browse files Browse the repository at this point in the history
Signal handling improvements
  • Loading branch information
Seldaek committed Jul 21, 2022
2 parents 8352ab8 + 07645b9 commit d0b60f1
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 109 deletions.
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -40,7 +40,8 @@
"react/promise": "^2.8",
"composer/pcre": "^2 || ^3",
"symfony/polyfill-php73": "^1.24",
"symfony/polyfill-php80": "^1.24"
"symfony/polyfill-php80": "^1.24",
"seld/signal-handler": "^2.0"
},
"require-dev": {
"symfony/phpunit-bridge": "^6.0",
Expand Down
63 changes: 62 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 15 additions & 22 deletions src/Composer/Command/CreateProjectCommand.php
Expand Up @@ -35,6 +35,7 @@
use Composer\Script\ScriptEvents;
use Composer\Util\Silencer;
use Composer\Console\Input\InputArgument;
use Seld\Signal\SignalHandler;
use Symfony\Component\Console\Input\InputInterface;
use Composer\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
Expand Down Expand Up @@ -452,28 +453,15 @@ protected function installRootPackage(IOInterface $io, Config $config, string $p
throw new \InvalidArgumentException($errorMessage .'.');
}

// handler Ctrl+C for unix-like systems
if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) {
@mkdir($directory, 0777, true);
if ($realDir = realpath($directory)) {
pcntl_async_signals(true);
pcntl_signal(SIGINT, static function () use ($realDir): void {
$fs = new Filesystem();
$fs->removeDirectory($realDir);
exit(130);
});
}
}
// handler Ctrl+C for Windows on PHP 7.4+
if (function_exists('sapi_windows_set_ctrl_handler') && PHP_SAPI === 'cli') {
@mkdir($directory, 0777, true);
if ($realDir = realpath($directory)) {
sapi_windows_set_ctrl_handler(static function () use ($realDir): void {
$fs = new Filesystem();
$fs->removeDirectory($realDir);
exit(130);
});
}
// handler Ctrl+C aborts gracefully
@mkdir($directory, 0777, true);
if (false !== ($realDir = realpath($directory))) {
$signalHandler = SignalHandler::create([SignalHandler::SIGINT, SignalHandler::SIGTERM, SignalHandler::SIGHUP], function (string $signal, SignalHandler $handler) use ($realDir) {
$this->getIO()->writeError('Received '.$signal.', aborting', true, IOInterface::DEBUG);
$fs = new Filesystem();
$fs->removeDirectory($realDir);
$handler->exitWithLastSignal();
});
}

// avoid displaying 9999999-dev as version if default-branch was selected
Expand Down Expand Up @@ -512,6 +500,11 @@ protected function installRootPackage(IOInterface $io, Config $config, string $p

Platform::putEnv('COMPOSER_ROOT_VERSION', $package->getPrettyVersion());

// 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 (isset($signalHandler)) {
$signalHandler->unregister();
}

return $installedFromVcs;
}
}
30 changes: 12 additions & 18 deletions src/Composer/Command/RequireCommand.php
Expand Up @@ -14,6 +14,7 @@

use Composer\DependencyResolver\Request;
use Composer\Util\Filesystem;
use Seld\Signal\SignalHandler;
use Symfony\Component\Console\Input\InputInterface;
use Composer\Console\Input\InputArgument;
use Composer\Console\Input\InputOption;
Expand Down Expand Up @@ -148,12 +149,11 @@ protected function execute(InputInterface $input, OutputInterface $output)
$this->composerBackup = file_get_contents($this->json->getPath());
$this->lockBackup = file_exists($this->lock) ? file_get_contents($this->lock) : null;

if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) {
pcntl_async_signals(true);
pcntl_signal(SIGINT, function () { $this->revertComposerFile(); });
pcntl_signal(SIGTERM, function () { $this->revertComposerFile(); });
pcntl_signal(SIGHUP, function () { $this->revertComposerFile(); });
}
$signalHandler = SignalHandler::create([SignalHandler::SIGINT, SignalHandler::SIGTERM, SignalHandler::SIGHUP], function (string $signal, SignalHandler $handler) {
$this->getIO()->writeError('Received '.$signal.', aborting', true, IOInterface::DEBUG);
$this->revertComposerFile();
$handler->exitWithLastSignal();
});

// 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
Expand Down Expand Up @@ -210,7 +210,7 @@ protected function execute(InputInterface $input, OutputInterface $output)
);
} catch (\Exception $e) {
if ($this->newlyCreated) {
$this->revertComposerFile(false);
$this->revertComposerFile();

throw new \RuntimeException('No composer.json present in the current directory ('.$this->file.'), this may be the cause of the following exception.', 0, $e);
}
Expand Down Expand Up @@ -293,9 +293,11 @@ protected function execute(InputInterface $input, OutputInterface $output)
return $this->doUpdate($input, $output, $io, $requirements, $requireKey, $removeKey);
} catch (\Exception $e) {
if (!$this->dependencyResolutionCompleted) {
$this->revertComposerFile(false);
$this->revertComposerFile();
}
throw $e;
} finally {
$signalHandler->unregister();
}
}

Expand Down Expand Up @@ -439,7 +441,7 @@ private function doUpdate(InputInterface $input, OutputInterface $output, IOInte
}
}
}
$this->revertComposerFile(false);
$this->revertComposerFile();
}

return $status;
Expand Down Expand Up @@ -479,11 +481,7 @@ protected function interact(InputInterface $input, OutputInterface $output): voi

}

/**
* @param bool $hardExit
* @return void
*/
public function revertComposerFile(bool $hardExit = true): void
private function revertComposerFile(): void
{
$io = $this->getIO();

Expand All @@ -504,9 +502,5 @@ public function revertComposerFile(bool $hardExit = true): void
file_put_contents($this->lock, $this->lockBackup);
}
}

if ($hardExit) {
exit(1);
}
}
}
16 changes: 8 additions & 8 deletions src/Composer/Console/Application.php
Expand Up @@ -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;
Expand Down Expand Up @@ -93,13 +94,14 @@ public function __construct()
date_default_timezone_set(Silencer::call('date_default_timezone_get'));
}

$this->io = new NullIO();

if (!$shutdownRegistered) {
if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) {
pcntl_async_signals(true);
pcntl_signal(SIGINT, static function ($sig): void {
exit(130);
});
}
$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;

Expand All @@ -114,8 +116,6 @@ public function __construct()
});
}

$this->io = new NullIO();

$this->initialWorkingDirectory = getcwd();

parent::__construct('Composer', Composer::getVersion());
Expand Down
2 changes: 2 additions & 0 deletions src/Composer/EventDispatcher/ScriptExecutionException.php
Expand Up @@ -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 <j.boggiano@seld.be>
*/
class ScriptExecutionException extends \RuntimeException
Expand Down
52 changes: 8 additions & 44 deletions src/Composer/Installer/InstallationManager.php
Expand Up @@ -28,6 +28,7 @@
use Composer\Util\Loop;
use Composer\Util\Platform;
use React\Promise\PromiseInterface;
use Seld\Signal\SignalHandler;

/**
* Package operation manager.
Expand Down Expand Up @@ -222,36 +223,11 @@ public function execute(InstalledRepositoryInterface $repo, array $operations, b
}
};

$handleInterruptsUnix = function_exists('pcntl_async_signals') && function_exists('pcntl_signal');
$handleInterruptsWindows = PHP_VERSION_ID >= 70400 && function_exists('sapi_windows_set_ctrl_handler') && PHP_SAPI === 'cli';
$prevHandler = null;
$windowsHandler = null;
if ($handleInterruptsUnix) {
pcntl_async_signals(true);
$prevHandler = pcntl_signal_get_handler(SIGINT);
pcntl_signal(SIGINT, static function ($sig) use ($runCleanup, $prevHandler, $io): void {
$io->writeError('Received SIGINT, aborting', true, IOInterface::DEBUG);
$runCleanup();

if (!in_array($prevHandler, array(SIG_DFL, SIG_IGN), true)) {
call_user_func($prevHandler, $sig);
}

exit(130);
});
}
if ($handleInterruptsWindows) {
$windowsHandler = static function ($event) use ($runCleanup, $io): void {
if ($event !== PHP_WINDOWS_EVENT_CTRL_C) {
return;
}
$io->writeError('Received CTRL+C, aborting', true, IOInterface::DEBUG);
$runCleanup();

exit(130);
};
sapi_windows_set_ctrl_handler($windowsHandler);
}
$signalHandler = SignalHandler::create([SignalHandler::SIGINT, SignalHandler::SIGTERM, SignalHandler::SIGHUP], static function (string $signal, SignalHandler $handler) use ($io, $runCleanup) {
$io->writeError('Received '.$signal.', aborting', true, IOInterface::DEBUG);
$runCleanup();
$handler->exitWithLastSignal();
});

try {
// execute operations in batches to make sure download-modifying-plugins are installed
Expand Down Expand Up @@ -284,21 +260,9 @@ public function execute(InstalledRepositoryInterface $repo, array $operations, b
} catch (\Exception $e) {
$runCleanup();

if ($handleInterruptsUnix) {
pcntl_signal(SIGINT, $prevHandler);
}
if ($handleInterruptsWindows) {
sapi_windows_set_ctrl_handler($windowsHandler, false);
}

throw $e;
}

if ($handleInterruptsUnix) {
pcntl_signal(SIGINT, $prevHandler);
}
if ($handleInterruptsWindows) {
sapi_windows_set_ctrl_handler($windowsHandler, false);
} finally {
$signalHandler->unregister();
}

// do a last write so that we write the repository even if nothing changed
Expand Down
27 changes: 22 additions & 5 deletions src/Composer/Util/ProcessExecutor.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down

0 comments on commit d0b60f1

Please sign in to comment.