Skip to content

Commit 1c70c98

Browse files
committed
[Console] Add ability to schedule alarm signals and a console.alarm event
1 parent 28ceddd commit 1c70c98

File tree

8 files changed

+389
-14
lines changed

8 files changed

+389
-14
lines changed

src/Symfony/Component/Console/Application.php

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Component\Console\Completion\CompletionInput;
2323
use Symfony\Component\Console\Completion\CompletionSuggestions;
2424
use Symfony\Component\Console\Completion\Suggestion;
25+
use Symfony\Component\Console\Event\ConsoleAlarmEvent;
2526
use Symfony\Component\Console\Event\ConsoleCommandEvent;
2627
use Symfony\Component\Console\Event\ConsoleErrorEvent;
2728
use Symfony\Component\Console\Event\ConsoleSignalEvent;
@@ -88,6 +89,7 @@ class Application implements ResetInterface
8889
private bool $initialized = false;
8990
private ?SignalRegistry $signalRegistry = null;
9091
private array $signalsToDispatchEvent = [];
92+
private ?int $alarmInterval = null;
9193

9294
public function __construct(
9395
private string $name = 'UNKNOWN',
@@ -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, \SIGQUIT, \SIGTERM, \SIGUSR1, \SIGUSR2];
102+
$this->signalsToDispatchEvent = [\SIGINT, \SIGQUIT, \SIGTERM, \SIGUSR1, \SIGUSR2, \SIGALRM];
101103
}
102104
}
103105

@@ -128,6 +130,19 @@ public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent): void
128130
$this->signalsToDispatchEvent = $signalsToDispatchEvent;
129131
}
130132

133+
public function setAlarmInterval(?int $interval): void
134+
{
135+
$this->alarmInterval = $interval;
136+
$this->scheduleAlarm();
137+
}
138+
139+
private function scheduleAlarm(): void
140+
{
141+
if (null !== $this->alarmInterval) {
142+
$this->getSignalRegistry()->scheduleAlarm($this->alarmInterval);
143+
}
144+
}
145+
131146
/**
132147
* Runs the current application.
133148
*
@@ -981,34 +996,47 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
981996

982997
$commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [];
983998
if ($commandSignals || $this->dispatcher && $this->signalsToDispatchEvent) {
984-
if (!$this->signalRegistry) {
985-
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.');
986-
}
999+
$signalRegistry = $this->getSignalRegistry();
9871000

9881001
if (Terminal::hasSttyAvailable()) {
9891002
$sttyMode = shell_exec('stty -g');
9901003

9911004
foreach ([\SIGINT, \SIGQUIT, \SIGTERM] as $signal) {
992-
$this->signalRegistry->register($signal, static fn () => shell_exec('stty '.$sttyMode));
1005+
$signalRegistry->register($signal, static fn () => shell_exec('stty '.$sttyMode));
9931006
}
9941007
}
9951008

9961009
if ($this->dispatcher) {
9971010
// We register application signals, so that we can dispatch the event
9981011
foreach ($this->signalsToDispatchEvent as $signal) {
999-
$event = new ConsoleSignalEvent($command, $input, $output, $signal);
1000-
1001-
$this->signalRegistry->register($signal, function ($signal) use ($event, $command, $commandSignals) {
1002-
$this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL);
1003-
$exitCode = $event->getExitCode();
1012+
$signalEvent = new ConsoleSignalEvent($command, $input, $output, $signal);
1013+
$alarmEvent = \SIGALRM === $signal ? new ConsoleAlarmEvent($command, $input, $output) : null;
1014+
1015+
$signalRegistry->register($signal, function ($signal) use ($signalEvent, $alarmEvent, $command, $commandSignals, $input, $output) {
1016+
$this->dispatcher->dispatch($signalEvent, ConsoleEvents::SIGNAL);
1017+
$exitCode = $signalEvent->getExitCode();
1018+
1019+
if (null !== $alarmEvent) {
1020+
if (false !== $exitCode) {
1021+
$alarmEvent->setExitCode($exitCode);
1022+
} else {
1023+
$alarmEvent->abortExit();
1024+
}
1025+
$this->dispatcher->dispatch($alarmEvent, ConsoleEvents::ALARM);
1026+
$exitCode = $alarmEvent->getExitCode();
1027+
}
10041028

10051029
// If the command is signalable, we call the handleSignal() method
10061030
if (\in_array($signal, $commandSignals, true)) {
10071031
$exitCode = $command->handleSignal($signal, $exitCode);
10081032
}
10091033

1034+
if (\SIGALRM === $signal) {
1035+
$this->scheduleAlarm();
1036+
}
1037+
10101038
if (false !== $exitCode) {
1011-
$event = new ConsoleTerminateEvent($command, $event->getInput(), $event->getOutput(), $exitCode, $signal);
1039+
$event = new ConsoleTerminateEvent($command, $input, $output, $exitCode, $signal);
10121040
$this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE);
10131041

10141042
exit($event->getExitCode());
@@ -1021,7 +1049,11 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
10211049
}
10221050

10231051
foreach ($commandSignals as $signal) {
1024-
$this->signalRegistry->register($signal, function (int $signal) use ($command): void {
1052+
$signalRegistry->register($signal, function (int $signal) use ($command): void {
1053+
if (\SIGALRM === $signal) {
1054+
$this->scheduleAlarm();
1055+
}
1056+
10251057
if (false !== $exitCode = $command->handleSignal($signal)) {
10261058
exit($exitCode);
10271059
}

src/Symfony/Component/Console/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ CHANGELOG
88
* Add `verbosity` argument to `mustRun` process helper method
99
* [BC BREAK] Add silent verbosity (`--silent`/`SHELL_VERBOSITY=-2`) to suppress all output, including errors
1010
* Add `OutputInterface::isSilent()`, `Output::isSilent()`, `OutputStyle::isSilent()` methods
11+
* Add ability to schedule alarm signals and a `console.alarm` event
1112

1213
7.1
1314
---

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)