Skip to content

[Console] Add ability to schedule alarm signals and a ConsoleAlarmEvent #53533

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions src/Symfony/Component/Console/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,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 @@ -88,6 +89,7 @@ class Application implements ResetInterface
private bool $initialized = false;
private ?SignalRegistry $signalRegistry = null;
private array $signalsToDispatchEvent = [];
private ?int $alarmInterval = null;

public function __construct(
private string $name = 'UNKNOWN',
Expand All @@ -97,7 +99,7 @@ public function __construct(
$this->defaultCommand = 'list';
if (\defined('SIGINT') && SignalRegistry::isSupported()) {
$this->signalRegistry = new SignalRegistry();
$this->signalsToDispatchEvent = [\SIGINT, \SIGQUIT, \SIGTERM, \SIGUSR1, \SIGUSR2];
$this->signalsToDispatchEvent = [\SIGINT, \SIGQUIT, \SIGTERM, \SIGUSR1, \SIGUSR2, \SIGALRM];
}
}

Expand Down Expand Up @@ -128,6 +130,22 @@ public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent): void
$this->signalsToDispatchEvent = $signalsToDispatchEvent;
}

/**
* Sets the interval to schedule a SIGALRM signal in seconds.
*/
public function setAlarmInterval(?int $seconds): void
{
$this->alarmInterval = $seconds;
$this->scheduleAlarm();
}

private function scheduleAlarm(): void
{
if (null !== $this->alarmInterval) {
$this->getSignalRegistry()->scheduleAlarm($this->alarmInterval);
}
}

/**
* Runs the current application.
*
Expand Down Expand Up @@ -981,34 +999,47 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI

$commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [];
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.');
}
$signalRegistry = $this->getSignalRegistry();
Comment on lines -984 to +1002
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getSignalRegistry throws a nearly identical exception, so it made sense to me to have it centralized.


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

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

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;

$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);
$exitCode = $alarmEvent->getExitCode();
}

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

if (\SIGALRM === $signal) {
$this->scheduleAlarm();
}

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 @@ -1021,7 +1052,11 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
}

foreach ($commandSignals as $signal) {
$this->signalRegistry->register($signal, function (int $signal) use ($command): void {
$signalRegistry->register($signal, function (int $signal) use ($command): void {
if (\SIGALRM === $signal) {
$this->scheduleAlarm();
}

if (false !== $exitCode = $command->handleSignal($signal)) {
exit($exitCode);
}
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Console/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ CHANGELOG
* [BC BREAK] Add silent verbosity (`--silent`/`SHELL_VERBOSITY=-2`) to suppress all output, including errors
* Add `OutputInterface::isSilent()`, `Output::isSilent()`, `OutputStyle::isSilent()` methods
* Add a configurable finished indicator to the progress indicator to show that the progress is finished
* Add ability to schedule alarm signals and a `ConsoleAlarmEvent`

7.1
---
Expand Down
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);
}
}
Loading