Skip to content

Commit

Permalink
Extract dispatching console signal handling and include subscribers
Browse files Browse the repository at this point in the history
  • Loading branch information
GwendolenLynch authored and nicolas-grekas committed Aug 2, 2022
1 parent 0fc8f7a commit 2b46650
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 38 deletions.
35 changes: 20 additions & 15 deletions src/Symfony/Component/Console/Application.php
Expand Up @@ -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);

Expand All @@ -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) {
Expand Down
161 changes: 138 additions & 23 deletions src/Symfony/Component/Console/Tests/ApplicationTest.php
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -1843,39 +1846,107 @@ public function testCommandNameMismatchWithCommandLoaderKeyThrows()
/**
* @requires extension pcntl
*/
public function testSignal()
public function testSignalListenerNotCalledByDefault()
{
$command = new SignableCommand();
$command = new SignableCommand(false);

$dispatcherCalled = false;
$dispatcher = new EventDispatcher();
$dispatcher->addListener('console.signal', function () use (&$dispatcherCalled) {
$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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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'];
}
}

0 comments on commit 2b46650

Please sign in to comment.