Skip to content

Commit a11d0da

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

File tree

11 files changed

+575
-24
lines changed

11 files changed

+575
-24
lines changed

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

+6-1
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;
@@ -114,7 +116,10 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
114116

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

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

src/Symfony/Component/Console/Application.php

+49-18
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

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

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

+10-4
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

+10
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
}
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

+8
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)