From df57119884f62c2c29e8a9232b56a287196dad32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Thu, 13 Aug 2020 15:09:54 +0200 Subject: [PATCH] [Console] Rework the signal integration --- src/Symfony/Component/Console/Application.php | 39 +++++++++++++++---- src/Symfony/Component/Console/CHANGELOG.md | 4 ++ .../Command/SignalableCommandInterface.php | 30 ++++++++++++++ .../Console/SignalRegistry/SignalRegistry.php | 32 ++++++--------- 4 files changed, 77 insertions(+), 28 deletions(-) create mode 100644 src/Symfony/Component/Console/Command/SignalableCommandInterface.php diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index d8ba39fd8f40b..a17625e8e6483 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -14,6 +14,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\HelpCommand; use Symfony\Component\Console\Command\ListCommand; +use Symfony\Component\Console\Command\SignalableCommandInterface; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleErrorEvent; @@ -79,6 +80,7 @@ class Application implements ResetInterface private $singleCommand = false; private $initialized; private $signalRegistry; + private $signalsToDispatchEvent = []; public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN') { @@ -86,6 +88,10 @@ public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN $this->version = $version; $this->terminal = new Terminal(); $this->defaultCommand = 'list'; + $this->signalRegistry = new SignalRegistry(); + if (\defined('SIGINT')) { + $this->signalsToDispatchEvent = [SIGINT, SIGTERM, SIGUSR1, SIGUSR2]; + } } /** @@ -101,9 +107,14 @@ public function setCommandLoader(CommandLoaderInterface $commandLoader) $this->commandLoader = $commandLoader; } - public function setSignalRegistry(SignalRegistry $signalRegistry) + public function getSignalRegistry(): SignalRegistry { - $this->signalRegistry = $signalRegistry; + return $this->signalRegistry; + } + + public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent) + { + $this->signalsToDispatchEvent = $signalsToDispatchEvent; } /** @@ -268,14 +279,20 @@ public function doRun(InputInterface $input, OutputInterface $output) $command = $this->find($alternative); } - if ($this->signalRegistry) { - foreach ($this->signalRegistry->getHandlingSignals() as $handlingSignal) { - $event = new ConsoleSignalEvent($command, $input, $output, $handlingSignal); - $onSignalHandler = function () use ($event) { + if ($this->dispatcher) { + foreach ($this->signalsToDispatchEvent as $signal) { + $event = new ConsoleSignalEvent($command, $input, $output, $signal); + + $this->signalRegistry->register($signal, function ($signal, $hasNext) use ($event) { $this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL); - }; - $this->signalRegistry->register($handlingSignal, $onSignalHandler); + // No more handlers, we try to simulate PHP default behavior + if (!$hasNext) { + if (!\in_array($signal, [SIGUSR1, SIGUSR2], true)) { + exit(0); + } + } + }); } } @@ -926,6 +943,12 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI } } + if ($command instanceof SignalableCommandInterface) { + foreach ($command->getSubscribedSignals() as $signal) { + $this->signalRegistry->register($signal, [$command, 'handleSignal']); + } + } + if (null === $this->dispatcher) { return $command->run($input, $output); } diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 8727c6d98abbe..4529bdaa810b3 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -7,6 +7,10 @@ CHANGELOG * Added `SingleCommandApplication::setAutoExit()` to allow testing via `CommandTester` * added support for multiline responses to questions through `Question::setMultiline()` and `Question::isMultiline()` + * Added `SignalRegistry` class to stack signals handlers + * Added support for signals: + * Added `Application::getSignalRegistry()` and `Application::setSignalsToDispatchEvent()` methods + * Added `SignalableCommandInterface` interface 5.1.0 ----- diff --git a/src/Symfony/Component/Console/Command/SignalableCommandInterface.php b/src/Symfony/Component/Console/Command/SignalableCommandInterface.php new file mode 100644 index 0000000000000..d439728b65225 --- /dev/null +++ b/src/Symfony/Component/Console/Command/SignalableCommandInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +/** + * Interface for command reacting to signal. + * + * @author Grégoire Pineau + */ +interface SignalableCommandInterface +{ + /** + * Returns the list of signals to subscribe. + */ + public function getSubscribedSignals(): array; + + /** + * The method will be called when the application is signaled. + */ + public function handleSignal(int $signal): void; +} diff --git a/src/Symfony/Component/Console/SignalRegistry/SignalRegistry.php b/src/Symfony/Component/Console/SignalRegistry/SignalRegistry.php index c8114f29f84cc..de256376d7cb1 100644 --- a/src/Symfony/Component/Console/SignalRegistry/SignalRegistry.php +++ b/src/Symfony/Component/Console/SignalRegistry/SignalRegistry.php @@ -13,26 +13,27 @@ final class SignalRegistry { - private $registeredSignals = []; - - private $handlingSignals = []; + private $signalHandlers = []; public function __construct() { - pcntl_async_signals(true); + if (\function_exists('pcntl_async_signals')) { + pcntl_async_signals(true); + } } public function register(int $signal, callable $signalHandler): void { - if (!isset($this->registeredSignals[$signal])) { + if (!isset($this->signalHandlers[$signal])) { $previousCallback = pcntl_signal_get_handler($signal); if (\is_callable($previousCallback)) { - $this->registeredSignals[$signal][] = $previousCallback; + $this->signalHandlers[$signal][] = $previousCallback; } } - $this->registeredSignals[$signal][] = $signalHandler; + $this->signalHandlers[$signal][] = $signalHandler; + pcntl_signal($signal, [$this, 'handle']); } @@ -41,20 +42,11 @@ public function register(int $signal, callable $signalHandler): void */ public function handle(int $signal): void { - foreach ($this->registeredSignals[$signal] as $signalHandler) { - $signalHandler($signal); - } - } + $count = \count($this->signalHandlers[$signal]); - public function addHandlingSignals(int ...$signals): void - { - foreach ($signals as $signal) { - $this->handlingSignals[$signal] = true; + foreach ($this->signalHandlers[$signal] as $i => $signalHandler) { + $hasNext = $i !== $count - 1; + $signalHandler($signal, $hasNext); } } - - public function getHandlingSignals(): array - { - return array_keys($this->handlingSignals); - } }