Bug 1896775 - set up a category manager helper in XPCOMUtils to enable easy JS dependency injection, r=mossop

Differential Revision: https://phabricator.services.mozilla.com/D210432
This commit is contained in:
Gijs Kruitbosch
2024-08-14 13:10:40 +00:00
parent a487867169
commit 17fa41a11d
5 changed files with 174 additions and 0 deletions

View File

@@ -31,6 +31,69 @@ function redefine(object, prop, value) {
return value;
}
let lazy = {};
ChromeUtils.defineLazyGetter(lazy, "CatManListenerManager", () => {
const CatManListenerManager = {
cachedModules: {},
cachedListeners: {},
// All 3 category manager notifications will have the category name
// as the `data` part of the observer notification.
observe(_subject, _topic, categoryName) {
delete this.cachedListeners[categoryName];
},
/**
* Fetch and parse category manager consumers for a given category name.
* Will use cachedListeners for the given category name if they exist.
*/
getListeners(categoryName) {
if (Object.hasOwn(this.cachedListeners, categoryName)) {
return this.cachedListeners[categoryName];
}
let rv = Array.from(
Services.catMan.enumerateCategory(categoryName),
({ data: module, value }) => {
try {
let [objName, method] = value.split(".");
if (!Object.hasOwn(this.cachedModules, module)) {
this.cachedModules[module] = ChromeUtils.importESModule(module);
}
let fn = async (...args) => {
try {
// This await doesn't do much as the caller won't await us,
// but means we can catch and report any exceptions.
await this.cachedModules[module][objName][method](...args);
} catch (ex) {
console.error(
`Error in processing ${categoryName} for ${objName}`
);
console.error(ex);
}
};
return fn;
} catch (ex) {
console.error(
`Error processing category manifest for ${module}: ${value}`,
ex
);
return null;
}
}
);
// Remove any null entries.
rv = rv.filter(l => !!l);
this.cachedListeners[categoryName] = rv;
return rv;
},
};
Services.obs.addObserver(
CatManListenerManager,
"xpcom-category-entry-removed"
);
Services.obs.addObserver(CatManListenerManager, "xpcom-category-entry-added");
Services.obs.addObserver(CatManListenerManager, "xpcom-category-cleared");
return CatManListenerManager;
});
export var XPCOMUtils = {
/**
* Defines a getter on a specified object that will be created upon first use.
@@ -322,6 +385,22 @@ export var XPCOMUtils = {
writable: false,
});
},
/**
* Invoke all the category manager consumers of a given JS consumer.
* Similar to the (C++-only) NS_CreateServicesFromCategory in that it'll
* abstract away the actual work of invoking the modules/services.
* Different in that it's JS-only and will invoke methods in modules
* instead of using XPCOM services.
*/
callModulesFromCategory(categoryName, ...args) {
for (let listener of lazy.CatManListenerManager.getListeners(
categoryName
)) {
// Note that we deliberately do not await anything here.
listener(...args);
}
},
};
ChromeUtils.defineLazyGetter(XPCOMUtils, "_scriptloader", () => {

View File

@@ -0,0 +1,5 @@
export let Module1 = {
test(arg) {
Services.obs.notifyObservers(null, "test-modules-from-catman-notification", arg);
},
};

View File

@@ -0,0 +1,6 @@
export let Module2 = {
othertest(arg) {
Services.obs.notifyObservers(null, "test-modules-from-catman-other-notification", arg);
},
};

View File

@@ -12,6 +12,7 @@
const {AppConstants} = ChromeUtils.importESModule("resource://gre/modules/AppConstants.sys.mjs");
const {ComponentUtils} = ChromeUtils.importESModule("resource://gre/modules/ComponentUtils.sys.mjs");
const {Preferences} = ChromeUtils.importESModule("resource://gre/modules/Preferences.sys.mjs");
const {TestUtils} = ChromeUtils.importESModule("resource://testing-common/TestUtils.sys.mjs");
const {XPCOMUtils} = ChromeUtils.importESModule("resource://gre/modules/XPCOMUtils.sys.mjs");
////////////////////////////////////////////////////////////////////////////////
@@ -239,6 +240,85 @@ add_test(function test_generateSingletonFactory()
run_next_test();
});
/**
* Verify that category manager calling modules are loaded on-demand,
* and that caching doesn't break adding more modules as category entries
* at runtime.
*/
add_test(async function test_callModulesFromCategory() {
const MODULE1 = "resource://test/my_catman_1.sys.mjs";
const MODULE2 = "resource://test/my_catman_2.sys.mjs";
const CATEGORY = "test-modules-from-catman";
const OBSTOPIC1 = CATEGORY + "-notification";
const OBSTOPIC2 = CATEGORY + "-other-notification";
// The two modules both fire different observer topics to allow us to ensure
// they have been called. This helper just makes it easier to get only
// that return value as a result of a promise, as `topicObserved` also
// returns the "subject" of the observer notification, which we don't care about.
let rvFromModule = topic => TestUtils.topicObserved(topic).then(
([_subj, data]) => data
);
// Start off with nothing in a category:
Assert.equal(Cu.isESModuleLoaded(MODULE1), false, "First module should not be loaded.");
let catEntries = Array.from(Services.catMan.enumerateCategory(CATEGORY));
Assert.deepEqual(catEntries, [], "Should be no entries for this category.");
try {
// There's nothing in this category right now so this should be a no-op.
XPCOMUtils.callModulesFromCategory(CATEGORY, "Hello");
} catch (ex) {
Assert.ok(false, `Should not have thrown but received an exception ${ex}`);
}
// Now add an item, check that calling it now works.
//
// Note that category manager observer notifications are async (they get
// dispatched as runnables) and so we have to wait for it to make sure that
// XPCOMUtils has had a chance of being told new entries have arrived.
let catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
Services.catMan.addCategoryEntry(CATEGORY, MODULE1, `Module1.test`, false, false);
catEntries = Array.from(Services.catMan.enumerateCategory(CATEGORY));
Assert.equal(catEntries.length, 1);
// See note above.
await catManUpdated;
Assert.equal(Cu.isESModuleLoaded(MODULE1), false, "First module should still not be loaded.");
// This entry will cause an observer topic to notify, so ensure that happens.
let moduleResult = rvFromModule(OBSTOPIC1);
XPCOMUtils.callModulesFromCategory(CATEGORY, "Hello");
Assert.equal(Cu.isESModuleLoaded(MODULE1), true, "First module should be loaded sync.");
Assert.equal("Hello", await moduleResult, "Should have been called.");
// Now add another item, check that both are called.
catManUpdated = TestUtils.topicObserved("xpcom-category-entry-added");
Services.catMan.addCategoryEntry(CATEGORY, MODULE2, `Module2.othertest`, false, false);
await catManUpdated;
moduleResult = Promise.all([rvFromModule(OBSTOPIC1), rvFromModule(OBSTOPIC2)]);
XPCOMUtils.callModulesFromCategory(CATEGORY, "Hello");
Assert.deepEqual(["Hello", "Hello"], await moduleResult, "Both modules should have been called.");
// Now remove the first module again, check that only the second one notifies.
catManUpdated = TestUtils.topicObserved("xpcom-category-entry-removed");
Services.catMan.deleteCategoryEntry(CATEGORY, MODULE1, false);
await catManUpdated;
let ob = () => Assert.ok(false, "I shouldn't be called.");
Services.obs.addObserver(ob, OBSTOPIC1);
moduleResult = rvFromModule(OBSTOPIC2);
XPCOMUtils.callModulesFromCategory(CATEGORY, "Hello");
Assert.equal("Hello", await moduleResult, "Second module should still be called.");
Services.obs.removeObserver(ob, OBSTOPIC1);
run_next_test();
});
////////////////////////////////////////////////////////////////////////////////
//// Test Runner

View File

@@ -427,6 +427,10 @@ head = "head_watchdog.js"
["test_wrapped_js_enumerator.js"]
["test_xpcomutils.js"]
support-files = [
"my_catman_1.sys.mjs",
"my_catman_2.sys.mjs",
]
["test_xpcwn_instanceof.js"]