Skip to content
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

[Console] Add signal event #33729

Merged
merged 1 commit into from Jul 31, 2020
Merged

[Console] Add signal event #33729

merged 1 commit into from Jul 31, 2020

Conversation

marie
Copy link
Contributor

@marie marie commented Sep 27, 2019

Q A
Branch? master
Bug fix? no
New feature? yes
Deprecations? no
Tickets Fix #33411
License MIT
Doc PR symfony/symfony-docs#...

This new feature allows to set a listener for performing some actions after the console command get a signal.

Usage:

<?php

require __DIR__.'/vendor/autoload.php';

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleSignalEvent;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\SignalRegistry\SignalRegistry;
use Symfony\Component\EventDispatcher\EventDispatcher;

class HelloWorldCommand extends Command {
    protected static $defaultName = 'app:hello-world';
    protected function execute(InputInterface $input, OutputInterface $output): int {
        $output->writeln('hello');

        for ($i=0; $i < 10; $i++) {
            $output->write('.');

            sleep(1);
        }
        $output->writeln('');

        $output->writeln('bye');

        return 0;
    }
}

$signalRegistry = new SignalRegistry();
// List of POSIX signals for handling
$signalRegistry->addHandlingSignals(SIGINT, SIGUSR1);

$dispatcher = new EventDispatcher();
// Function that will handle signals
$dispatcher->addListener(ConsoleEvents::SIGNAL, function (ConsoleSignalEvent $event) {
    echo 'Handled signal #' . $event->getHandlingSignal();
});

$application = new Application();
$application->add(new HelloWorldCommand());
$application->setDispatcher($dispatcher);
$application->setSignalRegistry($signalRegistry);

$application->run();

@lyrixx
Copy link
Member

@lyrixx lyrixx commented Sep 28, 2019

Thanks @marie for your first contribution.

This will be not so simple:

  1. You can register only one listener by signal. This is a limitation of PHP :/ I don't know what is the best solution to handle this. We can add an abstraction (Listener Registry, something like https://github.com/romainneutron/signal-handler). Or we can do nothing : Symfony will define a listener first. And if in my application I register another listener, then this new listener will be used. But there is a drawback: the original listener from SF will not be called. This could lead to some WTF.
  2. edit I missed the fact your already used it You need something to dispatch from PHP core (buffer) to PHP Userland the signal. Since Symfony requires PHP 7.1+ I think our best option is to use https://www.php.net/pcntl_async_signals
  3. You hardcode a list a signal, this should be configurable.

src/Symfony/Component/Console/Event/ConsoleStopEvent.php Outdated Show resolved Hide resolved
src/Symfony/Component/Console/Event/ConsoleStopEvent.php Outdated Show resolved Hide resolved
src/Symfony/Component/Console/Application.php Outdated Show resolved Hide resolved
src/Symfony/Component/Console/Event/ConsoleStopEvent.php Outdated Show resolved Hide resolved
src/Symfony/Component/Console/ConsoleEvents.php Outdated Show resolved Hide resolved
@marie
Copy link
Contributor Author

@marie marie commented Sep 29, 2019

Hello, @lyrixx. Thank you for your code review!
What do you think about the registry for signals? https://github.com/symfony/symfony/pull/33729/files#diff-a0717f72232896c363603bd89a126da5

@marie marie force-pushed the 33411-console-stop-event branch from 0118e9c to 1797ac7 Compare Oct 1, 2019
@marie marie marked this pull request as ready for review Oct 23, 2019
@marie marie force-pushed the 33411-console-stop-event branch 2 times, most recently from 3b2c4a8 to f5ab3f6 Compare Oct 26, 2019
@marie marie requested a review from lyrixx Oct 26, 2019
src/Symfony/Component/Console/Application.php Outdated Show resolved Hide resolved
src/Symfony/Component/Console/Application.php Outdated Show resolved Hide resolved
src/Symfony/Bundle/FrameworkBundle/Console/Application.php Outdated Show resolved Hide resolved
src/Symfony/Component/SignalRegistry/SignalRegistry.php Outdated Show resolved Hide resolved
src/Symfony/Component/SignalRegistry/SignalRegistry.php Outdated Show resolved Hide resolved
src/Symfony/Component/SignalRegistry/SignalRegistry.php Outdated Show resolved Hide resolved
src/Symfony/Component/SignalRegistry/SignalRegistry.php Outdated Show resolved Hide resolved
@dillingham
Copy link

@dillingham dillingham commented Nov 11, 2019

Ps thanks @marie for putting time towards this. Much appreciated! 😄

@marie
Copy link
Contributor Author

@marie marie commented Nov 12, 2019

@dillingham, I'm sorry I don't have time to finish the task now. I'll write in the related issue that it is unassigned.

@marie marie closed this Nov 12, 2019
@dillingham
Copy link

@dillingham dillingham commented Nov 12, 2019

I understand. Thanks for trying

Seems like a waste to close this when the reviews only seem to be cosmetic

If someone wants to take this over, I'll happily tip via paypal

@noniagriconomie
Copy link
Contributor

@noniagriconomie noniagriconomie commented Nov 12, 2019

yes it's too bad, could be a great addition for 4.4
what is missing exaclty?

@nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Nov 12, 2019

If anyone is interested in pursuing the work, please check it out, squash all commits in one, add yours after, then submit a PR.

@lyrixx
Copy link
Member

@lyrixx lyrixx commented Nov 12, 2019

Please, feel free to take it. if nobody wants to work on it, I may finish it (ping me in 1 month)

@noniagriconomie It will be for SF 5.1 or more :)

@chalasr chalasr force-pushed the 33411-console-stop-event branch 3 times, most recently from 063f49d to 1574956 Compare Jun 17, 2020
@marie
Copy link
Contributor Author

@marie marie commented Jun 29, 2020

Hello! The merge request is in status "needs work", but there are no discussions. What do I need to do to make it accepted? :)

@lyrixx
Copy link
Member

@lyrixx lyrixx commented Jun 30, 2020

Hello, Sorry for the delay. I did not test it, but If the code in the PR description works as expected, it seems good to me 👍

But (for another PR) :

  • And easier way to leverage this system in Symfony (framework) should be added (I could take care of this one)
  • The integration with messenger component should be fixed (maybe this one too)

chalasr
chalasr approved these changes Jul 2, 2020
Copy link
Member

@chalasr chalasr left a comment

We've got time to improve it. 👍

fabpot
fabpot approved these changes Jul 31, 2020
@fabpot fabpot force-pushed the 33411-console-stop-event branch from 1574956 to 859b692 Compare Jul 31, 2020
@fabpot
Copy link
Member

@fabpot fabpot commented Jul 31, 2020

Thank you @marie.

@fabpot
Copy link
Member

@fabpot fabpot commented Jul 31, 2020

@lyrixx it's now merged, do you have time to implement the 2 items you mentioned above?

@fabpot fabpot merged commit 2d5e7b0 into symfony:master Jul 31, 2020
0 of 5 checks passed
@lyrixx
Copy link
Member

@lyrixx lyrixx commented Aug 4, 2020

@fabpot Yes :) I'll work on this feature on 13th august

@dillingham
Copy link

@dillingham dillingham commented Aug 4, 2020

@marie I offered a bounty earlier, feel free to email me to collect. Thanks everyone for the effort on this!

fabpot added a commit that referenced this issue Aug 13, 2020
This PR was merged into the 5.2-dev branch.

Discussion
----------

[Console] Rework the signal integration

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | refs #33729
| License       | MIT
| Doc PR        |

I updated the code to have a better design and DX:

* `SignalRegistry::$handlingSignals` is gone. This was a temporary data store to
  bind signal to symfony event. This does not belong to the `SignalRegistry`. It
  has been replaced by `Application::$signalsToDispatchEvent`. A method has been
  added to edit this list `Application::setSignalsToDispatchEvent()`
* The default value for `Application::$signalsToDispatchEvent` is `SIGINT,
  SIGTERM, SIGUSR1, SIGUSR2`. Theses defaults seems good enough for most of
  application. for recall:
    * `SIGINT`: CTRL+C
    * `SIGTERM`: `kill PID`, this is what occurs when stopping a process with SystemD, upstart & co
    * `SIGUSR1` and `SIGUSR2`: Signal for user space
* `Application::setSignalRegistry()` is gone. Now the Application always owns a
  signal registry. Since it's a CLI, it's legit to always have support for
  signals. A method has been added for convenience, and to register signal
  handler manually from the command:

    ```php
        $application->getSignalRegistry()->register(SIGINT, function ($signal) {
            dump("Signal ($signal) caught");
        });
    ```
* The interface `SignalableCommandInterface` has been added. When a command
  implements this interface, the command is automatically called when a
  registered signal has been caught

A note about the BC:

* If one register an handler before the Symfony ones, the Signal Registry keeps
   existing registered handlers. The BC is kept 
* If one register an handler after the Symfony ones, it overrides the Symfony
  behavior. Since the feature is new. the BC is kept 

---

So now, If one want to listen a signal, they have few options depending on the context:

* A global action is common to all commands (such as logging, enabling a
  profiler (👋 blackfire.io)): Use a listener. With autoconfigure, the following
  code is enough:
    ```php
    class SignalSubscriber implements EventSubscriberInterface
    {
        private $logger;

        public function __construct(LoggerInterface $logger = null)
        {
            $this->logger = $logger ?: new NullLogger();
        }

        public function handleSignal(ConsoleSignalEvent $event)
        {
            $signal = $event->getHandlingSignal();

            $this->logger->debug('The application has been signaled', [
                'signal' => $signal,
            ]);
        }

        public static function getSubscribedEvents()
        {
            return [
                ConsoleEvents::SIGNAL => 'handleSignal',
            ];
        }
    }
    ```

* The command should react to a signal: Implements the interface:
    ```php
    class SignalCommand extends Command implements SignalableCommandInterface
    {
        protected static $defaultName = 'signal';

        private $shouldStop = false;

        protected function execute(InputInterface $input, OutputInterface $output): int
        {
            $output->writeln('hello, I am '. getmypid());

            for ($i=0; $i < 60; $i++) {
                if ($this->shouldStop) {
                    break;
                }

                $output->write('.');

                sleep(1);
            }
            $output->writeln('');

            $output->writeln('bye');

            return 0;
        }

        public function handleSignal(int $signal)
        {
            dump([__METHOD__, $signal]);

            $this->shouldStop = true;
        }

        public function getSubscribedSignals(): array
        {
            return [SIGINT];
        }
    }
    ```

* The command should react differently to many event and/or one wants a full control on name of the method called

    ```php
    class SignalCommand extends Command
    {
        protected static $defaultName = 'signal';

        private $shouldStop = false;

        protected function execute(InputInterface $input, OutputInterface $output): int
        {
            $this->getApplication()->getSignalRegistry()->register(SIGINT, [$this, 'stop']);
            $this->getApplication()->getSignalRegistry()->register(SIGUSR1, [$this, 'enableBlackfire']);

            $output->writeln('hello, I am '. getmypid());

            for ($i=0; $i < 60; $i++) {
                if ($this->shouldStop) {
                    break;
                }

                $output->write('.');

                sleep(1);
            }
            $output->writeln('');

            $output->writeln('bye');

            return 0;
        }

        public function stop()
        {
            $this->shouldStop = true;
        }

        public function enableBlackfire()
        {
            // ...
        }
    }
    ```

ping @marie

Commits
-------

df57119 [Console] Rework the signal integration
@nicolas-grekas nicolas-grekas removed this from the next milestone Oct 5, 2020
@nicolas-grekas nicolas-grekas added this to the 5.2 milestone Oct 5, 2020
@fabpot fabpot mentioned this pull request Oct 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment