8000 [Console] Add `AlarmableCommandInterface` and `console.alarm` event · symfony/symfony@e42a38a · GitHub
[go: up one dir, main page]

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 e42a38a

Browse files
committed
[Console] Add AlarmableCommandInterface and console.alarm event
1 parent db21ee4 commit e42a38a

File tree

11 files changed

+578
-24
lines changed

11 files changed

+578
-24
lines changed

src/Symfony/Bundle/FrameworkBundle/Console/Application.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
namespace Symfony\Bundle\FrameworkBundle\Console;
1313

1414
use Symfony\Component\Console\Application as BaseApplication;
15+
use Symfony\Component\Console\Command\AlarmableCommandInterface;
1516
use Symfony\Component\Console\Command\Command;
1617
use Symfony\Component\Console\Command\ListCommand;
18+
use Symfony\Component\Console\Command\TraceableAlarmableCommand;
1719
use Symfony\Component\Console\Command\TraceableCommand;
1820
use Symfony\Component\Console\Debug\CliRequest;
1921
use Symfony\Component\Console\Input\InputInterface;
@@ -112,7 +114,10 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
112114

113115
(new SymfonyStyle($input, $output))->warning('The "--profile" option needs the profiler integration. Try enabling the "framework.profiler" option.');
114116
} else {
115-
$command = new TraceableCommand($command, $container->get('debug.stopwatch'));
117+
$command = $command instanceof AlarmableCommandInterface
118+
? new TraceableAlarmableCommand($command, $container->get('debug.stopwatch'))
119+
: new TraceableCommand($command, $container->get('debug.stopwatch'))
120+
;
116121

117122
$requestStack = $container->get('.virtual_request_stack');
118123
$requestStack->push(new CliRequest($command));

src/Symfony/Component/Console/Application.php

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Console;
1313

14+
use Symfony\Component\Console\Command\AlarmableCommandInterface;
1415
use Symfony\Component\Console\Command\Command;
1516
use Symfony\Component\Console\Command\CompleteCommand;
1617
use Symfony\Component\Console\Command\DumpCompletionCommand;
@@ -22,6 +23,7 @@
2223
use Symfony\Component\Console\Completion\CompletionInput;
2324
use Symfony\Component\Console\Completion\CompletionSuggestions;
2425
use Symfony\Component\Console\Completion\Suggestion;
26+
use Symfony\Component\Console\Event\ConsoleAlarmEvent;
2527
use Symfony\Component\Console\Event\ConsoleCommandEvent;
2628
use Symfony\Component\Console\Event\ConsoleErrorEvent;
2729
use Symfony\Component\Console\Event\ConsoleSignalEvent;
@@ -97,7 +99,7 @@ public function __construct(
9799
$this->defaultCommand = 'list';
98100
if (\defined('SIGINT') && SignalRegistry::isSupported()) {
99101
$this->signalRegistry = new SignalRegistry();
100-
$this->signalsToDispatchEvent = [\SIGINT, \SIGTERM, \SIGUSR1, \SIGUSR2];
102+
$this->signalsToDispatchEvent = [\SIGINT, \SIGTERM, \SIGUSR1, \SIGUSR2, \SIGALRM];
101103
}
102104
}
103105

@@ -975,7 +977,18 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
975977
}
976978
}
977979

980+
// bind before getAlarmTime() and the console.command event, so the method and listeners have access to input options/arguments
981+
try {
982+
$command->mergeApplicationDefinition();
983+
$input->bind($command->getDefinition());
984+
} catch (ExceptionInterface) {
985+
// ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition
986+
}
987+
978988
$commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [];
989+
if ($command instanceof AlarmableCommandInterface && \defined('SIGALRM') && SignalRegistry::isSupported() && !\in_array(\SIGALRM, $commandSignals, true)) {
990+
$commandSignals[] = \SIGALRM;
991+
}
979992
if ($commandSignals || $this->dispatcher && $this->signalsToDispatchEvent) {
980993
if (!$this->signalRegistry) {
981994
throw new RuntimeException('Unable to subscribe to signal events. Make sure that the "pcntl" extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.');
@@ -992,19 +1005,34 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
9921005
if ($this->dispatcher) {
9931006
// We register application signals, so that we can dispatch the event
9941007
foreach ($this->signalsToDispatchEvent as $signal) {
995-
$event = new ConsoleSignalEvent($command, $input, $output, $signal);
996-
997-
$this->signalRegistry->register($signal, function ($signal) use ($event, $command, $commandSignals) {
998-
$this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL);
999-
$exitCode = $event->getExitCode();
1008+
$signalEvent = new ConsoleSignalEvent($command, $input, $output, $signal);
1009+
$alarmEvent = \SIGALRM === $signal ? new ConsoleAlarmEvent($command, $input, $output) : null;
1010+
1011+
$this->signalRegistry->register($signal, function ($signal) use ($signalEvent, $alarmEvent, $command, $commandSignals, $input, $output) {
1012+
$this->dispatcher->dispatch($signalEvent, ConsoleEvents::SIGNAL);
1013+
$exitCode = $signalEvent->getExitCode();
1014+
1015+
if (null !== $alarmEvent) {
1016+
if (false !== $exitCode) {
1017+
$alarmEvent->setExitCode($exitCode);
1018+
} else {
1019+
$alarmEvent->abortExit();
1020+
}
1021+
$this->dispatcher->dispatch($alarmEvent, ConsoleEvents::ALARM);
1022+
$exitCode = $alarmEvent->getExitCode();
1023+
}
10001024

1025+
if (\SIGALRM === $signal && $command instanceof AlarmableCommandInterface) {
1026+
$exitCode = $command->handleAlarm($exitCode);
1027+
$this->signalRegistry->scheduleAlarm($command->getAlarmTime($input));
1028+
}
10011029
// If the command is signalable, we call the handleSignal() method
1002-
if (\in_array($signal, $commandSignals, true)) {
1030+
elseif (\in_array($signal, $commandSignals, true)) {
10031031
$exitCode = $command->handleSignal($signal, $exitCode);
10041032
}
10051033

10061034
if (false !== $exitCode) {
1007-
$event = new ConsoleTerminateEvent($command, $event->getInput(), $event->getOutput(), $exitCode, $signal);
1035+
$event = new ConsoleTerminateEvent($command, $input, $output, $exitCode, $signal);
10081036
$this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE);
10091037

10101038
exit($event->getExitCode());
@@ -1017,26 +1045,29 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
10171045
}
10181046

10191047
foreach ($commandSignals as $signal) {
1020-
$this->signalRegistry->register($signal, function (int $signal) use ($command): void {
1021-
if (false !== $exitCode = $command->handleSignal($signal)) {
1048+
$this->signalRegistry->register($signal, function (int $signal) use ($command, $input): void {
1049+
if (\SIGALRM === $signal && $command instanceof AlarmableCommandInterface) {
1050+
$exitCode = $command->handleAlarm();
1051+
$this->signalRegistry->scheduleAlarm($command->getAlarmTime($input));
1052+
} else {
1053+
$exitCode = $command->handleSignal($signal);
1054+
}
1055+
1056+
if (false !== $exitCode) {
10221057
exit($exitCode);
10231058
}
10241059
});
10251060
}
1061+
1062+
if ($command instanceof AlarmableCommandInterface) {
1063+
$this->signalRegistry->scheduleAlarm($command->getAlarmTime($input));
1064+
}
10261065
}
10271066

10281067
if (null === $this->dispatcher) {
10291068
return $command->run($input, $output);
10301069
}
10311070

1032-
// bind before the console.command event, so the listeners have access to input options/arguments
1033-
try {
1034-
$command->mergeApplicationDefinition();
1035-
$input->bind($command->getDefinition());
1036-
} catch (ExceptionInterface) {
1037-
// ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition
1038-
}
1039-
10401071
$event = new ConsoleCommandEvent($command, $input, $output);
10411072
$e = null;
10421073

src/Symfony/Component/Console/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.1
5+
---
6+
7+
* Add `AlarmableCommandInterface` and `console.alarm` event
8+
49
7.0
510
---
611

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
use Symfony\Component\Console\Input\InputInterface;
15+
16+
/**
17+
* Interface for command that listens to SIGALRM signals.
18+
*/
19+
interface AlarmableCommandInterface
20+
{
21+
/**
22+
* The method will be called before the command is run and subsequently on each SIGALRM signal.
23+
*
24+
* @return int The alarm time in seconds
25+
*/
26+
public function getAlarmTime(InputInterface $input): int;
27+
28+
/**
29+
* The method will be called when the application is signaled with SIGALRM.
30+
*
31+
* @return int|false The exit code to return or false to continue the normal execution
32+
*/
33+
public function handleAlarm(int|false $previousExitCode = 0): int|false;
34+
}
Lines changed: 57 additions & 0 deletions
10000
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
use Symfony\Component\Console\Input\InputInterface;
15+
use Symfony\Component\Stopwatch\Stopwatch;
16+
17+
/**
18+
* @internal
19+
*
20+
* @property Command&AlarmableCommandInterface $command
21+
*/
22+
final class TraceableAlarmableCommand extends TraceableCommand implements AlarmableCommandInterface
23+
{
24+
public function __construct(Command&AlarmableCommandInterface $command, Stopwatch $stopwatch)
25+
{
26+
parent::__construct($command, $stopwatch);
27+
}
28+
29+
public function getSubscribedSignals(): array
30+
{
31+
$commandSignals = parent::getSubscribedSignals();
32+
33+
if (!\in_array(\SIGALRM, $commandSignals, true)) {
34+
$commandSignals[] = \SIGALRM;
35+
}
36+
37+
return $commandSignals;
38+
}
39+
40+
public function getAlarmTime(InputInterface $input): int
41+
{
42+
return $this->command->getAlarmTime($input);
43+
}
44+
45+
public function handleAlarm(false|int $previousExitCode = 0): int|false
46+
{
47+
$event = $this->stopwatch->start($this->getName().'.handle_alarm');
48+
49+
$exit = $this->command->handleAlarm($previousExitCode);
50+
51+
$event->stop();
52+
53+
$this->recordHandledSignal(\SIGALRM, $event);
54+
55+
return $exit;
56+
}
57+
}

src/Symfony/Component/Console/Command/TraceableCommand.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@
2121
use Symfony\Component\Console\Output\ConsoleOutputInterface;
2222
use Symfony\Component\Console\Output\OutputInterface;
2323
use Symfony\Component\Stopwatch\Stopwatch;
24+
use Symfony\Component\Stopwatch\StopwatchEvent;
2425

2526
/**
2627
* @internal
2728
*
2829
* @author Jules Pietri <jules@heahprod.com>
2930
*/
30-
final class TraceableCommand extends Command implements SignalableCommandInterface
31+
class TraceableCommand extends Command implements SignalableCommandInterface
3132
{
3233
public readonly Command $command;
3334
public int $exitCode;
@@ -48,7 +49,7 @@ final class TraceableCommand extends Command implements SignalableCommandInterfa
4849

4950
public function __construct(
5051
Command $command,
51-
private readonly Stopwatch $stopwatch,
52+
protected readonly Stopwatch $stopwatch,
5253
) {
5354
if ($command instanceof LazyCommand) {
5455
$command = $command->getCommand();
@@ -103,6 +104,13 @@ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|
103104

104105
$event->stop();
105106

107+
$this->recordHandledSignal($signal, $event);
108+
109+
return $exit;
110+
}
111+
112+
protected function recordHandledSignal(int $signal, StopwatchEvent $event): void
113+
{
106114
if (!isset($this->handledSignals[$signal])) {
107115
$this->handledSignals[$signal] = [
108116
'handled' => 0,
@@ -117,8 +125,6 @@ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|
117125
$this->handledSignals[$signal]['memory'],
118126
$event->getMemory() >> 20
119127
);
120-
121-
return $exit;
122128
}
123129

124130
/**

src/Symfony/Component/Console/ConsoleEvents.php

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

1212
namespace Symfony\Component\Console;
1313

14+
use Symfony\Component\Console\Event\ConsoleAlarmEvent;
1415
use Symfony\Component\Console\Event\ConsoleCommandEvent;
1516
use Symfony\Component\Console\Event\ConsoleErrorEvent;
1617
use Symfony\Component\Console\Event\ConsoleSignalEvent;
@@ -40,6 +41,14 @@ final class ConsoleEvents
4041
*/
4142
public const SIGNAL = 'console.signal';
4243

44+
/**
45+
* The ALARM event allows you to perform some actions
46+
* after the command received a SIGALRM signal.
47+
*
48+
* @Event("Symfony\Component\Console\Event\ConsoleAlarmEvent")
49+
*/
50+
public const ALARM = 'console.alarm';
51+
4352
/**
4453
* The TERMINATE event allows you to attach listeners after a command is
4554
* executed by the console.
@@ -67,6 +76,7 @@ final class ConsoleEvents
6776
ConsoleCommandEvent::class => self::COMMAND,
6877
ConsoleErrorEvent::class => self::ERROR,
6978
ConsoleSignalEvent::class => self::SIGNAL,
79+
ConsoleAlarmEvent::class => self::ALARM,
7080
ConsoleTerminateEvent::class => self::TERMINATE,
7181
];
7282
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
final class ConsoleAlarmEvent extends ConsoleEvent
19+
{
20+
public function __construct(
21+
Command $command,
22+
InputInterface $input,
23+
OutputInterface $output,
24+
private int|false $exitCode = 0,
25+
) {
26+
parent::__construct($command, $input, $output);
27+
}
28+
29+
public function setExitCode(int $exitCode): void
30+
{
31+
if ($exitCode < 0 || $exitCode > 255) {
32+
throw new \InvalidArgumentException('Exit code must be between 0 and 255.');
33+
}
34+
35+
$this->exitCode = $exitCode;
36+
}
37+
38+
public function abortExit(): void
39+
{
40+
$this->exitCode = false;
41+
}
42+
43+
public function getExitCode(): int|false
44+
{
45+
return $this->exitCode;
46+
}
47+
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,12 @@ public function handle(int $signal): void
5454
$signalHandler($signal, $hasNext);
5555
}
5656
}
57+
58+
/**
59+
* @internal
60+
*/
61+
public function scheduleAlarm(int $seconds): void
62+
{
63+
pcntl_alarm($seconds);
64+
}
5765
}

0 commit comments

Comments
 (0)
0