Skip to content

Commit 2d5e7b0

Browse files
committed
feature #33729 [Console] Add signal event (marie)
This PR was merged into the 5.2-dev branch. Discussion ---------- [Console] Add signal event | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | Deprecations? | no | Tickets | Fix #33411 | License | MIT | Doc PR | symfony/symfony-docs#... <!-- required for new features --> This new feature allows to set a listener for performing some actions after the console command get a signal. Usage: ```php use Symfony\Component\Console\Application; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleSignalEvent; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\SignalRegistry\SignalRegistry; use Symfony\Component\Console\Command\Command; class HelloWorldCommand extends Command { protected static $defaultName = 'app:hello-world'; protected function execute(InputInterface $input, OutputInterface $output) {} } $application = new Application(); $dispatcher = new EventDispatcher(); // Function that will handle signals $dispatcher->addListener(ConsoleEvents::SIGNAL, function (ConsoleSignalEvent $event) { echo 'Handled signal #' . $event->getHandlingSignal() . PHP_EOL; }); $application->setDispatcher($dispatcher); $application->setSignalRegistry(new SignalRegistry()); // List of POSIX signals for handling $application->addHandlingSignals(SIGINT, SIGUSR1); $application->add(new HelloWorldCommand()); $application->run(); ``` Commits ------- 859b692 [Console] add console.signal event
2 parents b8529a0 + 859b692 commit 2d5e7b0

File tree

6 files changed

+257
-0
lines changed

6 files changed

+257
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/services.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
use Symfony\Component\Config\Resource\SelfCheckingResourceChecker;
1616
use Symfony\Component\Config\ResourceCheckerConfigCacheFactory;
1717
use Symfony\Component\Console\ConsoleEvents;
18+
use Symfony\Component\Console\Event\ConsoleCommandEvent;
19+
use Symfony\Component\Console\Event\ConsoleErrorEvent;
20+
use Symfony\Component\Console\Event\ConsoleSignalEvent;
21+
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
1822
use Symfony\Component\DependencyInjection\Config\ContainerParametersResourceChecker;
1923
use Symfony\Component\DependencyInjection\EnvVarProcessor;
2024
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBag;

src/Symfony/Component/Console/Application.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
1818
use Symfony\Component\Console\Event\ConsoleCommandEvent;
1919
use Symfony\Component\Console\Event\ConsoleErrorEvent;
20+
use Symfony\Component\Console\Event\ConsoleSignalEvent;
2021
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
2122
use Symfony\Component\Console\Exception\CommandNotFoundException;
2223
use Symfony\Component\Console\Exception\ExceptionInterface;
@@ -39,6 +40,7 @@
3940
use Symfony\Component\Console\Output\ConsoleOutput;
4041
use Symfony\Component\Console\Output\ConsoleOutputInterface;
4142
use Symfony\Component\Console\Output\OutputInterface;
43+
use Symfony\Component\Console\SignalRegistry\SignalRegistry;
4244
use Symfony\Component\Console\Style\SymfonyStyle;
4345
use Symfony\Component\ErrorHandler\ErrorHandler;
4446
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
@@ -76,6 +78,7 @@ class Application implements ResetInterface
7678
private $defaultCommand;
7779
private $singleCommand = false;
7880
private $initialized;
81+
private $signalRegistry;
7982

8083
public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
8184
{
@@ -98,6 +101,11 @@ public function setCommandLoader(CommandLoaderInterface $commandLoader)
98101
$this->commandLoader = $commandLoader;
99102
}
100103

104+
public function setSignalRegistry(SignalRegistry $signalRegistry)
105+
{
106+
$this->signalRegistry = $signalRegistry;
107+
}
108+
101109
/**
102110
* Runs the current application.
103111
*
@@ -260,6 +268,17 @@ public function doRun(InputInterface $input, OutputInterface $output)
260268
$command = $this->find($alternative);
261269
}
262270

271+
if ($this->signalRegistry) {
272+
foreach ($this->signalRegistry->getHandlingSignals() as $handlingSignal) {
273+
$event = new ConsoleSignalEvent($command, $input, $output, $handlingSignal);
274+
$onSignalHandler = function () use ($event) {
275+
$this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL);
276+
};
277+
278+
$this->signalRegistry->register($handlingSignal, $onSignalHandler);
279+
}
280+
}
281+
263282
$this->runningCommand = $command;
264283
$exitCode = $this->doRunCommand($command, $input, $output);
265284
$this->runningCommand = null;

src/Symfony/Component/Console/ConsoleEvents.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Console\Event\ConsoleCommandEvent;
1515
use Symfony\Component\Console\Event\ConsoleErrorEvent;
16+
use Symfony\Component\Console\Event\ConsoleSignalEvent;
1617
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
1718

1819
/**
@@ -31,6 +32,14 @@ final class ConsoleEvents
3132
*/
3233
const COMMAND = 'console.command';
3334

35+
/**
36+
* The SIGNAL event allows you to perform some actions
37+
* after the command execution was interrupted.
38+
*
39+
* @Event("Symfony\Component\Console\Event\ConsoleSignalEvent")
40+
*/
41+
const SIGNAL = 'console.signal';
42+
3443
/**
3544
* The TERMINATE event allows you to attach listeners after a command is
3645
* executed by the console.
@@ -57,6 +66,7 @@ final class ConsoleEvents
5766
const ALIASES = [
5867
ConsoleCommandEvent::class => self::COMMAND,
5968
ConsoleErrorEvent::class => self::ERROR,
69+
ConsoleSignalEvent::class => 'console.signal',
6070
ConsoleTerminateEvent::class => self::TERMINATE,
6171
];
6272
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Event;
13+
14+
use Symfony\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Input\InputInterface;
16+
use Symfony\Component\Console\Output\OutputInterface;
17+
18+
/**
19+
* @author marie <marie@users.noreply.github.com>
20+
*/
21+
final class ConsoleSignalEvent extends ConsoleEvent
22+
{
23+
private $handlingSignal;
24+
25+
public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal)
26+
{
27+
parent::__construct($command, $input, $output);
28+
$this->handlingSignal = $handlingSignal;
29+
}
30+
31+
public function getHandlingSignal(): int
32+
{
33+
return $this->handlingSignal;
34+
}
35+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\SignalRegistry;
13+
14+
final class SignalRegistry
15+
{
16+
private $registeredSignals = [];
17+
18+
private $handlingSignals = [];
19+
20+
public function __construct()
21+
{
22+
pcntl_async_signals(true);
23+
}
24+
25+
public function register(int $signal, callable $signalHandler): void
26+
{
27+
if (!isset($this->registeredSignals[$signal])) {
28+
$previousCallback = pcntl_signal_get_handler($signal);
29+
30+
if (\is_callable($previousCallback)) {
31+
$this->registeredSignals[$signal][] = $previousCallback;
32+
}
33+
}
34+
35+
$this->registeredSignals[$signal][] = $signalHandler;
36+
pcntl_signal($signal, [$this, 'handle']);
37+
}
38+
39+
/**
40+
* @internal
41+
*/
42+
public function handle(int $signal): void
43+
{
44+
foreach ($this->registeredSignals[$signal] as $signalHandler) {
45+
$signalHandler($signal);
46+
}
47+
}
48+
49+
public function addHandlingSignals(int ...$signals): void
50+
{
51+
foreach ($signals as $signal) {
52+
$this->handlingSignals[$signal] = true;
53+
}
54+
}
55+
56+
public function getHandlingSignals(): array
57+
{
58+
return array_keys($this->handlingSignals);
59+
}
60+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Tests\SignalRegistry;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Console\SignalRegistry\SignalRegistry;
16+
17+
/**
18+
* @requires extension pcntl
19+
*/
20+
class SignalRegistryTest extends TestCase
21+
{
22+
public function tearDown(): void
23+
{
24+
pcntl_async_signals(false);
25+
pcntl_signal(SIGUSR1, SIG_DFL);
26+
pcntl_signal(SIGUSR2, SIG_DFL);
27+
}
28+
29+
public function testOneCallbackForASignal_signalIsHandled()
30+
{
31+
$signalRegistry = new SignalRegistry();
32+
33+
$isHandled = false;
34+
$signalRegistry->register(SIGUSR1, function () use (&$isHandled) {
35+
$isHandled = true;
36+
});
37+
38+
posix_kill(posix_getpid(), SIGUSR1);
39+
40+
$this->assertTrue($isHandled);
41+
}
42+
43+
public function testTwoCallbacksForASignal_bothCallbacksAreCalled()
44+
{
45+
$signalRegistry = new SignalRegistry();
46+
47+
$isHandled1 = false;
48+
$signalRegistry->register(SIGUSR1, function () use (&$isHandled1) {
49+
$isHandled1 = true;
50+
});
51+
52+
$isHandled2 = false;
53+
$signalRegistry->register(SIGUSR1, function () use (&$isHandled2) {
54+
$isHandled2 = true;
55+
});
56+
57+
posix_kill(posix_getpid(), SIGUSR1);
58+
59+
$this->assertTrue($isHandled1);
60+
$this->assertTrue($isHandled2);
61+
}
62+
63+
public function testTwoSignals_signalsAreHandled()
64+
{
65+
$signalRegistry = new SignalRegistry();
66+
67+
$isHandled1 = false;
68+
$isHandled2 = false;
69+
70+
$signalRegistry->register(SIGUSR1, function () use (&$isHandled1) {
71+
$isHandled1 = true;
72+
});
73+
74+
posix_kill(posix_getpid(), SIGUSR1);
75+
76+
$this->assertTrue($isHandled1);
77+
$this->assertFalse($isHandled2);
78+
79+
$signalRegistry->register(SIGUSR2, function () use (&$isHandled2) {
80+
$isHandled2 = true;
81+
});
82+
83+
posix_kill(posix_getpid(), SIGUSR2);
84+
85+
$this->assertTrue($isHandled2);
86+
}
87+
88+
public function testTwoCallbacksForASignal_previousAndRegisteredCallbacksWereCalled()
89+
{
90+
$signalRegistry = new SignalRegistry();
91+
92+
$isHandled1 = false;
93+
pcntl_signal(SIGUSR1, function () use (&$isHandled1) {
94+
$isHandled1 = true;
95+
});
96+
97+
$isHandled2 = false;
98+
$signalRegistry->register(SIGUSR1, function () use (&$isHandled2) {
99+
$isHandled2 = true;
100+
});
101+
102+
posix_kill(posix_getpid(), SIGUSR1);
103+
104+
$this->assertTrue($isHandled1);
105+
$this->assertTrue($isHandled2);
106+
}
107+
108+
public function testTwoCallbacksForASignal_previousCallbackFromAnotherRegistry()
109+
{
110+
$signalRegistry1 = new SignalRegistry();
111+
112+
$isHandled1 = false;
113+
$signalRegistry1->register(SIGUSR1, function () use (&$isHandled1) {
114+
$isHandled1 = true;
115+
});
116+
117+
$signalRegistry2 = new SignalRegistry();
118+
119+
$isHandled2 = false;
120+
$signalRegistry2->register(SIGUSR1, function () use (&$isHandled2) {
121+
$isHandled2 = true;
122+
});
123+
124+
posix_kill(posix_getpid(), SIGUSR1);
125+
126+
$this->assertTrue($isHandled1);
127+
$this->assertTrue($isHandled2);
128+
}
129+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy