Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit af0f5fe

Browse filesBrowse files
committed
[Console] Fix issue with signal handling
1 parent 9b30b94 commit af0f5fe
Copy full SHA for af0f5fe

File tree

5 files changed

+122
-12
lines changed
Filter options

5 files changed

+122
-12
lines changed

‎src/Symfony/Component/Console/Application.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Application.php
+25-8Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,26 +1012,43 @@ 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

1020+
$exitCode = 0;
1021+
if (method_exists($command, 'getExitCode')) {
1022+
$exitCode = $command->getExitCode();
1023+
}
1024+
1025+
// We want command::handleSignal() to be called before the event is
1026+
// dispatched, because at the end of event handler, exit might be
1027+
// called. More over, a listener might want to change the exit code.
1028+
foreach ($commandSignals as $signal) {
1029+
$this->signalRegistry->register($signal, function (int $signal) use ($command, $exitCode): void {
1030+
$command->handleSignal($signal);
1031+
1032+
// Since the event dispatcher is not used, and the class ask to exit,
1033+
// we need to exit ourself.
1034+
if (null !== $exitCode && null === $this->dispatcher) {
1035+
exit($exitCode);
1036+
}
1037+
});
1038+
}
1039+
10221040
if (null !== $this->dispatcher) {
10231041
foreach ($this->signalsToDispatchEvent as $signal) {
1024-
$event = new ConsoleSignalEvent($command, $input, $output, $signal);
1042+
$event = new ConsoleSignalEvent($command, $input, $output, $signal, $exitCode);
10251043

10261044
$this->signalRegistry->register($signal, function () use ($event) {
10271045
$this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL);
1046+
if (null !== ($code = $event->getExitCode())) {
1047+
exit($code);
1048+
}
10281049
});
10291050
}
10301051
}
1031-
1032-
foreach ($commandSignals as $signal) {
1033-
$this->signalRegistry->register($signal, [$command, 'handleSignal']);
1034-
}
10351052
}
10361053

10371054
if (null === $this->dispatcher) {

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Command/SignalableCommandInterface.php
+2Lines 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 <lyrixx@lyrix.info>
18+
*
19+
* @method ?int getExitCode() Returns whether to automatically exit with $exitCode after command handling, or not.
1820
*/
1921
interface SignalableCommandInterface
2022
{

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Event/ConsoleSignalEvent.php
+20-1Lines 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.
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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Tests/ApplicationTest.php
+16-3Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1929,7 +1929,8 @@ public function testSignalListener()
19291929

19301930
$dispatcherCalled = false;
19311931
$dispatcher = new EventDispatcher();
1932-
$dispatcher->addListener('console.signal', function () use (&$dispatcherCalled) {
1932+
$dispatcher->addListener('console.signal', function ($e) use (&$dispatcherCalled) {
1933+
$e->setExitCode(null);
19331934
$dispatcherCalled = true;
19341935
});
19351936

@@ -2044,7 +2045,7 @@ public function testSignalableCommandInterfaceWithoutSignals()
20442045
$this->assertSame(0, $application->run(new ArrayInput(['signal'])));
20452046
}
20462047

2047-
public function testSignalableCommandHandlerCalledAfterEventListener()
2048+
public function testSignalableCommandHandlerCalledBeforeEventListener()
20482049
{
20492050
if (!\defined('SIGUSR1')) {
20502051
$this->markTestSkipped('SIGUSR1 not available');
@@ -2060,7 +2061,7 @@ public function testSignalableCommandHandlerCalledAfterEventListener()
20602061
$application = $this->createSignalableApplication($command, $dispatcher);
20612062
$application->setSignalsToDispatchEvent(\SIGUSR1);
20622063
$this->assertSame(1, $application->run(new ArrayInput(['signal'])));
2063-
$this->assertSame([SignalEventSubscriber::class, SignableCommand::class], $command->signalHandlers);
2064+
$this->assertSame([SignableCommand::class, SignalEventSubscriber::class], $command->signalHandlers);
20642065
}
20652066

20662067
public function testSignalableCommandDoesNotInterruptedOnTermSignals()
@@ -2222,6 +2223,11 @@ public function handleSignal(int $signal): void
22222223
$this->signaled = true;
22232224
$this->signalHandlers[] = __CLASS__;
22242225
}
2226+
2227+
public function getExitCode(): ?int
2228+
{
2229+
return null;
2230+
}
22252231
}
22262232

22272233
#[AsCommand(name: 'signal')]
@@ -2237,6 +2243,11 @@ public function handleSignal(int $signal): void
22372243
$this->signaled = true;
22382244
$this->signalHandlers[] = __CLASS__;
22392245
}
2246+
2247+
public function getExitCode(): ?int
2248+
{
2249+
return null;
2250+
}
22402251
}
22412252

22422253
class SignalEventSubscriber implements EventSubscriberInterface
@@ -2248,6 +2259,8 @@ public function onSignal(ConsoleSignalEvent $event): void
22482259
$this->signaled = true;
22492260
$event->getCommand()->signaled = true;
22502261
$event->getCommand()->signalHandlers[] = __CLASS__;
2262+
2263+
$event->setExitCode(null);
22512264
}
22522265

22532266
public static function getSubscribedEvents(): array
+59Lines 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 (!\defined('SIGINT'));
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
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.