diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php index 4df97ed19c531..2f6d14cd650ce 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php @@ -15,6 +15,10 @@ use Symfony\Component\Config\Resource\SelfCheckingResourceChecker; use Symfony\Component\Config\ResourceCheckerConfigCacheFactory; use Symfony\Component\Console\ConsoleEvents; +use Symfony\Component\Console\Event\ConsoleCommandEvent; +use Symfony\Component\Console\Event\ConsoleErrorEvent; +use Symfony\Component\Console\Event\ConsoleSignalEvent; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\DependencyInjection\Config\ContainerParametersResourceChecker; use Symfony\Component\DependencyInjection\EnvVarProcessor; use Symfony\Component\DependencyInjection\ParameterBag\ContainerBag; diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index f3914bb788ba8..d8ba39fd8f40b 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -17,6 +17,7 @@ use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleErrorEvent; +use Symfony\Component\Console\Event\ConsoleSignalEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\ExceptionInterface; @@ -39,6 +40,7 @@ use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\SignalRegistry\SignalRegistry; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; @@ -76,6 +78,7 @@ class Application implements ResetInterface private $defaultCommand; private $singleCommand = false; private $initialized; + private $signalRegistry; public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN') { @@ -98,6 +101,11 @@ public function setCommandLoader(CommandLoaderInterface $commandLoader) $this->commandLoader = $commandLoader; } + public function setSignalRegistry(SignalRegistry $signalRegistry) + { + $this->signalRegistry = $signalRegistry; + } + /** * Runs the current application. * @@ -260,6 +268,17 @@ 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) { + $this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL); + }; + + $this->signalRegistry->register($handlingSignal, $onSignalHandler); + } + } + $this->runningCommand = $command; $exitCode = $this->doRunCommand($command, $input, $output); $this->runningCommand = null; diff --git a/src/Symfony/Component/Console/ConsoleEvents.php b/src/Symfony/Component/Console/ConsoleEvents.php index 2c1bb46cdeefb..ac0d7da30101a 100644 --- a/src/Symfony/Component/Console/ConsoleEvents.php +++ b/src/Symfony/Component/Console/ConsoleEvents.php @@ -13,6 +13,7 @@ use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleErrorEvent; +use Symfony\Component\Console\Event\ConsoleSignalEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; /** @@ -31,6 +32,14 @@ final class ConsoleEvents */ const COMMAND = 'console.command'; + /** + * The SIGNAL event allows you to perform some actions + * after the command execution was interrupted. + * + * @Event("Symfony\Component\Console\Event\ConsoleSignalEvent") + */ + const SIGNAL = 'console.signal'; + /** * The TERMINATE event allows you to attach listeners after a command is * executed by the console. @@ -57,6 +66,7 @@ final class ConsoleEvents const ALIASES = [ ConsoleCommandEvent::class => self::COMMAND, ConsoleErrorEvent::class => self::ERROR, + ConsoleSignalEvent::class => 'console.signal', ConsoleTerminateEvent::class => self::TERMINATE, ]; } diff --git a/src/Symfony/Component/Console/Event/ConsoleSignalEvent.php b/src/Symfony/Component/Console/Event/ConsoleSignalEvent.php new file mode 100644 index 0000000000000..ef13ed2f5d0b2 --- /dev/null +++ b/src/Symfony/Component/Console/Event/ConsoleSignalEvent.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Event; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author marie + */ +final class ConsoleSignalEvent extends ConsoleEvent +{ + private $handlingSignal; + + public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal) + { + parent::__construct($command, $input, $output); + $this->handlingSignal = $handlingSignal; + } + + public function getHandlingSignal(): int + { + return $this->handlingSignal; + } +} diff --git a/src/Symfony/Component/Console/SignalRegistry/SignalRegistry.php b/src/Symfony/Component/Console/SignalRegistry/SignalRegistry.php new file mode 100644 index 0000000000000..c8114f29f84cc --- /dev/null +++ b/src/Symfony/Component/Console/SignalRegistry/SignalRegistry.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\SignalRegistry; + +final class SignalRegistry +{ + private $registeredSignals = []; + + private $handlingSignals = []; + + public function __construct() + { + pcntl_async_signals(true); + } + + public function register(int $signal, callable $signalHandler): void + { + if (!isset($this->registeredSignals[$signal])) { + $previousCallback = pcntl_signal_get_handler($signal); + + if (\is_callable($previousCallback)) { + $this->registeredSignals[$signal][] = $previousCallback; + } + } + + $this->registeredSignals[$signal][] = $signalHandler; + pcntl_signal($signal, [$this, 'handle']); + } + + /** + * @internal + */ + public function handle(int $signal): void + { + foreach ($this->registeredSignals[$signal] as $signalHandler) { + $signalHandler($signal); + } + } + + public function addHandlingSignals(int ...$signals): void + { + foreach ($signals as $signal) { + $this->handlingSignals[$signal] = true; + } + } + + public function getHandlingSignals(): array + { + return array_keys($this->handlingSignals); + } +} diff --git a/src/Symfony/Component/Console/Tests/SignalRegistry/SignalRegistryTest.php b/src/Symfony/Component/Console/Tests/SignalRegistry/SignalRegistryTest.php new file mode 100644 index 0000000000000..995b27bc0b0de --- /dev/null +++ b/src/Symfony/Component/Console/Tests/SignalRegistry/SignalRegistryTest.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\SignalRegistry; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\SignalRegistry\SignalRegistry; + +/** + * @requires extension pcntl + */ +class SignalRegistryTest extends TestCase +{ + public function tearDown(): void + { + pcntl_async_signals(false); + pcntl_signal(SIGUSR1, SIG_DFL); + pcntl_signal(SIGUSR2, SIG_DFL); + } + + public function testOneCallbackForASignal_signalIsHandled() + { + $signalRegistry = new SignalRegistry(); + + $isHandled = false; + $signalRegistry->register(SIGUSR1, function () use (&$isHandled) { + $isHandled = true; + }); + + posix_kill(posix_getpid(), SIGUSR1); + + $this->assertTrue($isHandled); + } + + public function testTwoCallbacksForASignal_bothCallbacksAreCalled() + { + $signalRegistry = new SignalRegistry(); + + $isHandled1 = false; + $signalRegistry->register(SIGUSR1, function () use (&$isHandled1) { + $isHandled1 = true; + }); + + $isHandled2 = false; + $signalRegistry->register(SIGUSR1, function () use (&$isHandled2) { + $isHandled2 = true; + }); + + posix_kill(posix_getpid(), SIGUSR1); + + $this->assertTrue($isHandled1); + $this->assertTrue($isHandled2); + } + + public function testTwoSignals_signalsAreHandled() + { + $signalRegistry = new SignalRegistry(); + + $isHandled1 = false; + $isHandled2 = false; + + $signalRegistry->register(SIGUSR1, function () use (&$isHandled1) { + $isHandled1 = true; + }); + + posix_kill(posix_getpid(), SIGUSR1); + + $this->assertTrue($isHandled1); + $this->assertFalse($isHandled2); + + $signalRegistry->register(SIGUSR2, function () use (&$isHandled2) { + $isHandled2 = true; + }); + + posix_kill(posix_getpid(), SIGUSR2); + + $this->assertTrue($isHandled2); + } + + public function testTwoCallbacksForASignal_previousAndRegisteredCallbacksWereCalled() + { + $signalRegistry = new SignalRegistry(); + + $isHandled1 = false; + pcntl_signal(SIGUSR1, function () use (&$isHandled1) { + $isHandled1 = true; + }); + + $isHandled2 = false; + $signalRegistry->register(SIGUSR1, function () use (&$isHandled2) { + $isHandled2 = true; + }); + + posix_kill(posix_getpid(), SIGUSR1); + + $this->assertTrue($isHandled1); + $this->assertTrue($isHandled2); + } + + public function testTwoCallbacksForASignal_previousCallbackFromAnotherRegistry() + { + $signalRegistry1 = new SignalRegistry(); + + $isHandled1 = false; + $signalRegistry1->register(SIGUSR1, function () use (&$isHandled1) { + $isHandled1 = true; + }); + + $signalRegistry2 = new SignalRegistry(); + + $isHandled2 = false; + $signalRegistry2->register(SIGUSR1, function () use (&$isHandled2) { + $isHandled2 = true; + }); + + posix_kill(posix_getpid(), SIGUSR1); + + $this->assertTrue($isHandled1); + $this->assertTrue($isHandled2); + } +}