1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set sts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
9 * This module contains code for managing APIs that need to run in the
10 * parent process, and handles the parent side of operations that need
11 * to be proxied from ExtensionChild.jsm.
14 /* exported ExtensionParent */
16 var EXPORTED_SYMBOLS = ["ExtensionParent"];
18 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
19 const { XPCOMUtils } = ChromeUtils.import(
20 "resource://gre/modules/XPCOMUtils.jsm"
23 XPCOMUtils.defineLazyModuleGetters(this, {
24 AddonManager: "resource://gre/modules/AddonManager.jsm",
25 AppConstants: "resource://gre/modules/AppConstants.jsm",
26 AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
27 BroadcastConduit: "resource://gre/modules/ConduitsParent.jsm",
28 DeferredTask: "resource://gre/modules/DeferredTask.jsm",
29 DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.jsm",
30 ExtensionData: "resource://gre/modules/Extension.jsm",
31 ExtensionActivityLog: "resource://gre/modules/ExtensionActivityLog.jsm",
32 GeckoViewConnection: "resource://gre/modules/GeckoViewWebExtension.jsm",
33 MessageManagerProxy: "resource://gre/modules/MessageManagerProxy.jsm",
34 NativeApp: "resource://gre/modules/NativeMessaging.jsm",
35 PerformanceCounters: "resource://gre/modules/PerformanceCounters.jsm",
36 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
37 Schemas: "resource://gre/modules/Schemas.jsm",
40 XPCOMUtils.defineLazyServiceGetters(this, {
42 "@mozilla.org/addons/addon-manager-startup;1",
43 "amIAddonManagerStartup",
47 // We're using the pref to avoid loading PerformanceCounters.jsm for nothing.
48 XPCOMUtils.defineLazyPreferenceGetter(
51 "extensions.webextensions.enablePerformanceCounters",
54 const { ExtensionCommon } = ChromeUtils.import(
55 "resource://gre/modules/ExtensionCommon.jsm"
57 const { ExtensionUtils } = ChromeUtils.import(
58 "resource://gre/modules/ExtensionUtils.jsm"
73 promiseDocumentLoaded,
78 const ERROR_NO_RECEIVERS =
79 "Could not establish connection. Receiving end does not exist.";
81 const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
82 const CATEGORY_EXTENSION_MODULES = "webextension-modules";
83 const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
84 const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
86 let schemaURLs = new Set();
88 schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
96 // This object loads the ext-*.js scripts that define the extension API.
97 let apiManager = new (class extends SchemaAPIManager {
99 super("main", Schemas);
100 this.initialized = null;
102 /* eslint-disable mozilla/balanced-listeners */
103 this.on("startup", (e, extension) => {
104 return extension.apiManager.onStartup(extension);
107 this.on("update", async (e, { id, resourceURI }) => {
108 let modules = this.eventModules.get("update");
109 if (modules.size == 0) {
113 let extension = new ExtensionData(resourceURI);
114 await extension.loadManifest();
117 Array.from(modules).map(async apiName => {
118 let module = await this.asyncLoadModule(apiName);
119 module.onUpdate(id, extension.manifest);
124 this.on("uninstall", (e, { id }) => {
125 let modules = this.eventModules.get("uninstall");
127 Array.from(modules).map(async apiName => {
128 let module = await this.asyncLoadModule(apiName);
129 return module.onUninstall(id);
133 /* eslint-enable mozilla/balanced-listeners */
135 // Handle any changes that happened during startup
136 let disabledIds = AddonManager.getStartupChanges(
137 AddonManager.STARTUP_CHANGE_DISABLED
139 if (disabledIds.length) {
140 this._callHandlers(disabledIds, "disable", "onDisable");
143 let uninstalledIds = AddonManager.getStartupChanges(
144 AddonManager.STARTUP_CHANGE_UNINSTALLED
146 if (uninstalledIds.length) {
147 this._callHandlers(uninstalledIds, "uninstall", "onUninstall");
151 getModuleJSONURLs() {
153 Services.catMan.enumerateCategory(CATEGORY_EXTENSION_MODULES),
158 // Loads all the ext-*.js scripts currently registered.
160 if (this.initialized) {
161 return this.initialized;
164 let modulesPromise = StartupCache.other.get(["parentModules"], () =>
165 this.loadModuleJSON(this.getModuleJSONURLs())
169 for (let { value } of Services.catMan.enumerateCategory(
170 CATEGORY_EXTENSION_SCRIPTS
172 scriptURLs.push(value);
175 let promise = (async () => {
176 let scripts = await Promise.all(
177 scriptURLs.map(url => ChromeUtils.compileScript(url))
180 this.initModuleData(await modulesPromise);
183 for (let script of scripts) {
184 script.executeInGlobal(this.global);
187 // Load order matters here. The base manifest defines types which are
188 // extended by other schemas, so needs to be loaded first.
189 return Schemas.load(BASE_SCHEMA).then(() => {
191 for (let { value } of Services.catMan.enumerateCategory(
192 CATEGORY_EXTENSION_SCHEMAS
194 promises.push(Schemas.load(value));
196 for (let [url, { content }] of this.schemaURLs) {
197 promises.push(Schemas.load(url, content));
199 for (let url of schemaURLs) {
200 promises.push(Schemas.load(url));
202 return Promise.all(promises).then(() => {
203 Schemas.updateSharedSchemas();
208 /* eslint-disable mozilla/balanced-listeners */
209 Services.mm.addMessageListener("Extension:GetTabAndWindowId", this);
210 /* eslint-enable mozilla/balanced-listeners */
212 this.initialized = promise;
213 return this.initialized;
216 receiveMessage({ name, target, sync }) {
217 if (name === "Extension:GetTabAndWindowId") {
218 let result = this.global.tabTracker.getBrowserData(target);
224 target.messageManager.sendAsyncMessage(
225 "Extension:SetFrameData",
232 // Call static handlers for the given event on the given extension ids,
233 // and set up a shutdown blocker to ensure they all complete.
234 _callHandlers(ids, event, method) {
235 let promises = Array.from(this.eventModules.get(event))
236 .map(async modName => {
237 let module = await this.asyncLoadModule(modName);
238 return ids.map(id => module[method](id));
241 if (event === "disable") {
242 promises.push(...ids.map(id => this.emit("disable", id)));
244 if (event === "enabling") {
245 promises.push(...ids.map(id => this.emit("enabling", id)));
248 AsyncShutdown.profileBeforeChange.addBlocker(
249 `Extension API ${event} handlers for ${ids.join(",")}`,
250 Promise.all(promises)
255 // Receives messages related to the extension messaging API and forwards them
256 // to relevant child messengers. Also handles Native messaging and GeckoView.
257 const ProxyMessenger = {
259 * @typedef {object} ParentPort
260 * @prop {function(StructuredCloneHolder)} onPortMessage
261 * @prop {function()} onPortDisconnect
263 /** @type Map<number, ParentPort> */
267 this.conduit = new BroadcastConduit(ProxyMessenger, {
268 id: "ProxyMessenger",
269 reportOnClosed: "portId",
270 recv: ["PortConnect", "PortMessage", "NativeMessage", "RuntimeMessage"],
271 cast: ["PortConnect", "PortMessage", "PortDisconnect", "RuntimeMessage"],
275 openNative(nativeApp, sender) {
276 let context = ParentAPIManager.getContextById(sender.childId);
277 if (context.extension.hasPermission("geckoViewAddons")) {
278 return new GeckoViewConnection(
279 this.getSender(context.extension, sender),
280 sender.actor.browsingContext.top.embedderElement,
282 context.extension.hasPermission("nativeMessagingFromContent")
284 } else if (sender.verified) {
285 return new NativeApp(context, nativeApp);
287 throw new Error(`Native messaging not allowed: ${JSON.stringify(sender)}`);
290 recvNativeMessage({ nativeApp, holder }, { sender }) {
291 return this.openNative(nativeApp, sender).sendMessage(holder);
294 getSender(extension, source) {
296 contextId: source.id,
297 id: source.extensionId,
298 envType: source.envType,
299 frameId: source.frameId,
303 let browser = source.actor.browsingContext.top.embedderElement;
304 let data = browser && apiManager.global.tabTracker.getBrowserData(browser);
305 if (data?.tabId > 0) {
306 sender.tab = extension.tabManager.get(data.tabId, null)?.convert();
312 getTopBrowsingContextId(tabId) {
313 // If a tab alredy has content scripts, no need to check private browsing.
314 let tab = apiManager.global.tabTracker.getTab(tabId, null);
315 if (!tab || (tab.browser || tab).getAttribute("pending") === "true") {
316 // No receivers in discarded tabs, so bail early to keep the browser lazy.
317 throw new ExtensionError(ERROR_NO_RECEIVERS);
319 let browser = tab.linkedBrowser || tab.browser;
320 return browser.browsingContext.id;
323 // TODO: Rework/simplify this and getSender/getTopBC after bug 1580766.
324 async normalizeArgs(arg, sender) {
325 arg.extensionId = arg.extensionId || sender.extensionId;
326 let extension = GlobalManager.extensionMap.get(arg.extensionId);
328 return Promise.reject({ message: ERROR_NO_RECEIVERS });
330 await extension.wakeupBackground?.();
332 arg.sender = this.getSender(extension, sender);
333 arg.topBC = arg.tabId && this.getTopBrowsingContextId(arg.tabId);
334 return arg.tabId ? "tab" : "messenger";
337 async recvRuntimeMessage(arg, { sender }) {
338 arg.firstResponse = true;
339 let kind = await this.normalizeArgs(arg, sender);
340 let result = await this.conduit.castRuntimeMessage(kind, arg);
342 // "throw new ExtensionError" cannot be used because then the stack of the
343 // sendMessage call would not be added to the error object generated by
344 // context.normalizeError. Test coverage by test_ext_error_location.js.
345 return Promise.reject({ message: ERROR_NO_RECEIVERS });
350 async recvPortConnect(arg, { sender }) {
352 let port = this.openNative(arg.name, sender).onConnect(arg.portId, this);
353 this.ports.set(arg.portId, port);
357 // PortMessages that follow will need to wait for the port to be opened.
359 this.ports.set(arg.portId, new Promise(res => (resolvePort = res)));
361 let kind = await this.normalizeArgs(arg, sender);
362 let all = await this.conduit.castPortConnect(kind, arg);
365 // If there are no active onConnect listeners.
366 if (!all.some(x => x.value)) {
367 throw new ExtensionError(ERROR_NO_RECEIVERS);
371 async recvPortMessage({ holder }, { sender }) {
373 return this.ports.get(sender.portId).onPortMessage(holder);
375 await this.ports.get(sender.portId);
376 this.sendPortMessage(sender.portId, holder, !sender.source);
379 recvConduitClosed(sender) {
380 let app = this.ports.get(sender.portId);
381 if (this.ports.delete(sender.portId) && sender.native) {
382 return app.onPortDisconnect();
384 this.sendPortDisconnect(sender.portId, null, !sender.source);
387 sendPortMessage(portId, holder, source = true) {
388 this.conduit.castPortMessage("port", { portId, source, holder });
391 sendPortDisconnect(portId, error, source = true) {
392 this.conduit.castPortDisconnect("port", { portId, source, error });
393 this.ports.delete(portId);
396 ProxyMessenger.init();
398 // Responsible for loading extension APIs into the right globals.
400 // Map[extension ID -> Extension]. Determines which extension is
401 // responsible for content under a particular extension ID.
402 extensionMap: new Map(),
406 if (this.extensionMap.size == 0) {
407 apiManager.on("extension-browser-inserted", this._onExtensionBrowser);
408 this.initialized = true;
409 Services.ppmm.addMessageListener(
410 "Extension:SendPerformanceCounter",
414 this.extensionMap.set(extension.id, extension);
418 this.extensionMap.delete(extension.id);
420 if (this.extensionMap.size == 0 && this.initialized) {
421 apiManager.off("extension-browser-inserted", this._onExtensionBrowser);
422 this.initialized = false;
423 Services.ppmm.removeMessageListener(
424 "Extension:SendPerformanceCounter",
430 async receiveMessage({ name, data }) {
432 case "Extension:SendPerformanceCounter":
433 PerformanceCounters.merge(data.counters);
438 _onExtensionBrowser(type, browser, additionalData = {}) {
439 browser.messageManager.loadFrameScript(
440 "resource://gre/modules/onExtensionBrowser.js",
445 let viewType = browser.getAttribute("webextension-view-type");
447 let data = { viewType };
449 let { tabTracker } = apiManager.global;
450 Object.assign(data, tabTracker.getBrowserData(browser), additionalData);
452 browser.messageManager.sendAsyncMessage("Extension:SetFrameData", data);
456 getExtension(extensionId) {
457 return this.extensionMap.get(extensionId);
462 * The proxied parent side of a context in ExtensionChild.jsm, for the
463 * parent side of a proxied API.
465 class ProxyContextParent extends BaseContext {
466 constructor(envType, extension, params, xulBrowser, principal) {
467 super(envType, extension);
469 this.uri = Services.io.newURI(params.url);
471 this.incognito = params.incognito;
473 this.listenerPromises = new Set();
475 // This message manager is used by ParentAPIManager to send messages and to
476 // close the ProxyContext if the underlying message manager closes. This
477 // message manager object may change when `xulBrowser` swaps docshells, e.g.
478 // when a tab is moved to a different window.
479 this.messageManagerProxy = new MessageManagerProxy(xulBrowser);
481 Object.defineProperty(this, "principal", {
487 this.listenerProxies = new Map();
489 this.pendingEventBrowser = null;
491 apiManager.emit("proxy-context-load", this);
494 async withPendingBrowser(browser, callable) {
495 let savedBrowser = this.pendingEventBrowser;
496 this.pendingEventBrowser = browser;
498 let result = await callable();
501 this.pendingEventBrowser = savedBrowser;
505 logActivity(type, name, data) {
506 // The base class will throw so we catch any subclasses that do not implement.
507 // We do not want to throw here, but we also do not log here.
514 applySafe(callback, args) {
515 // There's no need to clone when calling listeners for a proxied
517 return this.applySafeWithoutClone(callback, args);
521 return this.messageManagerProxy.eventTarget;
524 get parentMessageManager() {
525 return this.messageManagerProxy.messageManager;
536 this.messageManagerProxy.dispose();
538 apiManager.emit("proxy-context-unload", this);
542 defineLazyGetter(ProxyContextParent.prototype, "apiCan", function() {
544 let can = new CanOfAPIs(this, this.extension.apiManager, obj);
548 defineLazyGetter(ProxyContextParent.prototype, "apiObj", function() {
549 return this.apiCan.root;
552 defineLazyGetter(ProxyContextParent.prototype, "sandbox", function() {
553 // NOTE: the required Blob and URL globals are used in the ext-registerContentScript.js
554 // API module to convert JS and CSS data into blob URLs.
555 return Cu.Sandbox(this.principal, {
556 sandboxName: this.uri.spec,
557 wantGlobalProperties: ["Blob", "URL"],
562 * The parent side of proxied API context for extension content script
563 * running in ExtensionContent.jsm.
565 class ContentScriptContextParent extends ProxyContextParent {}
568 * The parent side of proxied API context for extension page, such as a
569 * background script, a tab page, or a popup, running in
570 * ExtensionChild.jsm.
572 class ExtensionPageContextParent extends ProxyContextParent {
573 constructor(envType, extension, params, xulBrowser) {
574 super(envType, extension, params, xulBrowser, extension.principal);
576 this.viewType = params.viewType;
578 this.extension.views.add(this);
580 extension.emit("extension-proxy-context-load", this);
583 // The window that contains this context. This may change due to moving tabs.
585 let win = this.xulBrowser.ownerGlobal;
586 return win.browsingContext.topChromeWindow;
589 get currentWindow() {
590 if (this.viewType !== "background") {
591 return this.appWindow;
596 let { tabTracker } = apiManager.global;
597 let data = tabTracker.getBrowserData(this.xulBrowser);
598 if (data.tabId >= 0) {
603 onBrowserChange(browser) {
604 super.onBrowserChange(browser);
605 this.xulBrowser = browser;
610 this.extension.views.delete(this);
614 apiManager.emit("page-shutdown", this);
620 * The parent side of proxied API context for devtools extension page, such as a
621 * devtools pages and panels running in ExtensionChild.jsm.
623 class DevToolsExtensionPageContextParent extends ExtensionPageContextParent {
624 constructor(...params) {
627 // Set all attributes that are lazily defined to `null` here.
629 // Note that we can't do that for `this._devToolsToolbox` because it will
630 // be defined when calling our parent constructor and so would override it back to `null`.
631 this._devToolsCommands = null;
632 this._onNavigatedListeners = null;
634 this._onResourceAvailable = this._onResourceAvailable.bind(this);
637 set devToolsToolbox(toolbox) {
638 if (this._devToolsToolbox) {
639 throw new Error("Cannot set the context DevTools toolbox twice");
642 this._devToolsToolbox = toolbox;
645 get devToolsToolbox() {
646 return this._devToolsToolbox;
649 async addOnNavigatedListener(listener) {
650 if (!this._onNavigatedListeners) {
651 this._onNavigatedListeners = new Set();
653 await this.devToolsToolbox.resourceCommand.watchResources(
654 [this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
656 onAvailable: this._onResourceAvailable,
657 ignoreExistingResources: true,
662 this._onNavigatedListeners.add(listener);
665 removeOnNavigatedListener(listener) {
666 if (this._onNavigatedListeners) {
667 this._onNavigatedListeners.delete(listener);
672 * The returned "commands" object, exposing modules implemented from devtools/shared/commands.
673 * Each attribute being a static interface to communicate with the server backend.
675 * @returns {Promise<Object>}
677 async getDevToolsCommands() {
678 // Ensure that we try to instantiate a commands only once,
679 // even if createCommandsForTabForWebExtension is async.
680 if (this._devToolsCommandsPromise) {
681 return this._devToolsCommandsPromise;
683 if (this._devToolsCommands) {
684 return this._devToolsCommands;
687 this._devToolsCommandsPromise = (async () => {
688 const commands = await DevToolsShim.createCommandsForTabForWebExtension(
689 this.devToolsToolbox.descriptorFront.localTab
691 await commands.targetCommand.startListening();
692 this._devToolsCommands = commands;
693 this._devToolsCommandsPromise = null;
696 return this._devToolsCommandsPromise;
700 // Bail if the toolbox reference was already cleared.
701 if (!this.devToolsToolbox) {
705 if (this._onNavigatedListeners) {
706 this.devToolsToolbox.resourceCommand.unwatchResources(
707 [this.devToolsToolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
708 { onAvailable: this._onResourceAvailable }
712 if (this._devToolsCommands) {
713 this._devToolsCommands.destroy();
714 this._devToolsCommands = null;
717 if (this._onNavigatedListeners) {
718 this._onNavigatedListeners.clear();
719 this._onNavigatedListeners = null;
722 this._devToolsToolbox = null;
727 async _onResourceAvailable(resources) {
728 for (const resource of resources) {
729 const { targetFront } = resource;
730 if (targetFront.isTopLevel && resource.name === "dom-complete") {
731 const url = targetFront.localTab.linkedBrowser.currentURI.spec;
732 for (const listener of this._onNavigatedListeners) {
741 proxyContexts: new Map(),
744 // TODO: Bug 1595186 - remove/replace all usage of MessageManager below.
745 Services.obs.addObserver(this, "message-manager-close");
747 this.conduit = new BroadcastConduit(this, {
748 id: "ParentAPIManager",
749 reportOnClosed: "childId",
750 recv: ["CreateProxyContext", "APICall", "AddListener", "RemoveListener"],
751 send: ["CallResult"],
752 query: ["RunListener"],
756 attachMessageManager(extension, processMessageManager) {
757 extension.parentMessageManager = processMessageManager;
760 async observe(subject, topic, data) {
761 if (topic === "message-manager-close") {
763 for (let [childId, context] of this.proxyContexts) {
764 if (context.parentMessageManager === mm) {
765 this.closeProxyContext(childId);
769 // Reset extension message managers when their child processes shut down.
770 for (let extension of GlobalManager.extensionMap.values()) {
771 if (extension.parentMessageManager === mm) {
772 extension.parentMessageManager = null;
778 shutdownExtension(extensionId, reason) {
779 if (["ADDON_DISABLE", "ADDON_UNINSTALL"].includes(reason)) {
780 apiManager._callHandlers([extensionId], "disable", "onDisable");
783 for (let [childId, context] of this.proxyContexts) {
784 if (context.extension.id == extensionId) {
786 this.proxyContexts.delete(childId);
791 recvCreateProxyContext(data, { actor, sender }) {
792 let { envType, extensionId, childId, principal } = data;
793 let target = actor.browsingContext.top.embedderElement;
795 if (this.proxyContexts.has(childId)) {
797 "A WebExtension context with the given ID already exists!"
801 let extension = GlobalManager.getExtension(extensionId);
803 throw new Error(`No WebExtension found with ID ${extensionId}`);
807 if (envType == "addon_parent" || envType == "devtools_parent") {
808 if (!sender.verified) {
809 throw new Error(`Bad sender context envType: ${sender.envType}`);
812 let processMessageManager =
813 target.messageManager.processMessageManager ||
814 Services.ppmm.getChildAt(0);
816 if (!extension.parentMessageManager) {
817 if (target.remoteType === extension.remoteType) {
818 this.attachMessageManager(extension, processMessageManager);
822 if (processMessageManager !== extension.parentMessageManager) {
824 "Attempt to create privileged extension parent from incorrect child process"
828 if (envType == "addon_parent") {
829 context = new ExtensionPageContextParent(
835 } else if (envType == "devtools_parent") {
836 context = new DevToolsExtensionPageContextParent(
843 } else if (envType == "content_parent") {
844 context = new ContentScriptContextParent(
852 throw new Error(`Invalid WebExtension context envType: ${envType}`);
854 this.proxyContexts.set(childId, context);
857 recvConduitClosed(sender) {
858 this.closeProxyContext(sender.id);
861 closeProxyContext(childId) {
862 let context = this.proxyContexts.get(childId);
865 this.proxyContexts.delete(childId);
869 async retrievePerformanceCounters() {
870 // getting the parent counters
871 return PerformanceCounters.getData();
875 * Call the given function and also log the call as appropriate
876 * (i.e., with PerformanceCounters and/or activity logging)
878 * @param {BaseContext} context The context making this call.
879 * @param {object} data Additional data about the call.
880 * @param {function} callable The actual implementation to invoke.
882 async callAndLog(context, data, callable) {
883 let { id } = context.extension;
884 // If we were called via callParentAsyncFunction we don't want
885 // to log again, check for the flag.
886 const { alreadyLogged } = data.options || {};
887 if (!alreadyLogged) {
888 ExtensionActivityLog.log(id, context.viewType, "api_call", data.path, {
893 let start = Cu.now();
897 ChromeUtils.addProfilerMarker(
899 { startTime: start },
900 `${id}, api_call: ${data.path}`
902 if (gTimingEnabled) {
903 let end = Cu.now() * 1000;
904 PerformanceCounters.storeExecutionTime(
913 async recvAPICall(data, { actor }) {
914 let context = this.getContextById(data.childId);
915 let target = actor.browsingContext.top.embedderElement;
916 if (context.parentMessageManager !== target.messageManager) {
917 throw new Error("Got message on unexpected message manager");
920 let reply = result => {
921 if (!context.parentMessageManager) {
922 Services.console.logStringMessage(
923 "Cannot send function call result: other side closed connection " +
924 `(call data: ${uneval({ path: data.path, args: data.args })})`
929 this.conduit.sendCallResult(data.childId, {
930 childId: data.childId,
938 let args = data.args;
939 let pendingBrowser = context.pendingEventBrowser;
940 let fun = await context.apiCan.asyncFindAPIPath(data.path);
941 let result = this.callAndLog(context, data, () => {
942 return context.withPendingBrowser(pendingBrowser, () => fun(...args));
946 result = result || Promise.resolve();
950 result = result instanceof SpreadArgs ? [...result] : [result];
952 let holder = new StructuredCloneHolder(result);
954 reply({ result: holder });
957 error = context.normalizeError(error);
959 error: { message: error.message, fileName: error.fileName },
966 let error = context.normalizeError(e);
967 reply({ error: { message: error.message } });
974 async recvAddListener(data, { actor }) {
975 let context = this.getContextById(data.childId);
976 let target = actor.browsingContext.top.embedderElement;
977 if (context.parentMessageManager !== target.messageManager) {
978 throw new Error("Got message on unexpected message manager");
981 let { childId, alreadyLogged = false } = data;
982 let handlingUserInput = false;
984 let listener = async (...listenerArgs) => {
985 let startTime = Cu.now();
986 // Extract urgentSend flag to avoid deserializing args holder later.
987 let urgentSend = false;
988 if (listenerArgs[0] && data.path.startsWith("webRequest.")) {
989 urgentSend = listenerArgs[0].urgentSend;
990 delete listenerArgs[0].urgentSend;
992 let result = await this.conduit.queryRunListener(childId, {
995 listenerId: data.listenerId,
999 return new StructuredCloneHolder(listenerArgs);
1002 let rv = result && result.deserialize(global);
1003 ChromeUtils.addProfilerMarker(
1006 `${context.extension.id}, api_event: ${data.path}`
1008 ExtensionActivityLog.log(
1009 context.extension.id,
1013 { args: listenerArgs, result: rv }
1018 context.listenerProxies.set(data.listenerId, listener);
1020 let args = data.args;
1021 let promise = context.apiCan.asyncFindAPIPath(data.path);
1023 // Store pending listener additions so we can be sure they're all
1024 // fully initialize before we consider extension startup complete.
1025 if (context.viewType === "background" && context.listenerPromises) {
1026 const { listenerPromises } = context;
1027 listenerPromises.add(promise);
1028 let remove = () => {
1029 listenerPromises.delete(promise);
1031 promise.then(remove, remove);
1034 let handler = await promise;
1035 if (handler.setUserInput) {
1036 handlingUserInput = true;
1038 handler.addListener(listener, ...args);
1039 if (!alreadyLogged) {
1040 ExtensionActivityLog.log(
1041 context.extension.id,
1044 `${data.path}.addListener`,
1050 async recvRemoveListener(data) {
1051 let context = this.getContextById(data.childId);
1052 let listener = context.listenerProxies.get(data.listenerId);
1054 let handler = await context.apiCan.asyncFindAPIPath(data.path);
1055 handler.removeListener(listener);
1057 let { alreadyLogged = false } = data;
1058 if (!alreadyLogged) {
1059 ExtensionActivityLog.log(
1060 context.extension.id,
1063 `${data.path}.removeListener`,
1069 getContextById(childId) {
1070 let context = this.proxyContexts.get(childId);
1072 throw new Error("WebExtension context not found!");
1078 ParentAPIManager.init();
1081 * A hidden window which contains the extension pages that are not visible
1082 * (i.e., background pages and devtools pages), and is also used by
1083 * ExtensionDebuggingUtils to contain the browser elements used by the
1084 * addon debugger to connect to the devtools actors running in the same
1085 * process of the target extension (and be able to stay connected across
1086 * the addon reloads).
1088 class HiddenXULWindow {
1090 this._windowlessBrowser = null;
1091 this.unloaded = false;
1092 this.waitInitialized = this.initWindowlessBrowser();
1096 if (this.unloaded) {
1098 "Unable to shutdown an unloaded HiddenXULWindow instance"
1102 this.unloaded = true;
1104 this.waitInitialized = null;
1106 if (!this._windowlessBrowser) {
1107 Cu.reportError("HiddenXULWindow was shut down while it was loading.");
1108 // initWindowlessBrowser will close windowlessBrowser when possible.
1112 this._windowlessBrowser.close();
1113 this._windowlessBrowser = null;
1116 get chromeDocument() {
1117 return this._windowlessBrowser.document;
1121 * Private helper that create a HTMLDocument in a windowless browser.
1123 * @returns {Promise<void>}
1124 * A promise which resolves when the windowless browser is ready.
1126 async initWindowlessBrowser() {
1127 if (this.waitInitialized) {
1128 throw new Error("HiddenXULWindow already initialized");
1131 // The invisible page is currently wrapped in a XUL window to fix an issue
1132 // with using the canvas API from a background page (See Bug 1274775).
1133 let windowlessBrowser = Services.appShell.createWindowlessBrowser(true);
1135 // The windowless browser is a thin wrapper around a docShell that keeps
1136 // its related resources alive. It implements nsIWebNavigation and
1137 // forwards its methods to the underlying docShell. That .docShell
1138 // needs `QueryInterface(nsIWebNavigation)` to give us access to the
1139 // webNav methods that are already available on the windowless browser.
1140 let chromeShell = windowlessBrowser.docShell;
1141 chromeShell.QueryInterface(Ci.nsIWebNavigation);
1143 if (PrivateBrowsingUtils.permanentPrivateBrowsing) {
1144 let attrs = chromeShell.getOriginAttributes();
1145 attrs.privateBrowsingId = 1;
1146 chromeShell.setOriginAttributes(attrs);
1149 windowlessBrowser.browsingContext.useGlobalHistory = false;
1150 chromeShell.loadURI("chrome://extensions/content/dummy.xhtml", {
1151 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
1154 await promiseObserved(
1155 "chrome-document-global-created",
1156 win => win.document == chromeShell.document
1158 await promiseDocumentLoaded(windowlessBrowser.document);
1159 if (this.unloaded) {
1160 windowlessBrowser.close();
1163 this._windowlessBrowser = windowlessBrowser;
1167 * Creates the browser XUL element that will contain the WebExtension Page.
1169 * @param {Object} xulAttributes
1170 * An object that contains the xul attributes to set of the newly
1171 * created browser XUL element.
1173 * @returns {Promise<XULElement>}
1174 * A Promise which resolves to the newly created browser XUL element.
1176 async createBrowserElement(xulAttributes) {
1177 if (!xulAttributes || Object.keys(xulAttributes).length === 0) {
1178 throw new Error("missing mandatory xulAttributes parameter");
1181 await this.waitInitialized;
1183 const chromeDoc = this.chromeDocument;
1185 const browser = chromeDoc.createXULElement("browser");
1186 browser.setAttribute("type", "content");
1187 browser.setAttribute("disableglobalhistory", "true");
1188 browser.setAttribute("messagemanagergroup", "webext-browsers");
1190 for (const [name, value] of Object.entries(xulAttributes)) {
1191 if (value != null) {
1192 browser.setAttribute(name, value);
1196 let awaitFrameLoader = Promise.resolve();
1198 if (browser.getAttribute("remote") === "true") {
1199 awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
1202 chromeDoc.documentElement.appendChild(browser);
1204 // Forcibly flush layout so that we get a pres shell soon enough, see
1206 browser.getBoundingClientRect();
1208 await awaitFrameLoader;
1213 const SharedWindow = {
1218 if (this._window == null) {
1219 if (this._count != 0) {
1221 `Shared window already exists with count ${this._count}`
1225 this._window = new HiddenXULWindow();
1229 return this._window;
1233 if (this._count < 1) {
1234 throw new Error(`Releasing shared window with count ${this._count}`);
1238 if (this._count == 0) {
1239 this._window.shutdown();
1240 this._window = null;
1246 * This is a base class used by the ext-backgroundPage and ext-devtools API implementations
1247 * to inherits the shared boilerplate code needed to create a parent document for the hidden
1248 * extension pages (e.g. the background page, the devtools page) in the BackgroundPage and
1249 * DevToolsPage classes.
1251 * @param {Extension} extension
1252 * The Extension which owns the hidden extension page created (used to decide
1253 * if the hidden extension page parent doc is going to be a windowlessBrowser or
1254 * a visible XUL window).
1255 * @param {string} viewType
1256 * The viewType of the WebExtension page that is going to be loaded
1257 * in the created browser element (e.g. "background" or "devtools_page").
1259 class HiddenExtensionPage {
1260 constructor(extension, viewType) {
1261 if (!extension || !viewType) {
1262 throw new Error("extension and viewType parameters are mandatory");
1265 this.extension = extension;
1266 this.viewType = viewType;
1267 this.browser = null;
1268 this.unloaded = false;
1272 * Destroy the created parent document.
1275 if (this.unloaded) {
1277 "Unable to shutdown an unloaded HiddenExtensionPage instance"
1281 this.unloaded = true;
1284 this._releaseBrowser();
1289 this.browser.remove();
1290 this.browser = null;
1291 SharedWindow.release();
1295 * Creates the browser XUL element that will contain the WebExtension Page.
1297 * @returns {Promise<XULElement>}
1298 * A Promise which resolves to the newly created browser XUL element.
1300 async createBrowserElement() {
1302 throw new Error("createBrowserElement called twice");
1305 let window = SharedWindow.acquire();
1307 this.browser = await window.createBrowserElement({
1308 "webextension-view-type": this.viewType,
1309 remote: this.extension.remote ? "true" : null,
1310 remoteType: this.extension.remoteType,
1311 initialBrowsingContextGroupId: this.extension.browsingContextGroupId,
1314 SharedWindow.release();
1318 if (this.unloaded) {
1319 this._releaseBrowser();
1320 throw new Error("Extension shut down before browser element was created");
1323 return this.browser;
1328 * This object provides utility functions needed by the devtools actors to
1329 * be able to connect and debug an extension (which can run in the main or in
1330 * a child extension process).
1332 const DebugUtils = {
1333 // A lazily created hidden XUL window, which contains the browser elements
1334 // which are used to connect the webextension patent actor to the extension process.
1335 hiddenXULWindow: null,
1337 // Map<extensionId, Promise<XULElement>>
1338 debugBrowserPromises: new Map(),
1339 // DefaultWeakMap<Promise<browser XULElement>, Set<WebExtensionParentActor>>
1340 debugActors: new DefaultWeakMap(() => new Set()),
1342 _extensionUpdatedWatcher: null,
1343 watchExtensionUpdated() {
1344 if (!this._extensionUpdatedWatcher) {
1345 // Watch the updated extension objects.
1346 this._extensionUpdatedWatcher = async (evt, extension) => {
1347 const browserPromise = this.debugBrowserPromises.get(extension.id);
1348 if (browserPromise) {
1349 const browser = await browserPromise;
1351 browser.isRemoteBrowser !== extension.remote &&
1352 this.debugBrowserPromises.get(extension.id) === browserPromise
1354 // If the cached browser element is not anymore of the same
1355 // remote type of the extension, remove it.
1356 this.debugBrowserPromises.delete(extension.id);
1362 apiManager.on("ready", this._extensionUpdatedWatcher);
1366 unwatchExtensionUpdated() {
1367 if (this._extensionUpdatedWatcher) {
1368 apiManager.off("ready", this._extensionUpdatedWatcher);
1369 delete this._extensionUpdatedWatcher;
1373 getExtensionManifestWarnings(id) {
1374 const addon = GlobalManager.extensionMap.get(id);
1376 return addon.warnings;
1382 * Retrieve a XUL browser element which has been configured to be able to connect
1383 * the devtools actor with the process where the extension is running.
1385 * @param {WebExtensionParentActor} webExtensionParentActor
1386 * The devtools actor that is retrieving the browser element.
1388 * @returns {Promise<XULElement>}
1389 * A promise which resolves to the configured browser XUL element.
1391 async getExtensionProcessBrowser(webExtensionParentActor) {
1392 const extensionId = webExtensionParentActor.addonId;
1393 const extension = GlobalManager.getExtension(extensionId);
1395 throw new Error(`Extension not found: ${extensionId}`);
1398 const createBrowser = () => {
1399 if (!this.hiddenXULWindow) {
1400 this.hiddenXULWindow = new HiddenXULWindow();
1401 this.watchExtensionUpdated();
1404 return this.hiddenXULWindow.createBrowserElement({
1405 "webextension-addon-debug-target": extensionId,
1406 remote: extension.remote ? "true" : null,
1407 remoteType: extension.remoteType,
1408 initialBrowsingContextGroupId: extension.browsingContextGroupId,
1412 let browserPromise = this.debugBrowserPromises.get(extensionId);
1414 // Create a new promise if there is no cached one in the map.
1415 if (!browserPromise) {
1416 browserPromise = createBrowser();
1417 this.debugBrowserPromises.set(extensionId, browserPromise);
1418 browserPromise.then(browser => {
1419 browserPromise.browser = browser;
1421 browserPromise.catch(e => {
1423 this.debugBrowserPromises.delete(extensionId);
1427 this.debugActors.get(browserPromise).add(webExtensionParentActor);
1429 return browserPromise;
1432 getFrameLoader(extensionId) {
1433 let promise = this.debugBrowserPromises.get(extensionId);
1434 return promise && promise.browser && promise.browser.frameLoader;
1438 * Given the devtools actor that has retrieved an addon debug browser element,
1439 * it destroys the XUL browser element, and it also destroy the hidden XUL window
1440 * if it is not currently needed.
1442 * @param {WebExtensionParentActor} webExtensionParentActor
1443 * The devtools actor that has retrieved an addon debug browser element.
1445 async releaseExtensionProcessBrowser(webExtensionParentActor) {
1446 const extensionId = webExtensionParentActor.addonId;
1447 const browserPromise = this.debugBrowserPromises.get(extensionId);
1449 if (browserPromise) {
1450 const actorsSet = this.debugActors.get(browserPromise);
1451 actorsSet.delete(webExtensionParentActor);
1452 if (actorsSet.size === 0) {
1453 this.debugActors.delete(browserPromise);
1454 this.debugBrowserPromises.delete(extensionId);
1455 await browserPromise.then(browser => browser.remove());
1459 if (this.debugBrowserPromises.size === 0 && this.hiddenXULWindow) {
1460 this.hiddenXULWindow.shutdown();
1461 this.hiddenXULWindow = null;
1462 this.unwatchExtensionUpdated();
1468 * Returns a Promise which resolves with the message data when the given message
1469 * was received by the message manager. The promise is rejected if the message
1470 * manager was closed before a message was received.
1472 * @param {MessageListenerManager} messageManager
1473 * The message manager on which to listen for messages.
1474 * @param {string} messageName
1475 * The message to listen for.
1476 * @returns {Promise<*>}
1478 function promiseMessageFromChild(messageManager, messageName) {
1479 return new Promise((resolve, reject) => {
1481 function listener(message) {
1483 resolve(message.data);
1485 function observer(subject, topic, data) {
1486 if (subject === messageManager) {
1490 `Message manager was disconnected before receiving ${messageName}`
1495 unregister = () => {
1496 Services.obs.removeObserver(observer, "message-manager-close");
1497 messageManager.removeMessageListener(messageName, listener);
1499 messageManager.addMessageListener(messageName, listener);
1500 Services.obs.addObserver(observer, "message-manager-close");
1504 // This should be called before browser.loadURI is invoked.
1505 async function promiseExtensionViewLoaded(browser) {
1506 let { childId } = await promiseMessageFromChild(
1507 browser.messageManager,
1508 "Extension:ExtensionViewLoaded"
1511 return ParentAPIManager.getContextById(childId);
1516 * This helper is used to subscribe a listener (e.g. in the ext-devtools API implementation)
1517 * to be called for every ExtensionProxyContext created for an extension page given
1518 * its related extension, viewType and browser element (both the top level context and any context
1519 * created for the extension urls running into its iframe descendants).
1521 * @param {object} params.extension
1522 * The Extension on which we are going to listen for the newly created ExtensionProxyContext.
1523 * @param {string} params.viewType
1524 * The viewType of the WebExtension page that we are watching (e.g. "background" or
1526 * @param {XULElement} params.browser
1527 * The browser element of the WebExtension page that we are watching.
1528 * @param {function} onExtensionProxyContextLoaded
1529 * The callback that is called when a new context has been loaded (as `callback(context)`);
1531 * @returns {function}
1532 * Unsubscribe the listener.
1534 function watchExtensionProxyContextLoad(
1535 { extension, viewType, browser },
1536 onExtensionProxyContextLoaded
1538 if (typeof onExtensionProxyContextLoaded !== "function") {
1539 throw new Error("Missing onExtensionProxyContextLoaded handler");
1542 const listener = (event, context) => {
1543 if (context.viewType == viewType && context.xulBrowser == browser) {
1544 onExtensionProxyContextLoaded(context);
1548 extension.on("extension-proxy-context-load", listener);
1551 extension.off("extension-proxy-context-load", listener);
1555 // Manages icon details for toolbar buttons in the |pageAction| and
1556 // |browserAction| APIs.
1558 DEFAULT_ICON: "chrome://mozapps/skin/extensions/extensionGeneric.svg",
1560 // WeakMap<Extension -> Map<url-string -> Map<iconType-string -> object>>>
1561 iconCache: new DefaultWeakMap(() => {
1562 return new DefaultMap(() => new DefaultMap(() => new Map()));
1565 // Normalizes the various acceptable input formats into an object
1566 // with icon size as key and icon URL as value.
1568 // If a context is specified (function is called from an extension):
1569 // Throws an error if an invalid icon size was provided or the
1570 // extension is not allowed to load the specified resources.
1572 // If no context is specified, instead of throwing an error, this
1573 // function simply logs a warning message.
1574 normalize(details, extension, context = null) {
1575 if (!details.imageData && details.path != null) {
1576 // Pick a cache key for the icon paths. If the path is a string,
1577 // use it directly. Otherwise, stringify the path object.
1578 let key = details.path;
1579 if (typeof key !== "string") {
1583 let icons = this.iconCache
1585 .get(context && context.uri.spec)
1586 .get(details.iconType);
1588 let icon = icons.get(key);
1590 icon = this._normalize(details, extension, context);
1591 icons.set(key, icon);
1596 return this._normalize(details, extension, context);
1599 _normalize(details, extension, context = null) {
1603 let { imageData, path, themeIcons } = details;
1606 if (typeof imageData == "string") {
1607 imageData = { "19": imageData };
1610 for (let size of Object.keys(imageData)) {
1611 result[size] = imageData[size];
1615 let baseURI = context ? context.uri : extension.baseURI;
1618 if (typeof path != "object") {
1619 path = { "19": path };
1622 for (let size of Object.keys(path)) {
1623 let url = path[size];
1625 url = baseURI.resolve(path[size]);
1627 // The Chrome documentation specifies these parameters as
1628 // relative paths. We currently accept absolute URLs as well,
1629 // which means we need to check that the extension is allowed
1630 // to load them. This will throw an error if it's not allowed.
1631 this._checkURL(url, extension);
1633 result[size] = url || this.DEFAULT_ICON;
1638 themeIcons.forEach(({ size, light, dark }) => {
1639 let lightURL = baseURI.resolve(light);
1640 let darkURL = baseURI.resolve(dark);
1642 this._checkURL(lightURL, extension);
1643 this._checkURL(darkURL, extension);
1645 let defaultURL = result[size] || result[19]; // always fallback to default first
1647 default: defaultURL || darkURL, // Fallback to the dark url if no default is specified.
1654 // Function is called from extension code, delegate error.
1658 // If there's no context, it's because we're handling this
1659 // as a manifest directive. Log a warning rather than
1660 // raising an error.
1661 extension.manifestError(`Invalid icon data: ${e}`);
1667 // Checks if the extension is allowed to load the given URL with the specified principal.
1668 // This will throw an error if the URL is not allowed.
1669 _checkURL(url, extension) {
1670 if (!extension.checkLoadURL(url, { allowInheritsPrincipal: true })) {
1671 throw new ExtensionError(`Illegal URL ${url}`);
1675 // Returns the appropriate icon URL for the given icons object and the
1676 // screen resolution of the given window.
1677 getPreferredIcon(icons, extension = null, size = 16) {
1678 const DEFAULT = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
1680 let bestSize = null;
1683 } else if (icons[2 * size]) {
1684 bestSize = 2 * size;
1686 let sizes = Object.keys(icons)
1687 .map(key => parseInt(key, 10))
1688 .sort((a, b) => a - b);
1690 bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
1694 return { size: bestSize, icon: icons[bestSize] || DEFAULT };
1697 return { size, icon: DEFAULT };
1700 // These URLs should already be properly escaped, but make doubly sure CSS
1701 // string escape characters are escaped here, since they could lead to a
1704 return url.replace(/[\\\s"]/g, encodeURIComponent);
1708 // A cache to support faster initialization of extensions at browser startup.
1709 // All cached data is removed when the browser is updated.
1710 // Extension-specific data is removed when the add-on is updated.
1712 STORE_NAMES: Object.freeze([
1721 _ensureDirectoryPromise: null,
1724 _ensureDirectory() {
1725 if (this._ensureDirectoryPromise === null) {
1726 this._ensureDirectoryPromise = IOUtils.makeDirectory(
1727 PathUtils.parent(this.file),
1729 ignoreExisting: true,
1730 createAncestors: true,
1735 return this._ensureDirectoryPromise;
1738 // When the application version changes, this file is removed by
1739 // RemoveComponentRegistries in nsAppRunner.cpp.
1740 file: PathUtils.join(
1741 Services.dirsvc.get("ProfLD", Ci.nsIFile).path,
1747 let data = new Uint8Array(aomStartup.encodeBlob(this._data));
1748 await this._ensureDirectoryPromise;
1749 await IOUtils.write(this.file, data, { tmpPath: `${this.file}.tmp` });
1753 this._ensureDirectory();
1755 if (!this._saveTask) {
1756 this._saveTask = new DeferredTask(() => this._saveNow(), 5000);
1758 IOUtils.profileBeforeChange.addBlocker(
1759 "Flush WebExtension StartupCache",
1761 await this._saveTask.finalize();
1762 this._saveTask = null;
1767 return this._saveTask.arm();
1772 let result = new Map();
1774 let { buffer } = await IOUtils.read(this.file);
1776 result = aomStartup.decodeBlob(buffer);
1778 if (!(e instanceof DOMException) || e.name !== "NotFoundError") {
1783 this._data = result;
1788 if (!this._dataPromise) {
1789 this._dataPromise = this._readData();
1791 return this._dataPromise;
1794 clearAddonData(id) {
1795 return Promise.all([
1796 this.general.delete(id),
1797 this.locales.delete(id),
1798 this.manifests.delete(id),
1799 this.permissions.delete(id),
1801 // Ignore the error. It happens when we try to flush the add-on
1802 // data after the AddonManager has flushed the entire startup cache.
1806 observe(subject, topic, data) {
1807 if (topic === "startupcache-invalidate") {
1808 this._data = new Map();
1809 this._dataPromise = Promise.resolve(this._data);
1813 get(extension, path, createFunc) {
1814 return this.general.get(
1815 [extension.id, extension.version, ...path],
1820 delete(extension, path) {
1821 return this.general.delete([extension.id, extension.version, ...path]);
1825 Services.obs.addObserver(StartupCache, "startupcache-invalidate");
1828 constructor(storeName) {
1829 this.storeName = storeName;
1832 async getStore(path = null) {
1833 let data = await StartupCache.dataPromise;
1835 let store = data.get(this.storeName);
1838 data.set(this.storeName, store);
1842 if (Array.isArray(path)) {
1843 for (let elem of path.slice(0, -1)) {
1844 let next = store.get(elem);
1847 store.set(elem, next);
1851 key = path[path.length - 1];
1854 return [store, key];
1857 async get(path, createFunc) {
1858 let [store, key] = await this.getStore(path);
1860 let result = store.get(key);
1862 if (result === undefined) {
1863 result = await createFunc(path);
1864 store.set(key, result);
1865 StartupCache.save();
1871 async set(path, value) {
1872 let [store, key] = await this.getStore(path);
1874 store.set(key, value);
1875 StartupCache.save();
1879 let [store] = await this.getStore();
1881 return new Map(store);
1884 async delete(path) {
1885 let [store, key] = await this.getStore(path);
1887 if (store.delete(key)) {
1888 StartupCache.save();
1893 for (let name of StartupCache.STORE_NAMES) {
1894 StartupCache[name] = new CacheStore(name);
1897 var ExtensionParent = {
1899 HiddenExtensionPage,
1905 promiseExtensionViewLoaded,
1906 watchExtensionProxyContextLoad,
1910 // browserPaintedPromise and browserStartupPromise are promises that
1911 // resolve after the first browser window is painted and after browser
1912 // windows have been restored, respectively. Alternatively,
1913 // browserStartupPromise also resolves from the extensions-late-startup
1914 // notification sent by Firefox Reality on desktop platforms, because it
1915 // doesn't support SessionStore.
1916 // _resetStartupPromises should only be called from outside this file in tests.
1917 ExtensionParent._resetStartupPromises = () => {
1918 ExtensionParent.browserPaintedPromise = promiseObserved(
1919 "browser-delayed-startup-finished"
1921 ExtensionParent.browserStartupPromise = Promise.race([
1922 promiseObserved("sessionstore-windows-restored"),
1923 promiseObserved("extensions-late-startup"),
1926 ExtensionParent._resetStartupPromises();
1928 XPCOMUtils.defineLazyGetter(ExtensionParent, "PlatformInfo", () => {
1929 return Object.freeze({
1931 let os = AppConstants.platform;
1932 if (os == "macosx") {
1938 let abi = Services.appinfo.XPCOMABI;
1939 let [arch] = abi.split("-");
1940 if (arch == "x86") {
1942 } else if (arch == "x86_64") {
1951 * Retreives the browser_style stylesheets needed for extension popups and sidebars.
1952 * @returns {Array<string>} an array of stylesheets needed for the current platform.
1954 XPCOMUtils.defineLazyGetter(ExtensionParent, "extensionStylesheets", () => {
1955 let stylesheets = ["chrome://browser/content/extension.css"];
1957 if (AppConstants.platform === "macosx") {
1958 stylesheets.push("chrome://browser/content/extension-mac.css");