hechoendrupal / drupal-console

The Drupal CLI. A tool to generate boilerplate code, interact with and debug Drupal.
http://drupalconsole.com
GNU General Public License v2.0
939 stars 559 forks source link

[console] Kernel terminate is not called #4025

Open driskell opened 5 years ago

driskell commented 5 years ago

Problem/Motivation

Any command that runs that depends on services running their destructors (@needs_destruction) will not work properly with drupal console.

It seems this was originally in the code but somehow lost in some refactors along the way. See: https://github.com/hechoendrupal/drupal-console/pull/598

How to reproduce

I only have an example with purge module

Install purge module and enable the purge processor cron and purge core tags queuer Disable cron in all ways, but then run via cron using drupal cron:execute When saving posts, the queue is added to When cron:execute command is run, the items are invalided, but not removed from the queue, because purge queue service is not destructed

Drupal 8.6.15 Drupal console 1.8.0

Solution

Use KernelHelper to call terminate after commands are finished

adamfranco commented 3 years ago

I can confirm that this issue still exists in Drupal Console version 1.9.7 & Drupal 8.9.11. Like driskell, I'm also running into this problem with custom Drupal Console commands that save nodes don't queue up cache tags in the purge module.

I haven't fully figured out, but I think that the base Symfony Console is dispatching a ConsoleEvents::TERMINATE event after the command runs, but not a KernelEvents::TERMINATE event. See: https://symfony.com/doc/current/components/console/events.html#the-consoleevents-terminate-event

The purge module tags its queue service with needs_destruction. It looks like Dupal's KernelDestructionSubscriber::onKernelTerminate() is responsible for handling the needs_destruction tag and should be triggered by the KernalEvents::TERMINATE event. Because KernalEvents::TERMINATE isn't being dispatched, the destructor never gets called.

I'm not sure what the best way to address this is. Maybe the Drupal Console could subscribe to ConsoleEvents::TERMINATE and dispatch a KernelEvents::TERMINATE event. Or maybe this is a bug in Symfony's Console.

It looks like the KernalEvents::TERMINATE event is normally dispatched by HttpKernel::terminate(). My hunch is that Symfony Console isn't assuming there there is an HttpKernel, and so is using its own event, but the DrupalKernel is an HttpKernel and expects there to eventually be a terminate() call.

adamfranco commented 3 years ago

I tried to add a work-around with a very simple module that would listen for ConsoleEvents::TERMINATE event and call DrupalKernel->terminate(), but the event never gets triggered. Maybe that's due to the Console event being dispatched in a different container than the Drupal site's code and modules.

console_terminator.info.yml:

name: 'Console Terminator'
type: module
description: 'Terminates the Drupal Kernel when the Drupal Console terminates, fixing a console bug: https://github.com/hechoendrupal/drupal-console/issues/4025'
core_version_requirement: '^8.0.0 || ^9.0.0'

console_terminator.services.yml:

services:
  console_terminator:
    class: Drupal\console_terminator\EventSubscriber\ConsoleTerminator
    arguments: ['@kernel']
    tags:
      - { name: event_subscriber }

console_terminator/src/EventSubscriber/ConsoleTerminator.php:

<?php

namespace Drupal\console_terminator\EventSubscriber;

use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\TerminableInterface;

/**
 * Listen for ConsoleEvents::TERMINATE and dispatch KernelEvents::TERMINATE.
 */
class ConsoleTerminator implements EventSubscriberInterface {

  /**
   * The Drupal Kernel.
   *
   * @var \SSymfony\Component\HttpKernel\TerminableInterface
   */
  protected $kernel;

  /**
   * Construct the object.
   *
   * @param \Symfony\Component\HttpKernel\TerminableInterface $kernel
   *   The Drupal Kernel.
   */
  public function __construct(TerminableInterface $kernel) {
    $this->kernel = $kernel;
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents() {
    return [
      ConsoleEvents::TERMINATE => ['terminate'],
    ];
  }

  /**
   * Terminate the Drupal kernel when the console terminates.
   *
   * @param \Symfony\Component\Console\Event\ConsoleTerminateEvent $event
   *   The event to process.
   */
  public function terminate(ConsoleTerminateEvent $event) {
    if ($event->isPropagationStopped()) {
      return;
    }

    // Terminate the Drupal kernal.
    $this->kernel->terminate(new Request, new Response);
  }

}

This leads me to think that a solution will require DrupalConsole to call terminate() on the command's DrupalKernel.

adamfranco commented 3 years ago

In case it's helpful to anyone, here's what I had to add to my custom command class as a work-around for this bug, injecting the DrupalKernel service and then calling terminate() on it:

diff --git a/web/modules/custom/middlebury_event_sync/src/Command/EventSyncCommand.php b/web/modules/custom/middlebury_event_sync/src/Command/
index 55384c0..83e0b52 100644
--- a/web/modules/custom/middlebury_event_sync/src/Command/EventSyncCommand.php
+++ b/web/modules/custom/middlebury_event_sync/src/Command/EventSyncCommand.php
@@ -9,6 +9,9 @@
 use Drupal\Console\Core\Style\DrupalStyle;
 use Drupal\Console\Annotations\DrupalCommand; // @codingStandardsIgnoreLine
 use Drupal\middlebury_event_sync\Service\EventSyncer;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\TerminableInterface;

 /**
  * Command for syncing events.
@@ -28,10 +31,23 @@ class EventSyncCommand extends Command {
   protected $eventSyncer;

+  /**
+   * The Drupal Kernel.
+   *
+   * @var \Symfony\Component\HttpKernel\TerminableInterface
+   */
+  protected $kernel;
+
 /**
  * Constructs a new EventSyncCommand object.
  *
  * @param \Drupal\middlebury_event_sync\Service\EventSyncer $eventSyncer
  *   The EventSyncer service.
+   * @param \Symfony\Component\HttpKernel\TerminableInterface $kernel
+   *   The Drupal Kernel.
    */
-  public function __construct(EventSyncer $eventSyncer) {
+  public function __construct(EventSyncer $eventSyncer, TerminableInterface $kernel) {
     $this->eventSyncer = $eventSyncer;
+    $this->kernel = $kernel;
     parent::__construct();
   }

@@ -78,6 +94,10 @@ protected function execute(InputInterface $input, OutputInterface $output) {
     foreach ($messages as $message) {
       $io->info($message);
     }
+
+    // Work-around for Drupal-console not terminating the Drupal kernel.
+    // https://github.com/hechoendrupal/drupal-console/issues/4025
+    $this->kernel->terminate(new Request(), new Response());
   }