diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php index 1c41849e794d..a0d7f1d059e1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php @@ -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; @@ -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)); diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 9c301471c658..3b0af2b82949 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -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; @@ -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; @@ -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]; } } @@ -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.'); @@ -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()); @@ -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; diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 0ff71d2faf48..d6da184edc32 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.1 +--- + + * Add `AlarmableCommandInterface` and `console.alarm` event + 7.0 --- diff --git a/src/Symfony/Component/Console/Command/AlarmableCommandInterface.php b/src/Symfony/Component/Console/Command/AlarmableCommandInterface.php new file mode 100644 index 000000000000..d892287ed8a5 --- /dev/null +++ b/src/Symfony/Component/Console/Command/AlarmableCommandInterface.php @@ -0,0 +1,34 @@ + + * + * 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; +} diff --git a/src/Symfony/Component/Console/Command/TraceableAlarmableCommand.php b/src/Symfony/Component/Console/Command/TraceableAlarmableCommand.php new file mode 100644 index 000000000000..bdf8c4edda26 --- /dev/null +++ b/src/Symfony/Component/Console/Command/TraceableAlarmableCommand.php @@ -0,0 +1,57 @@ + + * + * 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; + } +} diff --git a/src/Symfony/Component/Console/Command/TraceableCommand.php b/src/Symfony/Component/Console/Command/TraceableCommand.php index 9ffb68da3976..47cdb4920202 100644 --- a/src/Symfony/Component/Console/Command/TraceableCommand.php +++ b/src/Symfony/Component/Console/Command/TraceableCommand.php @@ -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 */ -final class TraceableCommand extends Command implements SignalableCommandInterface +class TraceableCommand extends Command implements SignalableCommandInterface { public readonly Command $command; public int $exitCode; @@ -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(); @@ -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, @@ -117,8 +125,6 @@ public function handleSignal(int $signal, int|false $previousExitCode = 0): int| $this->handledSignals[$signal]['memory'], $event->getMemory() >> 20 ); - - return $exit; } /** diff --git a/src/Symfony/Component/Console/ConsoleEvents.php b/src/Symfony/Component/Console/ConsoleEvents.php index 6ae8f32b8b08..c3e7378401d7 100644 --- a/src/Symfony/Component/Console/ConsoleEvents.php +++ b/src/Symfony/Component/Console/ConsoleEvents.php @@ -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; @@ -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. @@ -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, ]; } diff --git a/src/Symfony/Component/Console/Event/ConsoleAlarmEvent.php b/src/Symfony/Component/Console/Event/ConsoleAlarmEvent.php new file mode 100644 index 000000000000..876ab59b9232 --- /dev/null +++ b/src/Symfony/Component/Console/Event/ConsoleAlarmEvent.php @@ -0,0 +1,47 @@ + + * + * 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; + } +} diff --git a/src/Symfony/Component/Console/SignalRegistry/SignalRegistry.php b/src/Symfony/Component/Console/SignalRegistry/SignalRegistry.php index ef2e5f04e16d..8c2939eec279 100644 --- a/src/Symfony/Component/Console/SignalRegistry/SignalRegistry.php +++ b/src/Symfony/Component/Console/SignalRegistry/SignalRegistry.php @@ -54,4 +54,12 @@ public function handle(int $signal): void $signalHandler($signal, $hasNext); } } + + /** + * @internal + */ + public function scheduleAlarm(int $seconds): void + { + pcntl_alarm($seconds); + } } diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index ca85c24b1f75..a0877e13923a 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Application; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\AlarmableCommandInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\HelpCommand; use Symfony\Component\Console\Command\LazyCommand; @@ -22,6 +23,7 @@ use Symfony\Component\Console\CommandLoader\FactoryCommandLoader; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass; +use Symfony\Component\Console\Event\ConsoleAlarmEvent; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\Console\Event\ConsoleSignalEvent; @@ -69,6 +71,9 @@ protected function tearDown(): void unset($_SERVER['SHELL_VERBOSITY']); if (\function_exists('pcntl_signal')) { + // We cancel any pending alarms + pcntl_alarm(0); + // We reset all signals to their default value to avoid side effects for ($i = 1; $i <= 15; ++$i) { if (9 === $i) { @@ -2226,6 +2231,234 @@ public function testSignalableRestoresStty() $this->assertSame($previousSttyMode, $sttyMode); } + /** + * @requires extension pcntl + */ + public function testInputIsBoundWhenPassedToGetAlarmTime() + { + $command = new #[AsCommand(name: 'alarm')] class(0) extends AlarmableCommand { + protected function configure(): void + { + $this + ->addArgument('arg', InputArgument::REQUIRED) + ->addOption('option', mode: InputOption::VALUE_REQUIRED) + ; + } + + public function getAlarmTime(InputInterface $input): int + { + TestCase::assertTrue($input->hasArgument('arg')); + TestCase::assertSame('argval', $input->getArgument('arg')); + TestCase::assertTrue($input->hasOption('option')); + TestCase::assertSame('optionval', $input->getOption('option')); + + return parent::getAlarmTime($input); + } + }; + + $application = $this->createSignalableApplication($command, null); + $application->setCatchExceptions(false); + + $input = new ArrayInput(['alarm', 'arg' => 'argval', '--option' => 'optionval']); + + $this->assertSame(0, $application->run($input)); + } + + /** + * @requires extension pcntl + */ + public function testAlarmSubscriberNotCalledByDefault() + { + $command = new BaseSignableCommand(false); + + $subscriber = new AlarmEventSubscriber(); + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber($subscriber); + + $application = $this->createSignalableApplication($command, $dispatcher); + + $this->assertSame(0, $application->run(new ArrayInput(['signal']))); + $this->assertFalse($subscriber->signaled); + } + + /** + * @requires extension pcntl + */ + public function testAlarmSubscriberNotCalledForOtherSignals() + { + $command = new SignableCommand(); + + $subscriber1 = new SignalEventSubscriber(); + $subscriber2 = new AlarmEventSubscriber(); + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber($subscriber1); + $dispatcher->addSubscriber($subscriber2); + + $application = $this->createSignalableApplication($command, $dispatcher); + + $this->assertSame(1, $application->run(new ArrayInput(['signal']))); + $this->assertTrue($subscriber1->signaled); + $this->assertFalse($subscriber2->signaled); + } + + /** + * @requires extension pcntl + */ + public function testAlarmSubscriber() + { + $command = new BaseSignableCommand(signal: \SIGALRM); + + $subscriber1 = new AlarmEventSubscriber(); + $subscriber2 = new AlarmEventSubscriber(); + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber($subscriber1); + $dispatcher->addSubscriber($subscriber2); + + $application = $this->createSignalableApplication($command, $dispatcher); + + $this->assertSame(1, $application->run(new ArrayInput(['signal']))); + $this->assertTrue($subscriber1->signaled); + $this->assertTrue($subscriber2->signaled); + } + + /** + * @requires extension pcntl + */ + public function testAlarmDispatchWithoutEventDispatcher() + { + $command = new AlarmableCommand(1); + $command->loop = 11000; + + $application = $this->createSignalableApplication($command, null); + + $this->assertSame(1, $application->run(new ArrayInput(['alarm']))); + $this->assertTrue($command->signaled); + } + + /** + * @requires extension pcntl + */ + public function testAlarmableCommandInterfaceWithoutSignal() + { + $command = new AlarmableCommand(0); + $command->loop = 11000; + + $dispatcher = new EventDispatcher(); + + $application = new Application(); + $application->setAutoExit(false); + $application->setDispatcher($dispatcher); + $application->add($command); + + $this->assertSame(0, $application->run(new ArrayInput(['alarm']))); + } + + /** + * @requires extension pcntl + */ + public function testAlarmableCommandHandlerCalledAfterEventListener() + { + $command = new AlarmableCommand(1); + $command->loop = 11000; + + $subscriber = new AlarmEventSubscriber(); + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber($subscriber); + + $application = $this->createSignalableApplication($command, $dispatcher); + + $this->assertSame(1, $application->run(new ArrayInput(['alarm']))); + $this->assertSame([AlarmEventSubscriber::class, AlarmableCommand::class], $command->signalHandlers); + } + + /** + * @requires extension pcntl + * + * @testWith [false] + * [4] + */ + public function testAlarmSubscriberCalledAfterSignalSubscriberAndInheritsExitCode(int|false $exitCode) + { + $command = new BaseSignableCommand(signal: \SIGALRM); + + $subscriber1 = new class($exitCode) extends SignalEventSubscriber { + public function __construct(private int|false $exitCode) + { + } + + public function onSignal(ConsoleSignalEvent $event): void + { + parent::onSignal($event); + + if (false === $this->exitCode) { + $event->abortExit(); + } else { + $event->setExitCode($this->exitCode); + } + } + }; + $subscriber2 = new class($exitCode) extends AlarmEventSubscriber { + public function __construct(private int|false $exitCode) + { + } + + public function onAlarm(ConsoleAlarmEvent $event): void + { + TestCase::assertSame($this->exitCode, $event->getExitCode()); + + parent::onAlarm($event); + } + }; + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber($subscriber1); + $dispatcher->addSubscriber($subscriber2); + + $application = $this->createSignalableApplication($command, $dispatcher); + + $this->assertSame(1, $application->run(new ArrayInput(['signal']))); + $this->assertSame([SignalEventSubscriber::class, AlarmEventSubscriber::class], $command->signalHandlers); + } + + /** + * @requires extension pcntl + */ + public function testHandleAlarmCalledInsteadOfHandleSignalWithoutDispatcher() + { + $command = new SignableAndAlarmableCommand(1); + $command->loop = 11000; + + $application = $this->createSignalableApplication($command, null); + + $this->assertSame(1, $application->run(new ArrayInput(['signal-and-alarm']))); + $this->assertSame([SignableAndAlarmableCommand::class], $command->signalHandlers); + } + + /** + * @requires extension pcntl + */ + public function testHandleAlarmCalledInsteadOfHandleSignalWithDispatcher() + { + $command = new SignableAndAlarmableCommand(1); + $command->loop = 11000; + + $subscriber1 = new SignalEventSubscriber(); + $subscriber2 = new AlarmEventSubscriber(); + + $dispatcher = new EventDispatcher(); + $dispatcher->addSubscriber($subscriber1); + $dispatcher->addSubscriber($subscriber2); + + $application = $this->createSignalableApplication($command, $dispatcher); + + $this->assertSame(1, $application->run(new ArrayInput(['signal-and-alarm']))); + $this->assertSame([SignalEventSubscriber::class, AlarmEventSubscriber::class, SignableAndAlarmableCommand::class], $command->signalHandlers); + } + private function createSignalableApplication(Command $command, ?EventDispatcherInterface $dispatcher): Application { $application = new Application(); @@ -2233,7 +2466,7 @@ private function createSignalableApplication(Command $command, ?EventDispatcherI if ($dispatcher) { $application->setDispatcher($dispatcher); } - $application->add(new LazyCommand('signal', [], '', false, fn () => $command, true)); + $application->add(new LazyCommand($command::getDefaultName(), [], '', false, fn () => $command, true)); return $application; } @@ -2423,3 +2656,66 @@ public static function getSubscribedEvents(): array return ['console.signal' => 'onSignal']; } } + +#[AsCommand(name: 'alarm')] +class AlarmableCommand extends BaseSignableCommand implements AlarmableCommandInterface +{ + public function __construct(private int $alarmTime) + { + parent::__construct(false); + } + + public function getAlarmTime(InputInterface $input): int + { + return $this->alarmTime; + } + + public function handleAlarm(false|int $previousExitCode = 0): int|false + { + $this->signaled = true; + $this->signalHandlers[] = __CLASS__; + + return false; + } +} + +class AlarmEventSubscriber implements EventSubscriberInterface +{ + public bool $signaled = false; + + public function onAlarm(ConsoleAlarmEvent $event): void + { + $this->signaled = true; + $event->getCommand()->signaled = true; + $event->getCommand()->signalHandlers[] = __CLASS__; + + $event->abortExit(); + } + + public static function getSubscribedEvents(): array + { + return ['console.alarm' => 'onAlarm']; + } +} + +#[AsCommand(name: 'signal-and-alarm')] +class SignableAndAlarmableCommand extends SignableCommand implements AlarmableCommandInterface +{ + public function __construct(private int $alarmTime) + { + parent::__construct(false); + } + + public function getAlarmTime(InputInterface $input): int + { + return $this->alarmTime; + } + + public function handleAlarm(false|int $previousExitCode = 0): int|false + { + $this->signaled = true; + $this->signalHandlers[] = __CLASS__; + + return false; + } +} diff --git a/src/Symfony/Component/Console/Tests/phpt/alarm/command_exit.phpt b/src/Symfony/Component/Console/Tests/phpt/alarm/command_exit.phpt new file mode 100644 index 000000000000..5c318b8c4bcb --- /dev/null +++ b/src/Symfony/Component/Console/Tests/phpt/alarm/command_exit.phpt @@ -0,0 +1,55 @@ +--TEST-- +Test command that exist +--SKIPIF-- + +--FILE-- +writeln('should not be displayed'); + + return 0; + } + + public function getAlarmTime(InputInterface $input): int + { + return 1; + } + + public function handleAlarm(int|false $previousExitCode = 0): int|false + { + echo "Received alarm!"; + + return 0; + } +} + +$app = new Application(); +$app->setDispatcher(new \Symfony\Component\EventDispatcher\EventDispatcher()); +$app->add(new MyCommand('foo')); + +$app + ->setDefaultCommand('foo', true) + ->run() +; +--EXPECT-- +Received alarm!