--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -1445,17 +1445,17 @@ class Extension extends ExtensionData {
return (AddonSettings.ALLOW_LEGACY_EXTENSIONS ||
this.isPrivileged);
}
saveStartupData() {
if (this.dontSaveStartupData) {
return;
}
- AddonManagerPrivate.setStartupData(this.id, this.startupData);
+ XPIProvider.setStartupData(this.id, this.startupData);
}
async _parseManifest() {
let manifest = await super.parseManifest();
if (manifest && manifest.permissions.has("mozillaAddons") &&
!this.isPrivileged) {
Cu.reportError(`Stripping mozillaAddons permission from ${this.id}`);
manifest.permissions.delete("mozillaAddons");
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -462,19 +462,28 @@ class Messenger {
return StrongPromise.wrap(result, channelId, caller);
} else if (result === true) {
return StrongPromise.wrap(promise, channelId, caller);
}
return response;
},
};
+ const childManager = this.context.viewType == "background" ? this.context.childManager : null;
MessageChannel.addListener(this.messageManagers, "Extension:Message", listener);
+ if (childManager) {
+ childManager.callParentFunctionNoReturn("runtime.addMessagingListener",
+ ["onMessage"]);
+ }
return () => {
MessageChannel.removeListener(this.messageManagers, "Extension:Message", listener);
+ if (childManager) {
+ childManager.callParentFunctionNoReturn("runtime.removeMessagingListener",
+ ["onMessage"]);
+ }
};
},
}).api();
}
onMessage(name) {
return this._onMessage(name, sender => sender.id === this.sender.id);
}
@@ -547,19 +556,28 @@ class Messenger {
delete recipient.tab;
}
let port = new Port(this.context, mm, this.messageManagers, name, portId, sender, recipient);
fire.asyncWithoutClone(port.api());
return true;
},
};
+ const childManager = this.context.viewType == "background" ? this.context.childManager : null;
MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
+ if (childManager) {
+ childManager.callParentFunctionNoReturn("runtime.addMessagingListener",
+ ["onConnect"]);
+ }
return () => {
MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
+ if (childManager) {
+ childManager.callParentFunctionNoReturn("runtime.removeMessagingListener",
+ ["onConnect"]);
+ }
};
},
}).api();
}
onConnect(name) {
return this._onConnect(name, sender => sender.id === this.sender.id);
}
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -1822,16 +1822,29 @@ class EventManager {
let {unregister, convert} = api.primeListener(extension, event, fire, listener.params);
Object.assign(primed, {unregister, convert});
}
}
}
}
+ // Remove a primed listener for the given event (with the given extra
+ // addListener arguments). This ordinarily happens as a side effect of
+ // calling addListener(), but APIs that need special handling (e.g.,
+ // runtime.onConnect and onMessage which don't have EventManagers in the
+ // parent process) can use this directly.
+ static clearOnePrimedListener(extension, module, event, args = []) {
+ let key = uneval(args);
+ let listener = extension.persistentListeners.get(module).get(event).get(key);
+ if (listener.primed) {
+ listener.primed = null;
+ }
+ }
+
// Remove any primed listeners that were not re-registered.
// This function is called after the background page has started.
static clearPrimedListeners(extension) {
for (let [module, moduleEntry] of extension.persistentListeners) {
for (let [event, listeners] of moduleEntry) {
for (let [key, listener] of listeners) {
let {primed} = listener;
if (!primed) {
@@ -1847,27 +1860,27 @@ class EventManager {
}
}
}
}
// Record the fact that there is a listener for the given event in
// the given extension. `args` is an Array containing any extra
// arguments that were passed to addListener().
- static savePersistentListener(extension, module, event, args) {
+ static savePersistentListener(extension, module, event, args = []) {
EventManager._initPersistentListeners(extension);
let key = uneval(args);
extension.persistentListeners.get(module).get(event).set(key, {params: args});
EventManager._writePersistentListeners(extension);
}
// Remove the record for the given event listener from the extension's
// startup data. `key` must be a string, the result of calling uneval()
// on the array of extra arguments originally passed to addListener().
- static clearPersistentListener(extension, module, event, key) {
+ static clearPersistentListener(extension, module, event, key = uneval([])) {
let listeners = extension.persistentListeners.get(module).get(event);
listeners.delete(key);
if (listeners.size == 0) {
let moduleEntry = extension.persistentListeners.get(module);
moduleEntry.delete(event);
if (moduleEntry.size == 0) {
extension.persistentListeners.delete(module);
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -340,16 +340,21 @@ ProxyMessenger = {
}
const noHandlerError = {
result: MessageChannel.RESULT_NO_HANDLER,
message: "No matching message handler for the given recipient.",
};
let extension = GlobalManager.extensionMap.get(sender.extensionId);
+
+ if (extension.wakeupBackground) {
+ await extension.wakeupBackground();
+ }
+
let {
messageManager: receiverMM,
xulBrowser: receiverBrowser,
} = this.getMessageManagerForRecipient(recipient);
if (!extension || !receiverMM) {
return Promise.reject(noHandlerError);
}
@@ -1724,41 +1729,40 @@ class CacheStore {
StartupCache.save();
}
}
for (let name of StartupCache.STORE_NAMES) {
StartupCache[name] = new CacheStore(name);
}
-function makeStartupPromise(event) {
- return ExtensionUtils.promiseObserved(event).then(() => {});
-}
-
-// browserPaintedPromise and browserStartupPromise are promises that
-// resolve after the first browser window is painted and after browser
-// windows have been restored, respectively.
-let browserPaintedPromise = makeStartupPromise("browser-delayed-startup-finished");
-let browserStartupPromise = makeStartupPromise("sessionstore-windows-restored");
-
var ExtensionParent = {
GlobalManager,
HiddenExtensionPage,
IconDetails,
ParentAPIManager,
StartupCache,
WebExtensionPolicy,
apiManager,
- browserPaintedPromise,
- browserStartupPromise,
promiseExtensionViewLoaded,
watchExtensionProxyContextLoad,
DebugUtils,
};
+// browserPaintedPromise and browserStartupPromise are promises that
+// resolve after the first browser window is painted and after browser
+// windows have been restored, respectively.
+// _resetStartupPromises should only be called from outside this file in tests.
+ExtensionParent._resetStartupPromises = () => {
+ ExtensionParent.browserPaintedPromise = promiseObserved("browser-delayed-startup-finished").then(() => {});
+ ExtensionParent.browserStartupPromise = promiseObserved("sessionstore-windows-restored").then(() => {});
+};
+ExtensionParent._resetStartupPromises();
+
+
XPCOMUtils.defineLazyGetter(ExtensionParent, "PlatformInfo", () => {
return Object.freeze({
os: (function() {
let os = AppConstants.platform;
if (os == "macosx") {
os = "mac";
}
return os;
--- a/toolkit/components/extensions/parent/ext-backgroundPage.js
+++ b/toolkit/components/extensions/parent/ext-backgroundPage.js
@@ -1,18 +1,16 @@
"use strict";
ChromeUtils.defineModuleGetter(this, "TelemetryStopwatch",
"resource://gre/modules/TelemetryStopwatch.jsm");
ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
var {
HiddenExtensionPage,
- browserPaintedPromise,
- browserStartupPromise,
promiseExtensionViewLoaded,
} = ExtensionParent;
XPCOMUtils.defineLazyPreferenceGetter(this, "DELAYED_STARTUP",
"extensions.webextensions.background-delayed-startup");
// Responsible for the background_page section of the manifest.
class BackgroundPage extends HiddenExtensionPage {
@@ -77,21 +75,21 @@ this.backgroundPage = class extends Exte
// There are two ways to start the background page:
// 1. If a primed event fires, then start the background page as
// soon as we have painted a browser window. Note that we have
// to touch browserPaintedPromise here to initialize the listener
// or else we can miss it if the event occurs after the first
// window is painted but before #2
// 2. After all windows have been restored.
extension.once("background-page-event", async () => {
- await browserPaintedPromise;
+ await ExtensionParent.browserPaintedPromise;
extension.emit("start-background-page");
});
- browserStartupPromise.then(() => {
+ ExtensionParent.browserStartupPromise.then(() => {
extension.emit("start-background-page");
});
}
onShutdown() {
this.bgPage.shutdown();
}
};
--- a/toolkit/components/extensions/parent/ext-runtime.js
+++ b/toolkit/components/extensions/parent/ext-runtime.js
@@ -1,22 +1,30 @@
"use strict";
ChromeUtils.defineModuleGetter(this, "AddonManager",
"resource://gre/modules/AddonManager.jsm");
ChromeUtils.defineModuleGetter(this, "AddonManagerPrivate",
"resource://gre/modules/AddonManager.jsm");
+ChromeUtils.defineModuleGetter(this, "ExtensionCommon",
+ "resource://gre/modules/ExtensionCommon.jsm");
ChromeUtils.defineModuleGetter(this, "ExtensionParent",
"resource://gre/modules/ExtensionParent.jsm");
ChromeUtils.defineModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
ChromeUtils.defineModuleGetter(this, "DevToolsShim",
"chrome://devtools-startup/content/DevToolsShim.jsm");
this.runtime = class extends ExtensionAPI {
+ constructor(...args) {
+ super(...args);
+
+ this.messagingListeners = new Map();
+ }
+
getAPI(context) {
let {extension} = context;
return {
runtime: {
onStartup: new EventManager({
context,
name: "runtime.onStartup",
register: fire => {
@@ -150,12 +158,58 @@ this.runtime = class extends ExtensionAP
// This function is not exposed to the extension js code and it is only
// used by the alert function redefined into the background pages to be
// able to open the BrowserConsole from the main process.
openBrowserConsole() {
if (AppConstants.platform !== "android") {
DevToolsShim.openBrowserConsole();
}
},
+
+ // Used internally by onMessage/onConnect
+ addMessagingListener: event => {
+ let count = (this.messagingListeners.get(event) || 0) + 1;
+ this.messagingListeners.set(event, count);
+ if (count == 1) {
+ ExtensionCommon.EventManager.savePersistentListener(extension,
+ "runtime", event);
+ }
+
+ ExtensionCommon.EventManager.clearOnePrimedListener(extension,
+ "runtime", event);
+ },
+
+ removeMessagingListener: event => {
+ let count = this.messagingListeners.get(event);
+ if (!count) {
+ return;
+ }
+ this.messagingListeners.set(event, --count);
+ if (count == 0) {
+ ExtensionCommon.EventManager.clearPersistentListener(extension,
+ "runtime", event);
+ }
+ },
+ },
+ };
+ }
+
+ primeListener(extension, event, fire, params) {
+ // The real work happens in ProxyMessenger which, if
+ // extension.wakeupBackground is set, holds the underlying messages
+ // that implement extension messaging until its Promise resolves.
+ // We rely on the ordering of these messages being preserved so be
+ // careful here to always return the same Promise, otherwise promise
+ // scheduling can inadvertently re-order messages.
+ extension.wakeupBackground = () => {
+ let promise = fire.wakeup();
+ promise.then(() => { extension.wakeupBackground = undefined; });
+ extension.wakeupBackground = () => promise;
+ return promise;
+ };
+
+ return {
+ unregister() {
+ extension.wakeupBackground = undefined;
},
};
}
};
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_messaging_startup.js
@@ -0,0 +1,176 @@
+"use strict";
+
+PromiseTestUtils.whitelistRejectionsGlobally(/Message manager disconnected/);
+
+const server = createHttpServer({hosts: ["example.com"]});
+server.registerDirectory("/data/", do_get_file("data"));
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "43");
+
+let {
+ promiseRestartManager,
+ promiseShutdownManager,
+ promiseStartupManager,
+} = AddonTestUtils;
+
+Services.prefs.setBoolPref("extensions.webextensions.background-delayed-startup", true);
+
+function trackEvents(wrapper) {
+ let events = new Map();
+ for (let event of ["background-page-event", "start-background-page"]) {
+ events.set(event, false);
+ wrapper.extension.once(event, () => events.set(event, true));
+ }
+ return events;
+}
+
+async function test(what, background, script) {
+ await promiseStartupManager();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+
+ manifest: {
+ content_scripts: [
+ {
+ matches: ["http://example.com/*"],
+ js: ["script.js"],
+ },
+ ],
+ },
+
+ files: {
+ "page.html": `<!DOCTYPE html><meta charset="utf-8"><script src="script.js"></script>`,
+ "script.js": script,
+ },
+
+ background,
+ });
+
+ info(`Set up ${what} listener`);
+ await extension.startup();
+ await extension.awaitMessage("bg-ran");
+
+ info(`Test wakeup for ${what} from an extension page`);
+ await promiseRestartManager();
+ await extension.awaitStartup();
+
+ function awaitBgEvent() {
+ return new Promise(resolve => extension.extension.once("background-page-event", resolve));
+ }
+
+ let events = trackEvents(extension);
+
+ let url = extension.extension.baseURI.resolve("page.html");
+
+ let [, page] = await Promise.all([
+ awaitBgEvent(),
+ ExtensionTestUtils.loadContentPage(url, {extension}),
+ ]);
+
+ equal(events.get("background-page-event"), true,
+ "Should have gotten a background page event");
+ equal(events.get("start-background-page"), false,
+ "Background page should not be started");
+
+ equal(extension.messageQueue.size, 0, "Have not yet received bg-ran message");
+
+ let promise = extension.awaitMessage("bg-ran");
+ Services.obs.notifyObservers(null, "browser-delayed-startup-finished");
+ await promise;
+
+ equal(events.get("start-background-page"), true,
+ "Should have gotten start-background-page event");
+
+ await extension.awaitFinish("messaging-test");
+ ok(true, "Background page loaded and received message from extension page");
+
+ await page.close();
+
+ info(`Test wakeup for ${what} from a content script`);
+ ExtensionParent._resetStartupPromises();
+ await promiseRestartManager();
+ await extension.awaitStartup();
+
+ events = trackEvents(extension);
+
+ [, page] = await Promise.all([
+ awaitBgEvent(),
+ ExtensionTestUtils.loadContentPage("http://example.com/data/file_sample.html"),
+ ]);
+
+ equal(events.get("background-page-event"), true,
+ "Should have gotten a background page event");
+ equal(events.get("start-background-page"), false,
+ "Background page should not be started");
+
+ equal(extension.messageQueue.size, 0, "Have not yet received bg-ran message");
+
+ promise = extension.awaitMessage("bg-ran");
+ Services.obs.notifyObservers(null, "browser-delayed-startup-finished");
+ await promise;
+
+ equal(events.get("start-background-page"), true,
+ "Should have gotten start-background-page event");
+
+ await extension.awaitFinish("messaging-test");
+ ok(true, "Background page loaded and received message from content script");
+
+ await page.close();
+ await extension.unload();
+
+ ExtensionParent._resetStartupPromises();
+ await promiseShutdownManager();
+}
+
+add_task(function test_onMessage() {
+ function script() {
+ browser.runtime.sendMessage("ping").then(reply => {
+ browser.test.assertEq(reply, "pong", "Extension page received pong reply");
+ browser.test.notifyPass("messaging-test");
+ });
+ }
+
+ async function background() {
+ browser.runtime.onMessage.addListener((msg, sender) => {
+ browser.test.assertEq(msg, "ping", "Background page received ping message");
+ return Promise.resolve("pong");
+ });
+
+ // addListener() returns right away but make a round trip to the
+ // main process to ensure the persistent onMessage listener is recorded.
+ await browser.runtime.getBrowserInfo();
+ browser.test.sendMessage("bg-ran");
+ }
+
+ return test("onMessage", background, script);
+});
+
+add_task(function test_onConnect() {
+ function script() {
+ let port = browser.runtime.connect();
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq(msg, "pong", "Extension page received pong reply");
+ browser.test.notifyPass("messaging-test");
+ });
+ port.postMessage("ping");
+ }
+
+ async function background() {
+ browser.runtime.onConnect.addListener(port => {
+ port.onMessage.addListener(msg => {
+ browser.test.assertEq(msg, "ping", "Background page received ping message");
+ port.postMessage("pong");
+ });
+ });
+
+ // addListener() returns right away but make a round trip to the
+ // main process to ensure the persistent onMessage listener is recorded.
+ await browser.runtime.getBrowserInfo();
+ browser.test.sendMessage("bg-ran");
+ }
+
+ return test("onConnect", background, script);
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -54,16 +54,17 @@ skip-if = os == "android" # checking for
[test_ext_geturl.js]
[test_ext_idle.js]
[test_ext_legacy_extension_context.js]
[test_ext_legacy_extension_embedding.js]
[test_ext_localStorage.js]
[test_ext_management.js]
skip-if = (os == "win" && !debug) #Bug 1419183 disable on Windows
[test_ext_management_uninstall_self.js]
+[test_ext_messaging_startup.js]
[test_ext_onmessage_removelistener.js]
skip-if = true # This test no longer tests what it is meant to test.
[test_ext_permission_xhr.js]
[test_ext_persistent_events.js]
[test_ext_privacy.js]
[test_ext_privacy_disable.js]
[test_ext_privacy_update.js]
[test_ext_proxy_auth.js]
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -3020,36 +3020,16 @@ var AddonManagerPrivate = {
return AddonManagerInternal._getProviderByName("XPIProvider")
.isTemporaryInstallID(extensionId);
},
isDBLoaded() {
let provider = AddonManagerInternal._getProviderByName("XPIProvider");
return provider ? provider.isDBLoaded : false;
},
-
- /**
- * Sets startupData for the given addon. The provided data will be stored
- * in addonsStartup.json so it is available early during browser startup.
- * Note that this file is read synchronously at startup, so startupData
- * should be used with care.
- *
- * @param {string} aID
- * The id of the addon to save startup data for.
- * @param {any} aData
- * The data to store. Must be JSON serializable.
- */
- setStartupData(aID, aData) {
- if (!gStarted)
- throw Components.Exception("AddonManager is not initialized",
- Cr.NS_ERROR_NOT_INITIALIZED);
-
- AddonManagerInternal._getProviderByName("XPIProvider")
- .setStartupData(aID, aData);
- },
};
/**
* This is the public API that UI and developers should be calling. All methods
* just forward to AddonManagerInternal.
* @class
*/
var AddonManager = {