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

Skip to content

Commit 3091710

Browse files
committed
[Console] Fix issue with signal handling
1 parent 9b30b94 commit 3091710

File tree

5 files changed

+206
-9
lines changed

5 files changed

+206
-9
lines changed

src/Symfony/Component/Console/Application.php

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,25 +1012,49 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
10121012
$sttyMode = shell_exec('stty -g');
10131013

10141014
foreach ([\SIGINT, \SIGTERM] as $signal) {
1015-
$this->signalRegistry->register($signal, static function () use ($sttyMode) {
1016-
shell_exec('stty '.$sttyMode);
1017-
});
1015+
$this->signalRegistry->register($signal, static fn () => shell_exec('stty '.$sttyMode));
10181016
}
10191017
}
10201018
}
10211019

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

1026-
$this->signalRegistry->register($signal, function () use ($event) {
1025+
$this->signalRegistry->register($signal, function ($signal) use ($event, $command, $commandSignals) {
10271026
$this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL);
1027+
$exitCode = $event->getExitCode();
1028+
1029+
if (\in_array($signal, $commandSignals, true)) {
1030+
$command->handleSignal($signal);
1031+
// BC layer for Symfony 5.3
1032+
if (method_exists($command, 'getExitCode')) {
1033+
$exitCode = $command->getExitCode($signal, $exitCode);
1034+
}
1035+
}
1036+
if (null !== $exitCode) {
1037+
exit($exitCode);
1038+
}
10281039
});
10291040
}
1041+
1042+
// then we register command signal, but not if already handled by the dispatcher
1043+
$commandSignals = array_diff($commandSignals, $this->signalsToDispatchEvent);
10301044
}
10311045

10321046
foreach ($commandSignals as $signal) {
1033-
$this->signalRegistry->register($signal, [$command, 'handleSignal']);
1047+
$this->signalRegistry->register($signal, function (int $signal) use ($command): void {
1048+
$command->handleSignal($signal);
1049+
$exitCode = 0;
1050+
// BC layer for Symfony 5.3
1051+
if (method_exists($command, 'getExitCode')) {
1052+
$exitCode = $command->getExitCode($signal, $exitCode);
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: 94 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

@@ -2077,9 +2079,36 @@ public function testSignalableCommandDoesNotInterruptedOnTermSignals()
20772079
$application->setAutoExit(false);
20782080
$application->setDispatcher($dispatcher);
20792081
$application->add($command);
2082+
20802083
$this->assertSame(129, $application->run(new ArrayInput(['signal'])));
20812084
}
20822085

2086+
public function testSignalableWithEventCommandDoesNotInterruptedOnTermSignals()
2087+
{
2088+
if (!\defined('SIGINT')) {
2089+
$this->markTestSkipped('SIGINT not available');
2090+
}
2091+
2092+
$command = new TerminatableWithEventCommand();
2093+
2094+
$dispatcher = new EventDispatcher();
2095+
$dispatcher->addSubscriber($command);
2096+
$application = new Application();
2097+
$application->setAutoExit(false);
2098+
$application->setDispatcher($dispatcher);
2099+
$application->add($command);
2100+
$tester = new ApplicationTester($application);
2101+
$this->assertSame(51, $tester->run(['signal']));
2102+
$expected = <<<EOTXT
2103+
Still processing...
2104+
["handling event",2,0]
2105+
["exit code",2,125]
2106+
Wrapping up, wait a sec...
2107+
2108+
EOTXT;
2109+
$this->assertSame($expected, $tester->getDisplay(true));
2110+
}
2111+
20832112
/**
20842113
* @group tty
20852114
*/
@@ -2222,6 +2251,11 @@ public function handleSignal(int $signal): void
22222251
$this->signaled = true;
22232252
$this->signalHandlers[] = __CLASS__;
22242253
}
2254+
2255+
public function getExitCode(int $signal, ?int $previousExitCode): ?int
2256+
{
2257+
return null;
2258+
}
22252259
}
22262260

22272261
#[AsCommand(name: 'signal')]
@@ -2237,6 +2271,63 @@ public function handleSignal(int $signal): void
22372271
$this->signaled = true;
22382272
$this->signalHandlers[] = __CLASS__;
22392273
}
2274+
2275+
public function getExitCode(int $signal, ?int $previousExitCode): ?int
2276+
{
2277+
return null;
2278+
}
2279+
}
2280+
2281+
#[AsCommand(name: 'signal')]
2282+
class TerminatableWithEventCommand extends Command implements SignalableCommandInterface, EventSubscriberInterface
2283+
{
2284+
private bool $shouldContinue = true;
2285+
private OutputInterface $output;
2286+
2287+
protected function execute(InputInterface $input, OutputInterface $output): int
2288+
{
2289+
$this->output = $output;
2290+
2291+
for ($i = 0; $i <= 10 && $this->shouldContinue; ++$i) {
2292+
$output->writeln('Still processing...');
2293+
posix_kill(posix_getpid(), SIGINT);
2294+
}
2295+
2296+
$output->writeln('Wrapping up, wait a sec...');
2297+
2298+
return 51;
2299+
}
2300+
2301+
public function getSubscribedSignals(): array
2302+
{
2303+
return [\SIGINT];
2304+
}
2305+
2306+
public function handleSignal(int $signal): void
2307+
{
2308+
$this->shouldContinue = false;
2309+
}
2310+
2311+
public function getExitCode(int $signal, ?int $previousExitCode): ?int
2312+
{
2313+
$this->output->writeln(json_encode(['exit code', $signal, $previousExitCode]));
2314+
2315+
return null;
2316+
}
2317+
2318+
public function handleSignalEvent(ConsoleSignalEvent $event): void
2319+
{
2320+
$this->output->writeln(json_encode(['handling event', $event->getHandlingSignal(), $event->getExitCode()]));
2321+
2322+
$event->setExitCode(125);
2323+
}
2324+
2325+
public static function getSubscribedEvents(): array
2326+
{
2327+
return [
2328+
ConsoleEvents::SIGNAL => 'handleSignalEvent',
2329+
];
2330+
}
22402331
}
22412332

22422333
class SignalEventSubscriber implements EventSubscriberInterface
@@ -2248,6 +2339,8 @@ public function onSignal(ConsoleSignalEvent $event): void
22482339
$this->signaled = true;
22492340
$event->getCommand()->signaled = true;
22502341
$event->getCommand()->signalHandlers[] = __CLASS__;
2342+
2343+
$event->setExitCode(null);
22512344
}
22522345

22532346
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)