Problem/Motivation

The API for enabling modules fails silently if one of the modules is not present in the filesystem.

This is especially a problem in tests:

- tests can fail to install modules and not run correctly, but report no failure (#2387669: ConfigInstallWebTest is broken)
- tests can fail to install modules, and fail, but it's hard to find the source of the problem

Proposed resolution

Throw an exception when this is the case.

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Bug because Drupal fails in subtle ways without warning
Issue priority Major because this affects other subsystems and can cause unknown weaknesses.
Unfrozen changes Not unfrozen.
Prioritized changes The main goal of this issue is to remove a system fragility and improve stability. This is a prioritized change for the beta phase.
Disruption This is not disruptive because it only affects ModuleInstaller and some tests.

Original bug report from duplicate issue

Let's say you enable three modules in module_enable().

If one of them isn't in the file system, this is what happens:

   if (!isset($module_data[$module])) {
        // This module is not found in the filesystem, abort.
        return FALSE;
      }

No error gets thrown at all, and the rest of the modules don't get enabled either.

In my case this happened when a new module wasn't checked in to git, then a subsequent update called a function in it - the first error you get is the fatal error undefined function.

It probably makes sense to do a complete abort to avoid sites getting in a completely unrecoverable state, but could probably throw an expection instead of the return FALSE there.

This is less of an issue when only enabling one module - it's pretty obvious if it doesn't get enabled when it's not in the filesystem, but when it's a different module that happens to be enabled by the same function call but which otherwise would have been enabled without complaint it's a bit odd.

Original bug report

Drupal 7: https://api.drupal.org/api/drupal/includes%21module.inc/function/module_...
Drupal 8: https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Extension...

if (!isset($module_data[$module])) {
  // This module is not found in the filesystem, abort.
  return FALSE;
}

The above snippet should include a watchdog call. Otherwise it's very difficult to find out why the module_enable() function is failing.

In Drupal 7, when enabling multiple modules, all modules up to the one that is missing will be installed, and the ones after will not be installed. This can cause instability issues.

In Drupal 8, the ModuleHandler::install() function is different in that it first checks to see if the modules all exist before installing any of them. So it's implementation is better, but it still doesn't report to the watchdog which module is missing.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

ttkaminski’s picture

Category: feature » bug
marcingy’s picture

Category: bug » task
ttkaminski’s picture

Just to give a bit of background on this: I was working on an automated script to enable modules on a site, and module_enable() function was failing silently because a particular module was missing. Should be able to backport it to drupal 7 without problems.

kshama_deshmukh’s picture

Assigned: Unassigned » kshama_deshmukh
kshama_deshmukh’s picture

kshama_deshmukh’s picture

Status: Active » Needs review
kshama_deshmukh’s picture

Assigned: kshama_deshmukh » Unassigned
andrewmacpherson’s picture

This is a good idea. As well as missing modules, it will also help whenever there are module-name typos (e.g. trying to enable entity_reference in an update hook fails, because the correct name is entityreference).

andrewmacpherson’s picture

Status: Needs review » Needs work

The last submitted patch, module_not_found-module_enable-1719648-4.patch, failed testing.

joachim’s picture

Category: task » bug
Status: Needs work » Closed (duplicate)
ttkaminski’s picture

Status: Closed (duplicate) » Needs work

Why mark this one as duplicate? It was reported first, and has a more active thread than #1979508: ModuleHandler::install() silently fails if a module isn't in the file system. That one should be marked as a duplicate.

joachim’s picture

Feel free to switch them if you like -- that one had a better summary and I was lazy :(
You'd need to update the status and title of this one too.

joachim’s picture

Issue summary: View changes

Revised code snippet to remove debugging info

joachim’s picture

Title: module_enable should report an error to the watchdog when a module is not found on the file system » ModuleHandler::install() silently fails if a module isn't in the file system
Issue tags: +DX (Developer Experience), +Needs backport to D7

If this is being kept as the main issue, it needs the title and summary updating.

Also, the patch is not the best approach: an exception should be thrown so that this shows up in tests.

joachim’s picture

Issue summary: View changes

Added details about installing multiple modules and Drupal 8 details

joachim’s picture

Issue summary: View changes

combining both issue summaries

ttkaminski’s picture

Status: Needs work » Needs review
FileSize
1.11 KB

Here's a patch that uses the watchdog method. If you feel that an exception based approach is better, then provide a patch.

joachim’s picture

I don't see that a logging a watchdog error is useful at all -- the system will already be in an unpredictable state by then, and it's of no use in a testing scenario.

ttkaminski’s picture

For Drupal 8, throwing an exception is fine. However, you can't use that method when backporting to drupal 7, as it will break the api.

joachim’s picture

#1979508: ModuleHandler::install() silently fails if a module isn't in the file system was tagged as needing backport, so presumably there's a way to do it.

I'd anything that's trying to enable non-existent modules is broken. This is just a better way to detect the problem.

Status: Needs review » Needs work
Issue tags: -DX (Developer Experience), -Needs backport to D7

The last submitted patch, 1719648.16.drupal.module-install-missing-throw.patch, failed testing.

Anonymous’s picture

Issue summary: View changes

Updated issue summary.

joachim’s picture

joachim’s picture

Status: Needs work » Needs review
FileSize
1.26 KB

Status: Needs review » Needs work

The last submitted patch, 21: 1719648.21.drupal.module-install-missing-throw.patch, failed testing.

joachim’s picture

Assigned: Unassigned » joachim
joachim’s picture

jhedstrom’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll
joachim’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
6.37 KB

Wow, that patch was nearly a year old :/

Status: Needs review » Needs work

The last submitted patch, 26: 1719648.26.drupal.module-install-missing-throw.patch, failed testing.

jhedstrom’s picture

Looks like the failures caught some missing modules :)

+++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
@@ -69,9 +69,12 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
+        throw new \Exception(format_string('Unable to enable modules %modules due to missing modules %missing.', array(

I wonder if we should add a new exception type here, so calling code can specifically check for this failure, versus other exceptions that might be thrown during the install process?

joachim’s picture

Status: Needs work » Needs review
FileSize
6.98 KB

Weird. config_existing_default_config_test exists at core/modules/config/tests/config_existing_default_config_test. Why would it claim it's missing? The test in ConfigInstallWebTest is not expecting it to be missing.

> I wonder if we should add a new exception type here, so calling code can specifically check for this failure, versus other exceptions that might be thrown during the install process?

I like the idea, but the logical thing would be to have an ExtensionInstallerException rather than just a ModuleInstallerException, and change ThemeHandler::install() to throw it too... which is patch creep. If we make it just a ModuleInstallerException, then we're introducing a brand new little DrupalWTF, which will need clean-up in 9. But then I suppose that we're already changing the behaviour of missing modules compared to missing themes here anyway.

So let's try that then :)

Status: Needs review » Needs work

The last submitted patch, 29: 1719648.29.drupal.module-install-missing-throw.patch, failed testing.

joachim’s picture

This should fix the ModuleHandlerTest fail: I can see why that's going wrong.

As for the config test -- no idea. I'm uploading a totally separate patch just adds an assertion to that test, so we can see if it's actually the test that's broken and currently fails silently on 8.0.x.

The last submitted patch, 31: 1719648.31.drupal.module-install-missing-throw.patch, failed testing.

Status: Needs review » Needs work
joachim’s picture

Ha! #2387669: ConfigInstallWebTest is broken.

Once that's fixed, this should be green.

joachim’s picture

Issue summary: View changes
catch’s picture

Priority: Normal » Major

I've run into this with drush when there was a typo in a module name, or running it on the wrong install, very confusing.

jhedstrom’s picture

+++ b/core/lib/Drupal/Core/Extension/ModuleInstallerInterface.php
@@ -31,7 +31,10 @@
+   * @throws Exception

Should be updated to @throws ModuleInstallerException.

joachim’s picture

Status: Needs work » Needs review
FileSize
7.09 KB

Thanks!

Setting back to needs review for other humans to look at, though we know the testbot will fail it and why.

Status: Needs review » Needs work

The last submitted patch, 38: 1719648.38.drupal.module-install-missing-throw.patch, failed testing.

almaudoh’s picture

Title: ModuleHandler::install() silently fails if a module isn't in the file system » ModuleInstaller::install() silently fails if a module isn't in the file system
Issue summary: View changes

This is a great patch. Just a few nits:

  1. +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
    @@ -69,9 +69,12 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
    +        throw new ModuleInstallerException(format_string('Unable to enable modules %modules due to missing modules %missing.', array(
    
    +++ b/core/modules/simpletest/src/Tests/SimpleTestTest.php
    @@ -210,7 +210,7 @@ function assertNothing() {
    +    $this->assertAssertion(t('Unable to enable modules %modules due to missing modules %missing.', array('%modules' => 'non_existent_module', '%missing' => 'non_existent_module')), 'Other', 'Fail', 'SimpleTestTest.php', 'Drupal\simpletest\Tests\SimpleTestTest->setUp()');
    

    "Unable to install modules..." sounds better given we don't "enable" modules anymore.

  2. +++ b/core/modules/simpletest/src/WebTestBase.php
    @@ -913,8 +913,16 @@ protected function setUp() {
    +        $this->assertTrue($success, String::format('Enabled modules: %modules', array('%modules' => implode(', ', $modules))));
    +        $this->assertTrue($success, t('Enabled modules: %modules', array('%modules' => implode(', ', $modules))));
    

    s/Enabled/Installed/ same as above.

Updated the title and added beta evaluation.

joachim’s picture

Status: Needs work » Needs review
FileSize
6.98 KB

Thanks for reviewing!

+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -913,8 +913,16 @@ protected function setUp() {
-      $success = $container->get('module_installer')->install($modules, TRUE);
-      $this->assertTrue($success, String::format('Enabled modules: %modules', array('%modules' => implode(', ', $modules))));
+      try {
+        $success = $container->get('module_installer')->install($modules, TRUE);
+        $this->assertTrue($success, String::format('Enabled modules: %modules', array('%modules' => implode(', ', $modules))));
+        $this->assertTrue($success, t('Enabled modules: %modules', array('%modules' => implode(', ', $modules))));
+      }

That test assertion message was there before; I've merely moved it into the try{} block. There are quite a few occurrences of 'enabled modules' throughout core/modules/simpletest/src, so it's out of scope to fix this here I think. Let's leave that for a follow-on.

Though I have spotted that we have two identical lines there, one using t(), which probably shouldn't be there. So fixing that :)

Status: Needs review » Needs work

The last submitted patch, 41: 1719648.41.drupal.module-install-missing-throw.patch, failed testing.

Berdir’s picture

Can we add a similar check to KernelTestBase::enableModules() ?

We always end up with no longer existing modules/typos in those tests, would be nice to throw an exception instead.

Status: Needs work » Needs review
joachim’s picture

Status: Needs review » Needs work

We should revert as part of this patch this change from #2387669: ConfigInstallWebTest is broken, as checking the return of install() is no longer necessary:

+++ b/core/modules/config/src/Tests/ConfigInstallWebTest.php
@@ -132,7 +132,8 @@ function testInstallProfileConfigOverwrite() {
     // should be ignored.
-    \Drupal::service('module_installer')->install(array('config_existing_default_config_test'));
+    $status = \Drupal::service('module_installer')->install(array('config_existing_default_config_test'));
+    $this->assertTrue($status, "The module config_existing_default_config_test was installed.");
 
joachim’s picture

Status: Needs work » Needs review
FileSize
7.89 KB

Updated patch.

> Can we add a similar check to KernelTestBase::enableModules() ?

Can that be done in a separate issue? I'm not sure where to change something in KernelTestBase::enableModules, and it's going to need corresponding changes to KernelTestBaseTest, I assume.

almaudoh’s picture

Just another little nit:

+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -913,8 +913,15 @@ protected function setUp() {
+        $this->assertTrue($success, String::format('Enabled modules: %modules', array('%modules' => implode(', ', $modules))));

s/Enabled/Installed/

Otherwise, this looks RTBC to me.

dawehner’s picture

I really like these detail issues, which really reduces the annoying things on a day to day work.

  1. +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
    @@ -69,9 +69,12 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
    +        throw new ModuleInstallerException(format_string('Unable to install modules %modules due to missing modules %missing.', array(
    +          '%modules' => implode(', ', $module_list),
    +          '%missing' => implode(', ', $missing_modules),
    +        )));
    

    So in case we install multiple modules at the same time, can we figure out which of the request modules has the unmet dependency? Note: Let's directly use String::format() as format_string() is marked as deprecated.

  2. +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php
    @@ -87,7 +90,9 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
    +            throw new ModuleInstallerException(format_string('Depency module %name is missing.', array(
    +              '%name' => $module,
    +            )));
    

    Note: Let's directly use String::format() as format_string() is marked as deprecated.

    +

    Let's use 'Dependency module %name is missing.'

joachim’s picture

New patch with changes requested in #48.

+            throw new ModuleInstallerException(format_string('Depency module %name is missing.', array(
+              '%name' => $module,

Turns out that $module IS the module with the missing dependency, so that message was wrong. Fixed and extended (and typo fixed too).

> +++ b/core/modules/simpletest/src/WebTestBase.php
> s/Enabled/Installed/

That's an assertion that's just getting moved into a try-catch block. There are uses of 'enabled' all over that test, so best all covered up in a follow-up.

Status: Needs review » Needs work

The last submitted patch, 49: 1719648.49.drupal.module-install-missing-throw.patch, failed testing.

joachim’s picture

Oh I probably need to make the string change in the tests too... :/

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 49: 1719648.49.drupal.module-install-missing-throw.patch, failed testing.

joachim’s picture

Status: Needs work » Needs review
FileSize
8 KB

Rerolled.

jhedstrom’s picture

Status: Needs review » Reviewed & tested by the community

I think this is RTBC.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 54: 1719648.54.drupal.module-install-missing-throw.patch, failed testing.

jhedstrom’s picture

Bizarre, that was green earlier.

joachim’s picture

Status: Needs work » Needs review
FileSize
8.22 KB

This should be!

(The problem was a missing 'use Drupal\Component\Utility\String;' at the top of ModuleInstaller.php.)

Berdir’s picture

+++ b/core/lib/Drupal/Core/Extension/ModuleInstallerException.php
@@ -0,0 +1,15 @@
+/**
+ * Exception class to throw when modules are missing on install.
+ *
+ * @see \Drupal\Core\Extension\ModuleInstaller::install()
+ */
+class ModuleInstallerException extends \Exception {}

If the exception is specific to that, then we should name it accordingly. MissingDependencyException or so.

joachim’s picture

jhedstrom’s picture

+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -916,8 +916,15 @@ protected function setUp() {
+      catch (\Exception $e) {

+++ b/core/modules/system/src/Tests/Extension/ModuleHandlerTest.php
@@ -116,8 +116,14 @@ function testDependencyResolution() {
+    catch (\Exception $e) {

I think these should only catch the expected exception, not all exceptions. Sorry I didn't notice earlier.

joachim’s picture

jhedstrom’s picture

Status: Needs review » Reviewed & tested by the community

I think this is now RTBC.

dawehner’s picture

@joachim
Just make sure that you do interdiffs in the future.

I love this issue, it is certainly a nice DX improvement.

catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.0.x, thanks!

  • catch committed 78e9940 on 8.0.x
    Issue #1719648 by joachim, ttkaminski, kshama_deshmukh: ModuleInstaller...
David_Rothstein’s picture

Title: ModuleInstaller::install() silently fails if a module isn't in the file system » ModuleInstaller::install() should provide more information than simply returning FALSE when a module isn't in the file system
Version: 8.0.x-dev » 7.x-dev
Category: Bug report » Task
Priority: Major » Normal
Status: Fixed » Patch (to be ported)

This is marked as a bug for Drupal 7 backport, but I don't see how it's backportable in its current form (could very easily break existing code to throw an exception where returning FALSE was previously expected), nor do I see a bug... I can't reproduce this at all (from the issue summary):

In Drupal 7, when enabling multiple modules, all modules up to the one that is missing will be installed, and the ones after will not be installed. This can cause instability issues.

If someone can reproduce this, please provide more details on how to do so.

I guess we could do a watchdog message instead (i.e. the earlier suggestion in this issue) if people think it would be useful. Generally I would think the caller should decide that kind of thing, but it's hard for the caller to determine and the function already does watchdog logging in other places.

  • catch committed 78e9940 on 8.1.x
    Issue #1719648 by joachim, ttkaminski, kshama_deshmukh: ModuleInstaller...

  • catch committed 78e9940 on 8.3.x
    Issue #1719648 by joachim, ttkaminski, kshama_deshmukh: ModuleInstaller...

  • catch committed 78e9940 on 8.3.x
    Issue #1719648 by joachim, ttkaminski, kshama_deshmukh: ModuleInstaller...

  • catch committed 78e9940 on 8.4.x
    Issue #1719648 by joachim, ttkaminski, kshama_deshmukh: ModuleInstaller...

  • catch committed 78e9940 on 8.4.x
    Issue #1719648 by joachim, ttkaminski, kshama_deshmukh: ModuleInstaller...