8000 [Console] Fix issue with signal handling · symfony/symfony@af0f5fe · GitHub
[go: up one dir, main page]

Skip to content

Commit af0f5fe

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

File tree

5 files changed

+122
-12
lines changed

5 files changed

+122
-12
lines changed

src/Symfony/Component/Console/Application.php

Lines 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

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

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.
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: 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
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 (!\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