Skip to content

Commit d6ccc4f

Browse files
committed
feature #37827 [Console] Rework the signal integration (lyrixx)
This PR was merged into the 5.2-dev branch. Discussion ---------- [Console] Rework the signal integration | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | refs #33729 | License | MIT | Doc PR | I updated the code to have a better design and DX: * `SignalRegistry::$handlingSignals` is gone. This was a temporary data store to bind signal to symfony event. This does not belong to the `SignalRegistry`. It has been replaced by `Application::$signalsToDispatchEvent`. A method has been added to edit this list `Application::setSignalsToDispatchEvent()` * The default value for `Application::$signalsToDispatchEvent` is `SIGINT, SIGTERM, SIGUSR1, SIGUSR2`. Theses defaults seems good enough for most of application. for recall: * `SIGINT`: CTRL+C * `SIGTERM`: `kill PID`, this is what occurs when stopping a process with SystemD, upstart & co * `SIGUSR1` and `SIGUSR2`: Signal for user space * `Application::setSignalRegistry()` is gone. Now the Application always owns a signal registry. Since it's a CLI, it's legit to always have support for signals. A method has been added for convenience, and to register signal handler manually from the command: ```php $application->getSignalRegistry()->register(SIGINT, function ($signal) { dump("Signal ($signal) caught"); }); ``` * The interface `SignalableCommandInterface` has been added. When a command implements this interface, the command is automatically called when a registered signal has been caught A note about the BC: * If one register an handler before the Symfony ones, the Signal Registry keeps existing registered handlers. The BC is kept ✅ * If one register an handler after the Symfony ones, it overrides the Symfony behavior. Since the feature is new. the BC is kept ✅ --- So now, If one want to listen a signal, they have few options depending on the context: * A global action is common to all commands (such as logging, enabling a profiler (👋 blackfire.io)): Use a listener. With autoconfigure, the following code is enough: ```php class SignalSubscriber implements EventSubscriberInterface { private $logger; public function __construct(LoggerInterface $logger = null) { $this->logger = $logger ?: new NullLogger(); } public function handleSignal(ConsoleSignalEvent $event) { $signal = $event->getHandlingSignal(); $this->logger->debug('The application has been signaled', [ 'signal' => $signal, ]); } public static function getSubscribedEvents() { return [ ConsoleEvents::SIGNAL => 'handleSignal', ]; } } ``` * The command should react to a signal: Implements the interface: ```php class SignalCommand extends Command implements SignalableCommandInterface { protected static $defaultName = 'signal'; private $shouldStop = false; protected function execute(InputInterface $input, OutputInterface $output): int { $output->writeln('hello, I am '. getmypid()); for ($i=0; $i < 60; $i++) { if ($this->shouldStop) { break; } $output->write('.'); sleep(1); } $output->writeln(''); $output->writeln('bye'); return 0; } public function handleSignal(int $signal) { dump([__METHOD__, $signal]); $this->shouldStop = true; } public function getSubscribedSignals(): array { return [SIGINT]; } } ``` * The command should react differently to many event and/or one wants a full control on name of the method called ```php class SignalCommand extends Command { protected static $defaultName = 'signal'; private $shouldStop = false; protected function execute(InputInterface $input, OutputInterface $output): int { $this->getApplication()->getSignalRegistry()->register(SIGINT, [$this, 'stop']); $this->getApplication()->getSignalRegistry()->register(SIGUSR1, [$this, 'enableBlackfire']); $output->writeln('hello, I am '. getmypid()); for ($i=0; $i < 60; $i++) { if ($this->shouldStop) { break; } $output->write('.'); sleep(1); } $output->writeln(''); $output->writeln('bye'); return 0; } public function stop() { $this->shouldStop = true; } public function enableBlackfire() { // ... } } ``` ping @marie Commits ------- df57119 [Console] Rework the signal integration
2 parents ae677cc + df57119 commit d6ccc4f

File tree

4 files changed

+77
-28
lines changed

4 files changed

+77
-28
lines changed

src/Symfony/Component/Console/Application.php

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Console\Command\Command;
1515
use Symfony\Component\Console\Command\HelpCommand;
1616
use Symfony\Component\Console\Command\ListCommand;
17+
use Symfony\Component\Console\Command\SignalableCommandInterface;
1718
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
1819
use Symfony\Component\Console\Event\ConsoleCommandEvent;
1920
use Symfony\Component\Console\Event\ConsoleErrorEvent;
@@ -79,13 +80,18 @@ class Application implements ResetInterface
7980
private $singleCommand = false;
8081
private $initialized;
8182
private $signalRegistry;
83+
private $signalsToDispatchEvent = [];
8284

8385
public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN')
8486
{
8587
$this->name = $name;
8688
$this->version = $version;
8789
$this->terminal = new Terminal();
8890
$this->defaultCommand = 'list';
91+
$this->signalRegistry = new SignalRegistry();
92+
if (\defined('SIGINT')) {
93+
$this->signalsToDispatchEvent = [SIGINT, SIGTERM, SIGUSR1, SIGUSR2];
94+
}
8995
}
9096

9197
/**
@@ -101,9 +107,14 @@ public function setCommandLoader(CommandLoaderInterface $commandLoader)
101107
$this->commandLoader = $commandLoader;
102108
}
103109

104-
public function setSignalRegistry(SignalRegistry $signalRegistry)
110+
public function getSignalRegistry(): SignalRegistry
105111
{
106-
$this->signalRegistry = $signalRegistry;
112+
return $this->signalRegistry;
113+
}
114+
115+
public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent)
116+
{
117+
$this->signalsToDispatchEvent = $signalsToDispatchEvent;
107118
}
108119

109120
/**
@@ -268,14 +279,20 @@ public function doRun(InputInterface $input, OutputInterface $output)
268279
$command = $this->find($alternative);
269280
}
270281

271-
if ($this->signalRegistry) {
272-
foreach ($this->signalRegistry->getHandlingSignals() as $handlingSignal) {
273-
$event = new ConsoleSignalEvent($command, $input, $output, $handlingSignal);
274-
$onSignalHandler = function () use ($event) {
282+
if ($this->dispatcher) {
283+
foreach ($this->signalsToDispatchEvent as $signal) {
284+
$event = new ConsoleSignalEvent($command, $input, $output, $signal);
285+
286+
$this->signalRegistry->register($signal, function ($signal, $hasNext) use ($event) {
275287
$this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL);
276-
};
277288

278-
$this->signalRegistry->register($handlingSignal, $onSignalHandler);
289+
// No more handlers, we try to simulate PHP default behavior
290+
if (!$hasNext) {
291+
if (!\in_array($signal, [SIGUSR1, SIGUSR2], true)) {
292+
exit(0);
293+
}
294+
}
295+
});
279296
}
280297
}
281298

@@ -926,6 +943,12 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
926943
}
927944
}
928945

946+
if ($command instanceof SignalableCommandInterface) {
947+
foreach ($command->getSubscribedSignals() as $signal) {
948+
$this->signalRegistry->register($signal, [$command, 'handleSignal']);
949+
}
950+
}
951+
929952
if (null === $this->dispatcher) {
930953
return $command->run($input, $output);
931954
}

src/Symfony/Component/Console/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ CHANGELOG
77
* Added `SingleCommandApplication::setAutoExit()` to allow testing via `CommandTester`
88
* added support for multiline responses to questions through `Question::setMultiline()`
99
and `Question::isMultiline()`
10+
* Added `SignalRegistry` class to stack signals handlers
11+
* Added support for signals:
12+
* Added `Application::getSignalRegistry()` and `Application::setSignalsToDispatchEvent()` methods
13+
* Added `SignalableCommandInterface` interface
1014

1115
5.1.0
1216
-----
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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\Command;
13+
14+
/**
15+
* Interface for command reacting to signal.
16+
*
17+
* @author Grégoire Pineau <lyrixx@lyrix.info>
18+
*/
19+
interface SignalableCommandInterface
20+
{
21+
/**
22+
* Returns the list of signals to subscribe.
23+
*/
24+
public function getSubscribedSignals(): array;
25+
26+
/**
27+
* The method will be called when the application is signaled.
28+
*/
29+
public function handleSignal(int $signal): void;
30+
}

src/Symfony/Component/Console/SignalRegistry/SignalRegistry.php

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,27 @@
1313

1414
final class SignalRegistry
1515
{
16-
private $registeredSignals = [];
17-
18-
private $handlingSignals = [];
16+
private $signalHandlers = [];
1917

2018
public function __construct()
2119
{
22-
pcntl_async_signals(true);
20+
if (\function_exists('pcntl_async_signals')) {
21+
pcntl_async_signals(true);
22+
}
2323
}
2424

2525
public function register(int $signal, callable $signalHandler): void
2626
{
27-
if (!isset($this->registeredSignals[$signal])) {
27+
if (!isset($this->signalHandlers[$signal])) {
2828
$previousCallback = pcntl_signal_get_handler($signal);
2929

3030
if (\is_callable($previousCallback)) {
31-
$this->registeredSignals[$signal][] = $previousCallback;
31+
$this->signalHandlers[$signal][] = $previousCallback;
3232
}
3333
}
3434

35-
$this->registeredSignals[$signal][] = $signalHandler;
35+
$this->signalHandlers[$signal][] = $signalHandler;
36+
3637
pcntl_signal($signal, [$this, 'handle']);
3738
}
3839

@@ -41,20 +42,11 @@ public function register(int $signal, callable $signalHandler): void
4142
*/
4243
public function handle(int $signal): void
4344
{
44-
foreach ($this->registeredSignals[$signal] as $signalHandler) {
45-
$signalHandler($signal);
46-
}
47-
}
45+
$count = \count($this->signalHandlers[$signal]);
4846

49-
public function addHandlingSignals(int ...$signals): void
50-
{
51-
foreach ($signals as $signal) {
52-
$this->handlingSignals[$signal] = true;
47+
foreach ($this->signalHandlers[$signal] as $i => $signalHandler) {
48+
$hasNext = $i !== $count - 1;
49+
$signalHandler($signal, $hasNext);
5350
}
5451
}
55-
56-
public function getHandlingSignals(): array
57-
{
58-
return array_keys($this->handlingSignals);
59-
}
6052
}

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