Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 35ccc92

Browse files
committed
[Console] Fix issue with signal handling
1 parent 2fc5f0a commit 35ccc92

File tree

5 files changed

+245
-20
lines changed

5 files changed

+245
-20
lines changed

src/Symfony/Component/Console/Application.php

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,37 +1000,61 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
10001000
}
10011001
}
10021002

1003-
if ($this->signalsToDispatchEvent) {
1004-
$commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [];
1005-
1006-
if ($commandSignals || null !== $this->dispatcher) {
1007-
if (!$this->signalRegistry) {
1008-
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.');
1009-
}
1003+
$commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [];
1004+
if ($commandSignals || null !== $this->dispatcher && $this->signalsToDispatchEvent) {
1005+
if (!$this->signalRegistry) {
1006+
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.');
1007+
}
10101008

1011-
if (Terminal::hasSttyAvailable()) {
1012-
$sttyMode = shell_exec('stty -g');
1009+
if (Terminal::hasSttyAvailable()) {
1010+
$sttyMode = shell_exec('stty -g');
10131011

1014-
foreach ([\SIGINT, \SIGTERM] as $signal) {
1015-
$this->signalRegistry->register($signal, static function () use ($sttyMode) {
1016-
shell_exec('stty '.$sttyMode);
1017-
});
1018-
}
1012+
foreach ([\SIGINT, \SIGTERM] as $signal) {
1013+
$this->signalRegistry->register($signal, static fn () => shell_exec('stty '.$sttyMode));
10191014
}
10201015
}
10211016

1022-
if (null !== $this->dispatcher) {
1017+
if ($this->dispatcher) {
1018+
// We register application signals, so that we can dispatch the event
10231019
foreach ($this->signalsToDispatchEvent as $signal) {
1024-
$event = new ConsoleSignalEvent($command, $input, $output, $signal);
1020+
$event = new ConsoleSignalEvent($command, $input, $output, $signal, 0);
10251021

1026-
$this->signalRegistry->register($signal, function () use ($event) {
1022+
$this->signalRegistry->register($signal, function ($signal) use ($event, $command, $commandSignals) {
10271023
$this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL);
1024+
$exitCode = $event->getExitCode();
1025+
1026+
// If the command is signalable, we call the handleSignal() method
1027+
if (\in_array($signal, $commandSignals, true)) {
1028+
$command->handleSignal($signal);
1029+
// BC layer for Symfony <5.3
1030+
if (method_exists($command, 'getExitCode')) {
1031+
$exitCode = $command->getExitCode($signal, $exitCode);
1032+
}
1033+
}
1034+
1035+
if (null !== $exitCode) {
1036+
exit($exitCode);
1037+
}
10281038
});
10291039
}
1040+
1041+
// then we register command signals, but not if already handled after the dispatcher
1042+
$commandSignals = array_diff($commandSignals, $this->signalsToDispatchEvent);
10301043
}
10311044

10321045
foreach ($commandSignals as $signal) {
1033-
$this->signalRegistry->register($signal, [$command, 'handleSignal']);
1046+
$this->signalRegistry->register($signal, function (int $signal) use ($command): void {
1047+
$command->handleSignal($signal);
1048+
$exitCode = 0;
1049+
// BC layer for Symfony <5.3
1050+
if (method_exists($command, 'getExitCode')) {
1051+
$exitCode = $command->getExitCode($signal, $exitCode);
1052+
}
1053+
1054+
if (null !== $exitCode) {
1055+
exit($exitCode);
1056+
}
1057+
});
10341058
}
10351059
}
10361060

src/Symfony/Component/Console/Command/SignalableCommandInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
* Interface for command reacting to signal.
1616
*
1717
* @author Grégoire Pineau <[email protected]>
18+
*
19+
* @method ?int getExitCode(int $signal, ?int $previousExitCode) Returns whether to automatically exit with $exitCode after command handling, or not with null.
1820
*/
1921
interface SignalableCommandInterface
2022
{

src/Symfony/Component/Console/Event/ConsoleSignalEvent.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,34 @@
2121
final class ConsoleSignalEvent extends ConsoleEvent
2222
{
2323
private int $handlingSignal;
24+
private ?int $exitCode;
2425

25-
public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal)
26+
public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal, ?int $exitCode = 0)
2627
{
2728
parent::__construct($command, $input, $output);
2829
$this->handlingSignal = $handlingSignal;
30+
$this->exitCode = $exitCode;
2931
}
3032

3133
public function getHandlingSignal(): int
3234
{
3335
return $this->handlingSignal;
3436
}
37+
38+
/**
39+
* Sets whether to automatically exit with $exitCode after command handling, or not with null.
40+
*/
41+
public function setExitCode(?int $exitCode): void
42+
{
43+
if (null !== $exitCode && ($exitCode < 0 || $exitCode > 255)) {
44+
throw new \InvalidArgumentException('Exit code must be between 0 and 255 or null.');
45+
}
46+
47+
$this->exitCode = $exitCode;
48+
}
49+
50+
public function getExitCode(): ?int
51+
{
52+
return $this->exitCode;
53+
}
3554
}

src/Symfony/Component/Console/Tests/ApplicationTest.php

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Symfony\Component\Console\Command\SignalableCommandInterface;
2121
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
2222
use Symfony\Component\Console\CommandLoader\FactoryCommandLoader;
23+
use Symfony\Component\Console\ConsoleEvents;
2324
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
2425
use Symfony\Component\Console\Event\ConsoleCommandEvent;
2526
use Symfony\Component\Console\Event\ConsoleErrorEvent;
@@ -1929,7 +1930,8 @@ public function testSignalListener()
19291930

19301931
$dispatcherCalled = false;
19311932
$dispatcher = new EventDispatcher();
1932-
$dispatcher->addListener('console.signal', function () use (&$dispatcherCalled) {
1933+
$dispatcher->addListener('console.signal', function ($e) use (&$dispatcherCalled) {
1934+
$e->setExitCode(null);
19331935
$dispatcherCalled = true;
19341936
});
19351937

@@ -1978,6 +1980,34 @@ public function testSignalSubscriber()
19781980
$this->assertTrue($subscriber2->signaled);
19791981
}
19801982

1983+
/**
1984+
* @requires extension pcntl
1985+
*/
1986+
public function testSignalDispatchWithoutEventToDispatch()
1987+
{
1988+
$command = new SignableCommand();
1989+
1990+
$application = $this->createSignalableApplication($command, null);
1991+
$application->setSignalsToDispatchEvent();
1992+
1993+
$this->assertSame(1, $application->run(new ArrayInput(['signal'])));
1994+
$this->assertTrue($command->signaled);
1995+
}
1996+
1997+
/**
1998+
* @requires extension pcntl
1999+
*/
2000+
public function testSignalDispatchWithoutEventDispatcher()
2001+
{
2002+
$command = new SignableCommand();
2003+
2004+
$application = $this->createSignalableApplication($command, null);
2005+
$application->setSignalsToDispatchEvent(\SIGUSR1);
2006+
2007+
$this->assertSame(1, $application->run(new ArrayInput(['signal'])));
2008+
$this->assertTrue($command->signaled);
2009+
}
2010+
19812011
/**
19822012
* @requires extension pcntl
19832013
*/
@@ -2077,9 +2107,36 @@ public function testSignalableCommandDoesNotInterruptedOnTermSignals()
20772107
$application->setAutoExit(false);
20782108
$application->setDispatcher($dispatcher);
20792109
$application->add($command);
2110+
20802111
$this->assertSame(129, $application->run(new ArrayInput(['signal'])));
20812112
}
20822113

2114+
public function testSignalableWithEventCommandDoesNotInterruptedOnTermSignals()
2115+
{
2116+
if (!\defined('SIGINT')) {
2117+
$this->markTestSkipped('SIGINT not available');
2118+
}
2119+
2120+
$command = new TerminatableWithEventCommand();
2121+
2122+
$dispatcher = new EventDispatcher();
2123+
$dispatcher->addSubscriber($command);
2124+
$application = new Application();
2125+
$application->setAutoExit(false);
2126+
$application->setDispatcher($dispatcher);
2127+
$application->add($command);
2128+
$tester = new ApplicationTester($application);
2129+
$this->assertSame(51, $tester->run(['signal']));
2130+
$expected = <<<EOTXT
2131+
Still processing...
2132+
["handling event",2,0]
2133+
["exit code",2,125]
2134+
Wrapping up, wait a sec...
2135+
2136+
EOTXT;
2137+
$this->assertSame($expected, $tester->getDisplay(true));
2138+
}
2139+
20832140
/**
20842141
* @group tty
20852142
*/
@@ -2222,6 +2279,11 @@ public function handleSignal(int $signal): void
22222279
$this->signaled = true;
22232280
$this->signalHandlers[] = __CLASS__;
22242281
}
2282+
2283+
public function getExitCode(int $signal, ?int $previousExitCode): ?int
2284+
{
2285+
return null;
2286+
}
22252287
}
22262288

22272289
#[AsCommand(name: 'signal')]
@@ -2237,6 +2299,63 @@ public function handleSignal(int $signal): void
22372299
$this->signaled = true;
22382300
$this->signalHandlers[] = __CLASS__;
22392301
}
2302+
2303+
public function getExitCode(int $signal, ?int $previousExitCode): ?int
2304+
{
2305+
return null;
2306+
}
2307+
}
2308+
2309+
#[AsCommand(name: 'signal')]
2310+
class TerminatableWithEventCommand extends Command implements SignalableCommandInterface, EventSubscriberInterface
2311+
{
2312+
private bool $shouldContinue = true;
2313+
private OutputInterface $output;
2314+
2315+
protected function execute(InputInterface $input, OutputInterface $output): int
2316+
{
2317+
$this->output = $output;
2318+
2319+
for ($i = 0; $i <= 10 && $this->shouldContinue; ++$i) {
2320+
$output->writeln('Still processing...');
2321+
posix_kill(posix_getpid(), SIGINT);
2322+
}
2323+
2324+
$output->writeln('Wrapping up, wait a sec...');
2325+
2326+
return 51;
2327+
}
2328+
2329+
public function getSubscribedSignals(): array
2330+
{
2331+
return [\SIGINT];
2332+
}
2333+
2334+
public function handleSignal(int $signal): void
2335+
{
2336+
$this->shouldContinue = false;
2337+
}
2338+
2339+
public function getExitCode(int $signal, ?int $previousExitCode): ?int
2340+
{
2341+
$this->output->writeln(json_encode(['exit code', $signal, $previousExitCode]));
2342+
2343+
return null;
2344+
}
2345+
2346+
public function handleSignalEvent(ConsoleSignalEvent $event): void
2347+
{
2348+
$this->output->writeln(json_encode(['handling event', $event->getHandlingSignal(), $event->getExitCode()]));
2349+
2350+
$event->setExitCode(125);
2351+
}
2352+
2353+
public static function getSubscribedEvents(): array
2354+
{
2355+
return [
2356+
ConsoleEvents::SIGNAL => 'handleSignalEvent',
2357+
];
2358+
}
22402359
}
22412360

22422361
class SignalEventSubscriber implements EventSubscriberInterface
@@ -2248,6 +2367,8 @@ public function onSignal(ConsoleSignalEvent $event): void
22482367
$this->signaled = true;
22492368
$event->getCommand()->signaled = true;
22502369
$event->getCommand()->signalHandlers[] = __CLASS__;
2370+
2371+
$event->setExitCode(null);
22512372
}
22522373

22532374
public static function getSubscribedEvents(): array
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
--TEST--
2+
Test command that exist
3+
--SKIPIF--
4+
<?php if (!extension_loaded("pcntl")) die("Skipped: pcntl extension required."); ?>
5+
--FILE--
6+
<?php
7+
8+
use Symfony\Component\Console\Application;
9+
use Symfony\Component\Console\Command\Command;
10+
use Symfony\Component\Console\Command\SignalableCommandInterface;
11+
use Symfony\Component\Console\Helper\QuestionHelper;
12+
use Symfony\Component\Console\Input\InputInterface;
13+
use Symfony\Component\Console\Output\OutputInterface;
14+
use Symfony\Component\Console\Question\Question;
15+
16+
$vendor = __DIR__;
17+
while (!file_exists($vendor.'/vendor')) {
18+
$vendor = \dirname($vendor);
19+
}
20+
require $vendor.'/vendor/autoload.php';
21+
22+
class MyCommand extends Command implements SignalableCommandInterface
23+
{
24+
protected function execute(InputInterface $input, OutputInterface $output): int
25+
{
26+
posix_kill(posix_getpid(), \SIGINT);
27+
28+
$output->writeln('should not be displayed');
29+
30+
return 0;
31+
}
32+
33+
34+
public function getSubscribedSignals(): array
35+
{
36+
return [\SIGINT];
37+
}
38+
39+
public function handleSignal(int $signal): void
40+
{
41+
echo "Received signal!";
42+
}
43+
44+
public function getExitCode(int $signal, ?int $previousExitCode): ?int
45+
{
46+
return 12;
47+
}
48+
}
49+
50+
$app = new Application();
51+
$app->setDispatcher(new \Symfony\Component\EventDispatcher\EventDispatcher());
52+
$app->add(new MyCommand('foo'));
53+
54+
$app
55+
->setDefaultCommand('foo', true)
56+
->run()
57+
;
58+
--EXPECT--
59+
Received signal!

0 commit comments

Comments
 (0)