summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/ExtensionWorkerChild.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/extensions/ExtensionWorkerChild.sys.mjs')
-rw-r--r--toolkit/components/extensions/ExtensionWorkerChild.sys.mjs816
1 files changed, 816 insertions, 0 deletions
diff --git a/toolkit/components/extensions/ExtensionWorkerChild.sys.mjs b/toolkit/components/extensions/ExtensionWorkerChild.sys.mjs
new file mode 100644
index 0000000000..d3c37aae54
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionWorkerChild.sys.mjs
@@ -0,0 +1,816 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This file handles extension background service worker logic that runs in the
+ * child process.
+ */
+
+import {
+ ExtensionChild,
+ ExtensionActivityLogChild,
+} from "resource://gre/modules/ExtensionChild.sys.mjs";
+
+import { ExtensionCommon } from "resource://gre/modules/ExtensionCommon.sys.mjs";
+import {
+ ExtensionPageChild,
+ getContextChildManagerGetter,
+} from "resource://gre/modules/ExtensionPageChild.sys.mjs";
+import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
+
+const { BaseContext, defineLazyGetter } = ExtensionCommon;
+
+const {
+ ChildAPIManager,
+ ChildLocalAPIImplementation,
+ MessageEvent,
+ Messenger,
+ Port,
+ ProxyAPIImplementation,
+ SimpleEventAPI,
+} = ExtensionChild;
+
+const { DefaultMap, getUniqueId } = ExtensionUtils;
+
+/**
+ * SimpleEventAPI subclass specialized for the worker port events
+ * used by WorkerMessenger.
+ */
+class WorkerRuntimePortEvent extends SimpleEventAPI {
+ api() {
+ return {
+ ...super.api(),
+ createListenerForAPIRequest: (...args) =>
+ this.createListenerForAPIRequest(...args),
+ };
+ }
+
+ createListenerForAPIRequest(request) {
+ const { eventListener } = request;
+ return function (port, ...args) {
+ return eventListener.callListener(args, {
+ apiObjectType: Ci.mozIExtensionListenerCallOptions.RUNTIME_PORT,
+ apiObjectDescriptor: { portId: port.portId, name: port.name },
+ });
+ };
+ }
+}
+
+/**
+ * SimpleEventAPI subclass specialized for the worker runtime messaging events
+ * used by WorkerMessenger.
+ */
+class WorkerMessageEvent extends MessageEvent {
+ api() {
+ return {
+ ...super.api(),
+ createListenerForAPIRequest: (...args) =>
+ this.createListenerForAPIRequest(...args),
+ };
+ }
+
+ createListenerForAPIRequest(request) {
+ const { eventListener } = request;
+ return function (message, sender) {
+ return eventListener.callListener([message, sender], {
+ eventListenerType:
+ Ci.mozIExtensionListenerCallOptions.CALLBACK_SEND_RESPONSE,
+ });
+ };
+ }
+}
+
+/**
+ * MessageEvent subclass specialized for the worker's port API events
+ * used by WorkerPort.
+ */
+class WorkerPortEvent extends SimpleEventAPI {
+ api() {
+ return {
+ ...super.api(),
+ createListenerForAPIRequest: (...args) =>
+ this.createListenerForAPIRequest(...args),
+ };
+ }
+
+ createListenerForAPIRequest(request) {
+ const { eventListener } = request;
+ switch (this.name) {
+ case "Port.onDisconnect":
+ return function (port) {
+ eventListener.callListener([], {
+ apiObjectType: Ci.mozIExtensionListenerCallOptions.RUNTIME_PORT,
+ apiObjectDescriptor: {
+ portId: port.portId,
+ name: port.name,
+ },
+ });
+ };
+ case "Port.onMessage":
+ return function (message, port) {
+ eventListener.callListener([message], {
+ apiObjectType: Ci.mozIExtensionListenerCallOptions.RUNTIME_PORT,
+ apiObjectDescriptor: {
+ portId: port.portId,
+ name: port.name,
+ },
+ });
+ };
+ }
+ return undefined;
+ }
+}
+
+/**
+ * Port subclass specialized for the workers and used by WorkerMessager.
+ */
+class WorkerPort extends Port {
+ constructor(context, portId, name, native, sender) {
+ const { viewType, contextId } = context;
+ if (viewType !== "background_worker") {
+ throw new Error(
+ `Unexpected viewType "${viewType}" on context ${contextId}`
+ );
+ }
+
+ super(context, portId, name, native, sender);
+ this.portId = portId;
+ }
+
+ initEventManagers() {
+ const { context } = this;
+ this.onMessage = new WorkerPortEvent(context, "Port.onMessage");
+ this.onDisconnect = new WorkerPortEvent(context, "Port.onDisconnect");
+ }
+
+ getAPI() {
+ const api = super.getAPI();
+ // Add the portId to the API object, needed by the WorkerMessenger
+ // to retrieve the port given the apiObjectId part of the
+ // mozIExtensionAPIRequest sent from the ExtensionPort webidl.
+ api.portId = this.portId;
+ return api;
+ }
+}
+
+defineLazyGetter(WorkerPort.prototype, "api", function () {
+ // No need to clone the API object for the worker, because it runs
+ // on a different JSRuntime and doesn't have direct access to this
+ // object.
+ return this.getAPI();
+});
+
+/**
+ * A Messenger subclass specialized for the background service worker.
+ */
+class WorkerMessenger extends Messenger {
+ constructor(context) {
+ const { viewType, contextId } = context;
+ if (viewType !== "background_worker") {
+ throw new Error(
+ `Unexpected viewType "${viewType}" on context ${contextId}`
+ );
+ }
+
+ super(context);
+
+ // Used by WebIDL API requests to get a port instance given the apiObjectId
+ // received in the API request coming from the ExtensionPort instance
+ // returned in the thread where the request was originating from.
+ this.portsById = new Map();
+ this.context.callOnClose(this);
+ }
+
+ initEventManagers() {
+ const { context } = this;
+ this.onConnect = new WorkerRuntimePortEvent(context, "runtime.onConnect");
+ this.onConnectEx = new WorkerRuntimePortEvent(
+ context,
+ "runtime.onConnectExternal"
+ );
+ this.onMessage = new WorkerMessageEvent(this.context, "runtime.onMessage");
+ this.onMessageEx = new WorkerMessageEvent(
+ context,
+ "runtime.onMessageExternal"
+ );
+ }
+
+ close() {
+ this.portsById.clear();
+ }
+
+ getPortById(portId) {
+ return this.portsById.get(portId);
+ }
+
+ connect({ name, native, ...args }) {
+ let portId = getUniqueId();
+ let port = new WorkerPort(this.context, portId, name, !!native);
+ this.conduit
+ .queryPortConnect({ portId, name, native, ...args })
+ .catch(error => port.recvPortDisconnect({ error }));
+ this.portsById.set(`${portId}`, port);
+ // Extension worker calls this method through the WebIDL bindings,
+ // and the Port instance returned by the runtime.connect/connectNative
+ // methods will be an instance of ExtensionPort webidl interface based
+ // on the ExtensionPortDescriptor dictionary returned by this method.
+ return { portId, name };
+ }
+
+ recvPortConnect({ extensionId, portId, name, sender }) {
+ let event = sender.id === extensionId ? this.onConnect : this.onConnectEx;
+ if (this.context.active && event.fires.size) {
+ let port = new WorkerPort(this.context, portId, name, false, sender);
+ this.portsById.set(`${port.portId}`, port);
+ return event.emit(port).length;
+ }
+ }
+}
+
+/**
+ * APIImplementation subclass specialized for handling mozIExtensionAPIRequests
+ * originated from webidl bindings.
+ *
+ * Provides a createListenerForAPIRequest method which is used by
+ * WebIDLChildAPIManager to retrieve an API event specific wrapper
+ * for the mozIExtensionEventListener for the API events that needs
+ * special handling (e.g. runtime.onConnect).
+ *
+ * createListenerForAPIRequest delegates to the API event the creation
+ * of the special event listener wrappers, the EventManager api objects
+ * for the events that needs special wrapper are expected to provide
+ * a method with the same name.
+ */
+class ChildLocalWebIDLAPIImplementation extends ChildLocalAPIImplementation {
+ constructor(pathObj, namespace, name, childApiManager) {
+ super(pathObj, namespace, name, childApiManager);
+ this.childApiManager = childApiManager;
+ }
+
+ createListenerForAPIRequest(request) {
+ return this.pathObj[this.name].createListenerForAPIRequest?.(request);
+ }
+
+ setProperty() {
+ // mozIExtensionAPIRequest doesn't support this requestType at the moment,
+ // setting a pref would just replace the previous value on the wrapper
+ // object living in the owner thread.
+ // To be implemented if we have an actual use case where that is needed.
+ throw new Error("Unexpected call to setProperty");
+ }
+
+ hasListener(listener) {
+ // hasListener is implemented in C++ by ExtensionEventManager, and so
+ // a call to this method is unexpected.
+ throw new Error("Unexpected call to hasListener");
+ }
+}
+
+/**
+ * APIImplementation subclass specialized for handling API requests related
+ * to an API Object type.
+ *
+ * Retrieving the apiObject instance is delegated internally to the
+ * ExtensionAPI subclass that implements the request apiNamespace,
+ * through an optional getAPIObjectForRequest method expected to be
+ * available on the ExtensionAPI class.
+ */
+class ChildWebIDLObjectTypeImplementation extends ChildLocalWebIDLAPIImplementation {
+ constructor(request, childApiManager) {
+ const { apiNamespace, apiName, apiObjectType, apiObjectId } = request;
+ const api = childApiManager.getExtensionAPIInstance(apiNamespace);
+ const pathObj = api.getAPIObjectForRequest?.(
+ childApiManager.context,
+ request
+ );
+ if (!pathObj) {
+ throw new Error(`apiObject instance not found for ${request}`);
+ }
+ super(pathObj, apiNamespace, apiName, childApiManager);
+ this.fullname = `${apiNamespace}.${apiObjectType}(${apiObjectId}).${apiName}`;
+ }
+}
+
+/**
+ * A ChildAPIManager subclass specialized for handling mozIExtensionAPIRequest
+ * originated from the WebIDL bindings.
+ *
+ * Currently used only for the extension contexts related to the background
+ * service worker.
+ */
+class WebIDLChildAPIManager extends ChildAPIManager {
+ constructor(...args) {
+ super(...args);
+ // Map<apiPathToEventString, WeakMap<nsIExtensionEventListener, Function>>
+ //
+ // apiPathToEventString is a string that represents the full API path
+ // related to the event name (e.g. "runtime.onConnect", or "runtime.Port.onMessage")
+ this.eventListenerWrappers = new DefaultMap(() => new WeakMap());
+ }
+
+ getImplementation(namespace, name) {
+ this.apiCan.findAPIPath(`${namespace}.${name}`);
+ let obj = this.apiCan.findAPIPath(namespace);
+
+ if (obj && name in obj) {
+ return new ChildLocalWebIDLAPIImplementation(obj, namespace, name, this);
+ }
+
+ return this.getFallbackImplementation(namespace, name);
+ }
+
+ getImplementationForRequest(request) {
+ const { apiNamespace, apiName, apiObjectType } = request;
+ if (apiObjectType) {
+ return new ChildWebIDLObjectTypeImplementation(request, this);
+ }
+ return this.getImplementation(apiNamespace, apiName);
+ }
+
+ /**
+ * Handles an ExtensionAPIRequest originated by the Extension APIs WebIDL bindings.
+ *
+ * @param {mozIExtensionAPIRequest} request
+ * The object that represents the API request received
+ * (including arguments, an event listener wrapper etc)
+ *
+ * @returns {mozIExtensionAPIRequestResult}
+ * Result for the API request, either a value to be returned
+ * (which has to be a value that can be structure cloned
+ * if the request was originated from the worker thread) or
+ * an error to raise to the extension code.
+ */
+ handleWebIDLAPIRequest(request) {
+ try {
+ const impl = this.getImplementationForRequest(request);
+ let result;
+ this.context.withAPIRequest(request, () => {
+ if (impl instanceof ProxyAPIImplementation) {
+ result = this.handleForProxyAPIImplementation(request, impl);
+ } else {
+ result = this.callAPIImplementation(request, impl);
+ }
+ });
+
+ return {
+ type: Ci.mozIExtensionAPIRequestResult.RETURN_VALUE,
+ value: result,
+ };
+ } catch (error) {
+ return this.handleExtensionError(error);
+ }
+ }
+
+ /**
+ * Convert an error raised while handling an API request,
+ * into the expected mozIExtensionAPIRequestResult.
+ *
+ * @param {Error | WorkerExtensionError} error
+ * @returns {mozIExtensionAPIRequestResult}
+ */
+
+ handleExtensionError(error) {
+ // Propagate an extension error to the caller on the worker thread.
+ if (error instanceof this.context.Error) {
+ return {
+ type: Ci.mozIExtensionAPIRequestResult.EXTENSION_ERROR,
+ value: error,
+ };
+ }
+
+ // Otherwise just log it and throw a generic error.
+ Cu.reportError(error);
+ return {
+ type: Ci.mozIExtensionAPIRequestResult.EXTENSION_ERROR,
+ value: new this.context.Error("An unexpected error occurred"),
+ };
+ }
+
+ /**
+ * Handle the given mozIExtensionAPIRequest using the given
+ * APIImplementation instance.
+ *
+ * @param {mozIExtensionAPIRequest} request
+ * @param {ChildLocalWebIDLAPIImplementation | ProxyAPIImplementation} impl
+ * @returns {any}
+ * @throws {Error | WorkerExtensionError}
+ */
+
+ callAPIImplementation(request, impl) {
+ const { requestType, normalizedArgs } = request;
+
+ switch (requestType) {
+ // TODO (Bug 1728328): follow up to take callAsyncFunction requireUserInput
+ // parameter into account (until then callAsyncFunction, callFunction
+ // and callFunctionNoReturn calls do not differ yet).
+ case "callAsyncFunction":
+ case "callFunction":
+ case "callFunctionNoReturn":
+ case "getProperty":
+ return impl[requestType](normalizedArgs);
+ case "addListener": {
+ const listener = this.getOrCreateListenerWrapper(request, impl);
+ impl.addListener(listener, normalizedArgs);
+
+ return undefined;
+ }
+ case "removeListener": {
+ const listener = this.getListenerWrapper(request);
+ if (listener) {
+ // Remove the previously added listener and forget the cleanup
+ // observer previously passed to context.callOnClose.
+ listener._callOnClose.close();
+ this.context.forgetOnClose(listener._callOnClose);
+ this.forgetListenerWrapper(request);
+ }
+ return undefined;
+ }
+ default:
+ throw new Error(
+ `Unexpected requestType ${requestType} while handling "${request}"`
+ );
+ }
+ }
+
+ /**
+ * Handle the given mozIExtensionAPIRequest using the given
+ * ProxyAPIImplementation instance.
+ *
+ * @param {mozIExtensionAPIRequest} request
+ * @param {ProxyAPIImplementation} impl
+ * @returns {any}
+ * @throws {Error | WorkerExtensionError}
+ */
+
+ handleForProxyAPIImplementation(request, impl) {
+ const { requestType } = request;
+ switch (requestType) {
+ case "callAsyncFunction":
+ case "callFunctionNoReturn":
+ case "addListener":
+ case "removeListener":
+ return this.callAPIImplementation(request, impl);
+ default:
+ // Any other request types (e.g. getProperty or callFunction) are
+ // unexpected and so we raise a more detailed error to be logged
+ // on the browser console (while the extension will receive the
+ // generic "An unexpected error occurred" one).
+ throw new Error(
+ `Unexpected requestType ${requestType} while handling "${request}"`
+ );
+ }
+ }
+
+ getAPIPathForWebIDLRequest(request) {
+ const { apiNamespace, apiName, apiObjectType } = request;
+ if (apiObjectType) {
+ return `${apiNamespace}.${apiObjectType}.${apiName}`;
+ }
+
+ return `${apiNamespace}.${apiName}`;
+ }
+
+ /**
+ * Return an ExtensionAPI class instance given its namespace.
+ *
+ * @param {string} namespace
+ * @returns {ExtensionAPI}
+ */
+ getExtensionAPIInstance(namespace) {
+ return this.apiCan.apis.get(namespace);
+ }
+
+ getOrCreateListenerWrapper(request, impl) {
+ let listener = this.getListenerWrapper(request);
+ if (listener) {
+ return listener;
+ }
+
+ // Look for special wrappers that are needed for some API events
+ // (e.g. runtime.onMessage/onConnect/...).
+ if (impl instanceof ChildLocalWebIDLAPIImplementation) {
+ listener = impl.createListenerForAPIRequest(request);
+ }
+
+ const { eventListener } = request;
+ listener =
+ listener ??
+ function (...args) {
+ // Default wrapper just forwards all the arguments to the
+ // extension callback (all arguments has to be structure cloneable
+ // if the extension callback is on the worker thread).
+ eventListener.callListener(args);
+ };
+ listener._callOnClose = {
+ close: () => {
+ this.eventListenerWrappers.delete(eventListener);
+ // Failing to send the request to remove the listener in the parent
+ // process shouldn't prevent the extension or context shutdown,
+ // otherwise we would leak a WebExtensionPolicy instance.
+ try {
+ impl.removeListener(listener);
+ } catch (err) {
+ // Removing a listener when the extension context is being closed can
+ // fail if the API is proxied to the parent process and the conduit
+ // has been already closed, and so we ignore the error if we are not
+ // processing a call proxied to the parent process.
+ if (impl instanceof ChildLocalWebIDLAPIImplementation) {
+ Cu.reportError(err);
+ }
+ }
+ },
+ };
+ this.storeListenerWrapper(request, listener);
+ this.context.callOnClose(listener._callOnClose);
+ return listener;
+ }
+
+ getListenerWrapper(request) {
+ const { eventListener } = request;
+ if (!(eventListener instanceof Ci.mozIExtensionEventListener)) {
+ throw new Error(`Unexpected eventListener type for request: ${request}`);
+ }
+ const apiPath = this.getAPIPathForWebIDLRequest(request);
+ if (!this.eventListenerWrappers.has(apiPath)) {
+ return undefined;
+ }
+ return this.eventListenerWrappers.get(apiPath).get(eventListener);
+ }
+
+ storeListenerWrapper(request, listener) {
+ const { eventListener } = request;
+ if (!(eventListener instanceof Ci.mozIExtensionEventListener)) {
+ throw new Error(`Missing eventListener for request: ${request}`);
+ }
+ const apiPath = this.getAPIPathForWebIDLRequest(request);
+ this.eventListenerWrappers.get(apiPath).set(eventListener, listener);
+ }
+
+ forgetListenerWrapper(request) {
+ const { eventListener } = request;
+ if (!(eventListener instanceof Ci.mozIExtensionEventListener)) {
+ throw new Error(`Missing eventListener for request: ${request}`);
+ }
+ const apiPath = this.getAPIPathForWebIDLRequest(request);
+ if (this.eventListenerWrappers.has(apiPath)) {
+ this.eventListenerWrappers.get(apiPath).delete(eventListener);
+ }
+ }
+}
+
+class WorkerContextChild extends BaseContext {
+ /**
+ * This WorkerContextChild represents an addon execution environment
+ * that is running on the worker thread in an extension child process.
+ *
+ * @param {BrowserExtensionContent} extension This context's owner.
+ * @param {object} params
+ * @param {mozIExtensionServiceWorkerInfo} params.serviceWorkerInfo
+ */
+ constructor(extension, { serviceWorkerInfo }) {
+ if (
+ !serviceWorkerInfo?.scriptURL ||
+ !serviceWorkerInfo?.clientInfoId ||
+ !serviceWorkerInfo?.principal
+ ) {
+ throw new Error("Missing or invalid serviceWorkerInfo");
+ }
+
+ super("addon_child", extension);
+ this.viewType = "background_worker";
+ this.uri = Services.io.newURI(serviceWorkerInfo.scriptURL);
+ this.workerClientInfoId = serviceWorkerInfo.clientInfoId;
+ this.workerDescriptorId = serviceWorkerInfo.descriptorId;
+ this.workerPrincipal = serviceWorkerInfo.principal;
+ this.incognito = serviceWorkerInfo.principal.privateBrowsingId > 0;
+
+ // A mozIExtensionAPIRequest being processed (set by the withAPIRequest
+ // method while executing a given callable, can be optionally used by
+ // the API implementation methods to access the mozIExtensionAPIRequest
+ // being processed and customize their result if necessary to handle
+ // requests originated by the webidl bindings).
+ this.webidlAPIRequest = null;
+
+ // This context uses a plain object as a cloneScope (anyway the values
+ // moved across thread are going to be automatically serialized/deserialized
+ // as structure clone data, we may remove this if we are changing the
+ // internals to not use the context.cloneScope).
+ this.workerCloneScope = {
+ Promise,
+ // The instances of this Error constructor will be recognized by the
+ // ExtensionAPIRequestHandler as errors that should be propagated to
+ // the worker thread and received by extension code that originated
+ // the API request.
+ Error: ExtensionUtils.WorkerExtensionError,
+ };
+ }
+
+ getCreateProxyContextData() {
+ const { workerDescriptorId } = this;
+ return { workerDescriptorId };
+ }
+
+ openConduit(subject, address) {
+ let proc = ChromeUtils.domProcessChild;
+ let conduit = proc.getActor("ProcessConduits").openConduit(subject, {
+ id: subject.id || getUniqueId(),
+ extensionId: this.extension.id,
+ envType: this.envType,
+ workerScriptURL: this.uri.spec,
+ workerDescriptorId: this.workerDescriptorId,
+ ...address,
+ });
+ this.callOnClose(conduit);
+ conduit.setCloseCallback(() => {
+ this.forgetOnClose(conduit);
+ });
+ return conduit;
+ }
+
+ notifyWorkerLoaded() {
+ this.childManager.conduit.sendContextLoaded({
+ childId: this.childManager.id,
+ extensionId: this.extension.id,
+ workerDescriptorId: this.workerDescriptorId,
+ });
+ }
+
+ withAPIRequest(request, callable) {
+ this.webidlAPIRequest = request;
+ try {
+ return callable();
+ } finally {
+ this.webidlAPIRequest = null;
+ }
+ }
+
+ getAPIRequest() {
+ return this.webidlAPIRequest;
+ }
+
+ /**
+ * Captures the most recent stack frame from the WebIDL API request being
+ * processed.
+ *
+ * @returns {SavedFrame?}
+ */
+ getCaller() {
+ return this.webidlAPIRequest?.callerSavedFrame;
+ }
+
+ logActivity(type, name, data) {
+ ExtensionActivityLogChild.log(this, type, name, data);
+ }
+
+ get cloneScope() {
+ return this.workerCloneScope;
+ }
+
+ get principal() {
+ return this.workerPrincipal;
+ }
+
+ get tabId() {
+ return -1;
+ }
+
+ get useWebIDLBindings() {
+ return true;
+ }
+
+ shutdown() {
+ this.unload();
+ }
+
+ unload() {
+ if (this.unloaded) {
+ return;
+ }
+
+ super.unload();
+ }
+}
+
+defineLazyGetter(WorkerContextChild.prototype, "messenger", function () {
+ return new WorkerMessenger(this);
+});
+
+defineLazyGetter(
+ WorkerContextChild.prototype,
+ "childManager",
+ getContextChildManagerGetter(
+ { envType: "addon_parent" },
+ WebIDLChildAPIManager
+ )
+);
+
+export var ExtensionWorkerChild = {
+ // Map<serviceWorkerDescriptorId, ExtensionWorkerContextChild>
+ extensionWorkerContexts: new Map(),
+
+ apiManager: ExtensionPageChild.apiManager,
+
+ /**
+ * Create an extension worker context (on a mozExtensionAPIRequest with
+ * requestType "initWorkerContext").
+ *
+ * @param {BrowserExtensionContent} extension
+ * The extension for which the context should be created.
+ * @param {mozIExtensionServiceWorkerInfo} serviceWorkerInfo
+ */
+ initExtensionWorkerContext(extension, serviceWorkerInfo) {
+ if (!WebExtensionPolicy.isExtensionProcess) {
+ throw new Error(
+ "Cannot create an extension worker context in current process"
+ );
+ }
+
+ const swId = serviceWorkerInfo.descriptorId;
+ let context = this.extensionWorkerContexts.get(swId);
+ if (context) {
+ if (context.extension !== extension) {
+ throw new Error(
+ "A different extension context already exists for this service worker"
+ );
+ }
+ throw new Error(
+ "An extension context was already initialized for this service worker"
+ );
+ }
+
+ context = new WorkerContextChild(extension, { serviceWorkerInfo });
+ this.extensionWorkerContexts.set(swId, context);
+ },
+
+ /**
+ * Get an existing extension worker context for the given extension and
+ * service worker.
+ *
+ * @param {BrowserExtensionContent} extension
+ * The extension for which the context should be created.
+ * @param {mozIExtensionServiceWorkerInfo} serviceWorkerInfo
+ *
+ * @returns {ExtensionWorkerContextChild}
+ */
+ getExtensionWorkerContext(extension, serviceWorkerInfo) {
+ if (!serviceWorkerInfo) {
+ return null;
+ }
+
+ const context = this.extensionWorkerContexts.get(
+ serviceWorkerInfo.descriptorId
+ );
+
+ if (context?.extension === extension) {
+ return context;
+ }
+
+ return null;
+ },
+
+ /**
+ * Notify the main process when an extension worker script has been loaded.
+ *
+ * @param {number} descriptorId The service worker descriptor ID of the destroyed context.
+ * @param {WebExtensionPolicy} policy
+ */
+ notifyExtensionWorkerContextLoaded(descriptorId, policy) {
+ let context = this.extensionWorkerContexts.get(descriptorId);
+ if (context) {
+ if (context.extension.id !== policy.id) {
+ Cu.reportError(
+ new Error(
+ `ServiceWorker ${descriptorId} does not belong to the expected extension: ${policy.id}`
+ )
+ );
+ return;
+ }
+ context.notifyWorkerLoaded();
+ }
+ },
+
+ /**
+ * Close the ExtensionWorkerContextChild belonging to the given service worker, if any.
+ *
+ * @param {number} descriptorId The service worker descriptor ID of the destroyed context.
+ */
+ destroyExtensionWorkerContext(descriptorId) {
+ let context = this.extensionWorkerContexts.get(descriptorId);
+ if (context) {
+ context.unload();
+ this.extensionWorkerContexts.delete(descriptorId);
+ }
+ },
+
+ shutdownExtension(extensionId) {
+ for (let [workerClientInfoId, context] of this.extensionWorkerContexts) {
+ if (context.extension.id == extensionId) {
+ context.shutdown();
+ this.extensionWorkerContexts.delete(workerClientInfoId);
+ }
+ }
+ },
+};