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
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.
Comments
Comment #1
ttkaminski CreditAttribution: ttkaminski commentedComment #2
marcingy CreditAttribution: marcingy commentedComment #3
ttkaminski CreditAttribution: ttkaminski commentedJust 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.
Comment #4
kshama_deshmukh CreditAttribution: kshama_deshmukh commentedComment #5
kshama_deshmukh CreditAttribution: kshama_deshmukh commentedComment #6
kshama_deshmukh CreditAttribution: kshama_deshmukh commentedComment #7
kshama_deshmukh CreditAttribution: kshama_deshmukh commentedComment #8
andrewmacpherson CreditAttribution: andrewmacpherson commentedThis 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).
Comment #9
andrewmacpherson CreditAttribution: andrewmacpherson commented#5: module_not_found-module_enable-1719648-4.patch queued for re-testing.
Comment #11
joachim CreditAttribution: joachim commentedDuplicate of #1979508: ModuleHandler::install() silently fails if a module isn't in the file system.
Comment #12
ttkaminski CreditAttribution: ttkaminski commentedWhy 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.
Comment #13
joachim CreditAttribution: joachim commentedFeel 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.
Comment #13.0
joachim CreditAttribution: joachim commentedRevised code snippet to remove debugging info
Comment #14
joachim CreditAttribution: joachim commentedIf 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.
Comment #14.0
joachim CreditAttribution: joachim commentedAdded details about installing multiple modules and Drupal 8 details
Comment #14.1
joachim CreditAttribution: joachim commentedcombining both issue summaries
Comment #15
ttkaminski CreditAttribution: ttkaminski commentedHere's a patch that uses the watchdog method. If you feel that an exception based approach is better, then provide a patch.
Comment #16
joachim CreditAttribution: joachim commentedI 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.
Comment #17
ttkaminski CreditAttribution: ttkaminski commentedFor 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.
Comment #18
joachim CreditAttribution: joachim commented#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.
Comment #19.0
(not verified) CreditAttribution: commentedUpdated issue summary.
Comment #20
joachim CreditAttribution: joachim commentedComment #21
joachim CreditAttribution: joachim commentedComment #23
joachim CreditAttribution: joachim commentedComment #24
joachim CreditAttribution: joachim commentedFigured out the failing tests.
Comment #25
jhedstromVery much still an issue, but needs a reroll after #2324055: Split up the module manager into runtime information and extension information..
Comment #26
joachim CreditAttribution: joachim commentedWow, that patch was nearly a year old :/
Comment #28
jhedstromLooks like the failures caught some missing modules :)
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?
Comment #29
joachim CreditAttribution: joachim commentedWeird. 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 :)
Comment #31
joachim CreditAttribution: joachim commentedThis 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.
Comment #34
joachim CreditAttribution: joachim commentedHa! #2387669: ConfigInstallWebTest is broken.
Once that's fixed, this should be green.
Comment #35
joachim CreditAttribution: joachim commentedComment #36
catchI've run into this with drush when there was a typo in a module name, or running it on the wrong install, very confusing.
Comment #37
jhedstromShould be updated to
@throws ModuleInstallerException
.Comment #38
joachim CreditAttribution: joachim commentedThanks!
Setting back to needs review for other humans to look at, though we know the testbot will fail it and why.
Comment #40
almaudoh CreditAttribution: almaudoh commentedThis is a great patch. Just a few nits:
"Unable to install modules..." sounds better given we don't "enable" modules anymore.
s/Enabled/Installed/ same as above.
Updated the title and added beta evaluation.
Comment #41
joachim CreditAttribution: joachim commentedThanks for reviewing!
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 :)
Comment #43
BerdirCan 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.
Comment #45
joachim CreditAttribution: joachim commentedWe should revert as part of this patch this change from #2387669: ConfigInstallWebTest is broken, as checking the return of install() is no longer necessary:
Comment #46
joachim CreditAttribution: joachim commentedUpdated 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.
Comment #47
almaudoh CreditAttribution: almaudoh commentedJust another little nit:
s/Enabled/Installed/
Otherwise, this looks RTBC to me.
Comment #48
dawehnerI really like these detail issues, which really reduces the annoying things on a day to day work.
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.
Note: Let's directly use String::format() as format_string() is marked as deprecated.
+
Let's use 'Dependency module %name is missing.'
Comment #49
joachim CreditAttribution: joachim commentedNew patch with changes requested in #48.
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.
Comment #51
joachim CreditAttribution: joachim commentedOh I probably need to make the string change in the tests too... :/
Comment #54
joachim CreditAttribution: joachim commentedRerolled.
Comment #55
jhedstromI think this is RTBC.
Comment #57
jhedstromBizarre, that was green earlier.
Comment #58
joachim CreditAttribution: joachim commentedThis should be!
(The problem was a missing 'use Drupal\Component\Utility\String;' at the top of ModuleInstaller.php.)
Comment #59
BerdirIf the exception is specific to that, then we should name it accordingly. MissingDependencyException or so.
Comment #60
joachim CreditAttribution: joachim commentedDone.
Comment #61
jhedstromI think these should only catch the expected exception, not all exceptions. Sorry I didn't notice earlier.
Comment #62
joachim CreditAttribution: joachim commentedDone.
Comment #63
jhedstromI think this is now RTBC.
Comment #64
dawehner@joachim
Just make sure that you do interdiffs in the future.
I love this issue, it is certainly a nice DX improvement.
Comment #65
catchCommitted/pushed to 8.0.x, thanks!
Comment #67
David_Rothstein CreditAttribution: David_Rothstein commentedThis 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):
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.