From 2b46650b9cd3f3765b5751954957ae978355b2f7 Mon Sep 17 00:00:00 2001 From: Gwendolen Lynch Date: Mon, 7 Feb 2022 10:53:23 +0100 Subject: [PATCH] Extract dispatching console signal handling and include subscribers --- src/Symfony/Component/Console/Application.php | 35 ++-- .../Console/Tests/ApplicationTest.php | 161 +++++++++++++++--- 2 files changed, 158 insertions(+), 38 deletions(-) diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index a81cfdcbbc4d..bb6b29edd280 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -974,22 +974,31 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI } } - if ($command instanceof SignalableCommandInterface && ($this->signalsToDispatchEvent || $command->getSubscribedSignals())) { - if (!$this->signalRegistry) { - throw new RuntimeException('Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); - } + if ($this->signalsToDispatchEvent) { + $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : []; + $dispatchSignals = $this->dispatcher && $this->dispatcher->hasListeners(ConsoleEvents::SIGNAL); - if (Terminal::hasSttyAvailable()) { - $sttyMode = shell_exec('stty -g'); + if ($commandSignals || $dispatchSignals) { + if (!$this->signalRegistry) { + throw new RuntimeException('Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); + } - foreach ([\SIGINT, \SIGTERM] as $signal) { - $this->signalRegistry->register($signal, static function () use ($sttyMode) { - shell_exec('stty '.$sttyMode); - }); + if (Terminal::hasSttyAvailable()) { + $sttyMode = shell_exec('stty -g'); + + foreach ([\SIGINT, \SIGTERM] as $signal) { + $this->signalRegistry->register($signal, static function () use ($sttyMode) { + shell_exec('stty '.$sttyMode); + }); + } + } + + foreach ($commandSignals as $signal) { + $this->signalRegistry->register($signal, [$command, 'handleSignal']); } } - if ($this->dispatcher) { + if ($dispatchSignals) { foreach ($this->signalsToDispatchEvent as $signal) { $event = new ConsoleSignalEvent($command, $input, $output, $signal); @@ -1005,10 +1014,6 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI }); } } - - foreach ($command->getSubscribedSignals() as $signal) { - $this->signalRegistry->register($signal, [$command, 'handleSignal']); - } } if (null === $this->dispatcher) { diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index a5918aa3fc81..08e795b5e0bb 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -21,6 +21,7 @@ use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass; 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\NamespaceNotFoundException; @@ -42,6 +43,8 @@ use Symfony\Component\Console\Tester\ApplicationTester; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Process\Process; class ApplicationTest extends TestCase @@ -1843,9 +1846,9 @@ public function testCommandNameMismatchWithCommandLoaderKeyThrows() /** * @requires extension pcntl */ - public function testSignal() + public function testSignalListenerNotCalledByDefault() { - $command = new SignableCommand(); + $command = new SignableCommand(false); $dispatcherCalled = false; $dispatcher = new EventDispatcher(); @@ -1853,29 +1856,97 @@ public function testSignal() $dispatcherCalled = true; }); - $application = new Application(); - $application->setAutoExit(false); - $application->setDispatcher($dispatcher); - $application->setSignalsToDispatchEvent(\SIGALRM); - $application->add(new LazyCommand('signal', [], '', false, function () use ($command) { return $command; }, true)); - - $this->assertFalse($command->signaled); - $this->assertFalse($dispatcherCalled); + $application = $this->createSignalableApplication($command, $dispatcher); $this->assertSame(0, $application->run(new ArrayInput(['signal']))); $this->assertFalse($command->signaled); $this->assertFalse($dispatcherCalled); + } + + /** + * @requires extension pcntl + */ + public function testSignalListener() + { + $command = new SignableCommand(); + + $dispatcherCalled = false; + $dispatcher = new EventDispatcher(); + $dispatcher->addListener('console.signal', function () use (&$dispatcherCalled) { + $dispatcherCalled = true; + }); + + $application = $this->createSignalableApplication($command, $dispatcher); - $command->loop = 100000; - pcntl_alarm(1); $this->assertSame(1, $application->run(new ArrayInput(['signal']))); - $this->assertTrue($command->signaled); $this->assertTrue($dispatcherCalled); + $this->assertTrue($command->signaled); + } + + /** + * @requires extension pcntl + */ + public function testSignalSubscriberNotCalledByDefault() + { + $command = new BaseSignableCommand(false); + + $subscriber = new SignalEventSubscriber(); + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber($subscriber); + + $application = $this->createSignalableApplication($command, $dispatcher); + + $this->assertSame(0, $application->run(new ArrayInput(['signal']))); + $this->assertFalse($subscriber->signaled); + } + + /** + * @requires extension pcntl + */ + public function testSignalSubscriber() + { + $command = new BaseSignableCommand(); + + $subscriber1 = new SignalEventSubscriber(); + $subscriber2 = new SignalEventSubscriber(); + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber($subscriber1); + $dispatcher->addSubscriber($subscriber2); + + $application = $this->createSignalableApplication($command, $dispatcher); + + $this->assertSame(1, $application->run(new ArrayInput(['signal']))); + $this->assertTrue($subscriber1->signaled); + $this->assertTrue($subscriber2->signaled); + } + + /** + * @requires extension pcntl + */ + public function testSetSignalsToDispatchEvent() + { + $command = new BaseSignableCommand(); + + $subscriber = new SignalEventSubscriber(); + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber($subscriber); + + $application = $this->createSignalableApplication($command, $dispatcher); + $application->setSignalsToDispatchEvent(\SIGUSR2); + $this->assertSame(0, $application->run(new ArrayInput(['signal']))); + $this->assertFalse($subscriber->signaled); + + $application = $this->createSignalableApplication($command, $dispatcher); + $application->setSignalsToDispatchEvent(\SIGUSR1); + $this->assertSame(1, $application->run(new ArrayInput(['signal']))); + $this->assertTrue($subscriber->signaled); } public function testSignalableCommandInterfaceWithoutSignals() { - $command = new SignableCommand(); + $command = new SignableCommand(false); $dispatcher = new EventDispatcher(); $application = new Application(); @@ -1917,6 +1988,18 @@ public function testSignalableRestoresStty() $this->assertSame($previousSttyMode, $sttyMode); } + + private function createSignalableApplication(Command $command, ?EventDispatcherInterface $dispatcher): Application + { + $application = new Application(); + $application->setAutoExit(false); + if ($dispatcher) { + $application->setDispatcher($dispatcher); + } + $application->add(new LazyCommand('signal', [], '', false, function () use ($command) { return $command; }, true)); + + return $application; + } } class CustomApplication extends Application @@ -1971,25 +2054,26 @@ public function isEnabled(): bool } } -class SignableCommand extends Command implements SignalableCommandInterface +class BaseSignableCommand extends Command { public $signaled = false; - public $loop = 100; + public $loop = 1000; + private $emitsSignal; protected static $defaultName = 'signal'; - public function getSubscribedSignals(): array + public function __construct(bool $emitsSignal = true) { - return SignalRegistry::isSupported() ? [\SIGALRM] : []; - } - - public function handleSignal(int $signal): void - { - $this->signaled = true; + parent::__construct(); + $this->emitsSignal = $emitsSignal; } protected function execute(InputInterface $input, OutputInterface $output): int { + if ($this->emitsSignal) { + posix_kill(posix_getpid(), SIGUSR1); + } + for ($i = 0; $i < $this->loop; ++$i) { usleep(100); if ($this->signaled) { @@ -2000,3 +2084,34 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } } + +class SignableCommand extends BaseSignableCommand implements SignalableCommandInterface +{ + protected static $defaultName = 'signal'; + + public function getSubscribedSignals(): array + { + return SignalRegistry::isSupported() ? [\SIGUSR1] : []; + } + + public function handleSignal(int $signal): void + { + $this->signaled = true; + } +} + +class SignalEventSubscriber implements EventSubscriberInterface +{ + public $signaled = false; + + public function onSignal(ConsoleSignalEvent $event): void + { + $this->signaled = true; + $event->getCommand()->signaled = true; + } + + public static function getSubscribedEvents(): array + { + return ['console.signal' => 'onSignal']; + } +}