Skip to content

Commit

Permalink
[Console] Add AlarmableCommandInterface and console.alarm event
Browse files Browse the repository at this point in the history
  • Loading branch information
HypeMC committed Feb 17, 2024
1 parent db21ee4 commit e42a38a
Show file tree
Hide file tree
Showing 11 changed files with 578 additions and 24 deletions.
7 changes: 6 additions & 1 deletion src/Symfony/Bundle/FrameworkBundle/Console/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
namespace Symfony\Bundle\FrameworkBundle\Console;

use Symfony\Component\Console\Application as BaseApplication;
use Symfony\Component\Console\Command\AlarmableCommandInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\ListCommand;
use Symfony\Component\Console\Command\TraceableAlarmableCommand;
use Symfony\Component\Console\Command\TraceableCommand;
use Symfony\Component\Console\Debug\CliRequest;
use Symfony\Component\Console\Input\InputInterface;
Expand Down Expand Up @@ -112,7 +114,10 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI

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

$requestStack = $container->get('.virtual_request_stack');
$requestStack->push(new CliRequest($command));
Expand Down
67 changes: 49 additions & 18 deletions src/Symfony/Component/Console/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Console;

use Symfony\Component\Console\Command\AlarmableCommandInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\CompleteCommand;
use Symfony\Component\Console\Command\DumpCompletionCommand;
Expand All @@ -22,6 +23,7 @@
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Completion\Suggestion;
use Symfony\Component\Console\Event\ConsoleAlarmEvent;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleSignalEvent;
Expand Down Expand Up @@ -97,7 +99,7 @@ public function __construct(
$this->defaultCommand = 'list';
if (\defined('SIGINT') && SignalRegistry::isSupported()) {
$this->signalRegistry = new SignalRegistry();
$this->signalsToDispatchEvent = [\SIGINT, \SIGTERM, \SIGUSR1, \SIGUSR2];
$this->signalsToDispatchEvent = [\SIGINT, \SIGTERM, \SIGUSR1, \SIGUSR2, \SIGALRM];
}
}

Expand Down Expand Up @@ -975,7 +977,18 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
}
}

// bind before getAlarmTime() and the console.command event, so the method and listeners have access to input options/arguments
try {
$command->mergeApplicationDefinition();
$input->bind($command->getDefinition());
} catch (ExceptionInterface) {
// ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition
}

$commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [];
if ($command instanceof AlarmableCommandInterface && \defined('SIGALRM') && SignalRegistry::isSupported() && !\in_array(\SIGALRM, $commandSignals, true)) {
$commandSignals[] = \SIGALRM;
}
if ($commandSignals || $this->dispatcher && $this->signalsToDispatchEvent) {
if (!$this->signalRegistry) {
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.');
Expand All @@ -992,19 +1005,34 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
if ($this->dispatcher) {
// We register application signals, so that we can dispatch the event
foreach ($this->signalsToDispatchEvent as $signal) {
$event = new ConsoleSignalEvent($command, $input, $output, $signal);

$this->signalRegistry->register($signal, function ($signal) use ($event, $command, $commandSignals) {
$this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL);
$exitCode = $event->getExitCode();
$signalEvent = new ConsoleSignalEvent($command, $input, $output, $signal);
$alarmEvent = \SIGALRM === $signal ? new ConsoleAlarmEvent($command, $input, $output) : null;

$this->signalRegistry->register($signal, function ($signal) use ($signalEvent, $alarmEvent, $command, $commandSignals, $input, $output) {
$this->dispatcher->dispatch($signalEvent, ConsoleEvents::SIGNAL);
$exitCode = $signalEvent->getExitCode();

if (null !== $alarmEvent) {
if (false !== $exitCode) {
$alarmEvent->setExitCode($exitCode);
} else {
$alarmEvent->abortExit();
}
$this->dispatcher->dispatch($alarmEvent, ConsoleEvents::ALARM);
$exitCode = $alarmEvent->getExitCode();
}

if (\SIGALRM === $signal && $command instanceof AlarmableCommandInterface) {
$exitCode = $command->handleAlarm($exitCode);
$this->signalRegistry->scheduleAlarm($command->getAlarmTime($input));
}
// If the command is signalable, we call the handleSignal() method
if (\in_array($signal, $commandSignals, true)) {
elseif (\in_array($signal, $commandSignals, true)) {
$exitCode = $command->handleSignal($signal, $exitCode);
}

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

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

foreach ($commandSignals as $signal) {
$this->signalRegistry->register($signal, function (int $signal) use ($command): void {
if (false !== $exitCode = $command->handleSignal($signal)) {
$this->signalRegistry->register($signal, function (int $signal) use ($command, $input): void {
if (\SIGALRM === $signal && $command instanceof AlarmableCommandInterface) {
$exitCode = $command->handleAlarm();
$this->signalRegistry->scheduleAlarm($command->getAlarmTime($input));
} else {
$exitCode = $command->handleSignal($signal);
}

if (false !== $exitCode) {
exit($exitCode);
}
});
}

if ($command instanceof AlarmableCommandInterface) {
$this->signalRegistry->scheduleAlarm($command->getAlarmTime($input));
}
}

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

// bind before the console.command event, so the listeners have access to input options/arguments
try {
$command->mergeApplicationDefinition();
$input->bind($command->getDefinition());
} catch (ExceptionInterface) {
// ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition
}

$event = new ConsoleCommandEvent($command, $input, $output);
$e = null;

Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Component/Console/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

7.1
---

* Add `AlarmableCommandInterface` and `console.alarm` event

7.0
---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Console\Command;

use Symfony\Component\Console\Input\InputInterface;

/**
* Interface for command that listens to SIGALRM signals.
*/
interface AlarmableCommandInterface
{
/**
* The method will be called before the command is run and subsequently on each SIGALRM signal.
*
* @return int The alarm time in seconds
*/
public function getAlarmTime(InputInterface $input): int;

/**
* The method will be called when the application is signaled with SIGALRM.
*
* @return int|false The exit code to return or false to continue the normal execution
*/
public function handleAlarm(int|false $previousExitCode = 0): int|false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Console\Command;

use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Stopwatch\Stopwatch;

/**
* @internal
*
* @property Command&AlarmableCommandInterface $command
*/
final class TraceableAlarmableCommand extends TraceableCommand implements AlarmableCommandInterface
{
public function __construct(Command&AlarmableCommandInterface $command, Stopwatch $stopwatch)
{
parent::__construct($command, $stopwatch);
}

public function getSubscribedSignals(): array
{
$commandSignals = parent::getSubscribedSignals();

if (!\in_array(\SIGALRM, $commandSignals, true)) {
$commandSignals[] = \SIGALRM;
}

return $commandSignals;
}

public function getAlarmTime(InputInterface $input): int
{
return $this->command->getAlarmTime($input);
}

public function handleAlarm(false|int $previousExitCode = 0): int|false
{
$event = $this->stopwatch->start($this->getName().'.handle_alarm');

$exit = $this->command->handleAlarm($previousExitCode);

$event->stop();

$this->recordHandledSignal(\SIGALRM, $event);

return $exit;
}
}
14 changes: 10 additions & 4 deletions src/Symfony/Component/Console/Command/TraceableCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\Stopwatch\StopwatchEvent;

/**
* @internal
*
* @author Jules Pietri <jules@heahprod.com>
*/
final class TraceableCommand extends Command implements SignalableCommandInterface
class TraceableCommand extends Command implements SignalableCommandInterface
{
public readonly Command $command;
public int $exitCode;
Expand All @@ -48,7 +49,7 @@ final class TraceableCommand extends Command implements SignalableCommandInterfa

public function __construct(
Command $command,
private readonly Stopwatch $stopwatch,
protected readonly Stopwatch $stopwatch,
) {
if ($command instanceof LazyCommand) {
$command = $command->getCommand();
Expand Down Expand Up @@ -103,6 +104,13 @@ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|

$event->stop();

$this->recordHandledSignal($signal, $event);

return $exit;
}

protected function recordHandledSignal(int $signal, StopwatchEvent $event): void
{
if (!isset($this->handledSignals[$signal])) {
$this->handledSignals[$signal] = [
'handled' => 0,
Expand All @@ -117,8 +125,6 @@ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|
$this->handledSignals[$signal]['memory'],
$event->getMemory() >> 20
);

return $exit;
}

/**
Expand Down
10 changes: 10 additions & 0 deletions src/Symfony/Component/Console/ConsoleEvents.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Console;

use Symfony\Component\Console\Event\ConsoleAlarmEvent;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleSignalEvent;
Expand Down Expand Up @@ -40,6 +41,14 @@ final class ConsoleEvents
*/
public const SIGNAL = 'console.signal';

/**
* The ALARM event allows you to perform some actions
* after the command received a SIGALRM signal.
*
* @Event("Symfony\Component\Console\Event\ConsoleAlarmEvent")
*/
public const ALARM = 'console.alarm';

/**
* The TERMINATE event allows you to attach listeners after a command is
* executed by the console.
Expand Down Expand Up @@ -67,6 +76,7 @@ final class ConsoleEvents
ConsoleCommandEvent::class => self::COMMAND,
ConsoleErrorEvent::class => self::ERROR,
ConsoleSignalEvent::class => self::SIGNAL,
ConsoleAlarmEvent::class => self::ALARM,
ConsoleTerminateEvent::class => self::TERMINATE,
];
}
47 changes: 47 additions & 0 deletions src/Symfony/Component/Console/Event/ConsoleAlarmEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Console\Event;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

final class ConsoleAlarmEvent extends ConsoleEvent
{
public function __construct(
Command $command,
InputInterface $input,
OutputInterface $output,
private int|false $exitCode = 0,
) {
parent::__construct($command, $input, $output);
}

public function setExitCode(int $exitCode): void
{
if ($exitCode < 0 || $exitCode > 255) {
throw new \InvalidArgumentException('Exit code must be between 0 and 255.');
}

$this->exitCode = $exitCode;
}

public function abortExit(): void
{
$this->exitCode = false;
}

public function getExitCode(): int|false
{
return $this->exitCode;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,12 @@ public function handle(int $signal): void
$signalHandler($signal, $hasNext);
}
}

/**
* @internal
*/
public function scheduleAlarm(int $seconds): void
{
pcntl_alarm($seconds);
}
}

0 comments on commit e42a38a

Please sign in to comment.