summaryrefslogtreecommitdiffstats
path: root/devtools/server/connectors/process-actor/DevToolsServiceWorkerChild.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/connectors/process-actor/DevToolsServiceWorkerChild.sys.mjs')
-rw-r--r--devtools/server/connectors/process-actor/DevToolsServiceWorkerChild.sys.mjs741
1 files changed, 0 insertions, 741 deletions
diff --git a/devtools/server/connectors/process-actor/DevToolsServiceWorkerChild.sys.mjs b/devtools/server/connectors/process-actor/DevToolsServiceWorkerChild.sys.mjs
deleted file mode 100644
index 2e461cbd03..0000000000
--- a/devtools/server/connectors/process-actor/DevToolsServiceWorkerChild.sys.mjs
+++ /dev/null
@@ -1,741 +0,0 @@
-/* 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/. */
-
-import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";
-import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
-
-const lazy = {};
-ChromeUtils.defineESModuleGetters(lazy, {
- loader: "resource://devtools/shared/loader/Loader.sys.mjs",
-});
-
-XPCOMUtils.defineLazyServiceGetter(
- lazy,
- "wdm",
- "@mozilla.org/dom/workers/workerdebuggermanager;1",
- "nsIWorkerDebuggerManager"
-);
-
-XPCOMUtils.defineLazyModuleGetters(lazy, {
- SessionDataHelpers:
- "resource://devtools/server/actors/watcher/SessionDataHelpers.jsm",
-});
-
-ChromeUtils.defineLazyGetter(lazy, "DevToolsUtils", () =>
- lazy.loader.require("devtools/shared/DevToolsUtils")
-);
-
-// Name of the attribute into which we save data in `sharedData` object.
-const SHARED_DATA_KEY_NAME = "DevTools:watchedPerWatcher";
-
-export class DevToolsServiceWorkerChild extends JSProcessActorChild {
- constructor() {
- super();
-
- // The map is indexed by the Watcher Actor ID.
- // The values are objects containing the following properties:
- // - connection: the DevToolsServerConnection itself
- // - workers: An array of object containing the following properties:
- // - dbg: A WorkerDebuggerInstance
- // - serviceWorkerTargetForm: The associated worker target instance form
- // - workerThreadServerForwardingPrefix: The prefix used to forward events to the
- // worker target on the worker thread ().
- // - forwardingPrefix: Prefix used by the JSWindowActorTransport pair to communicate
- // between content and parent processes.
- // - sessionData: Data (targets, resources, …) the watcher wants to be notified about.
- // See WatcherRegistry.getSessionData to see the full list of properties.
- this._connections = new Map();
-
- this._onConnectionChange = this._onConnectionChange.bind(this);
-
- EventEmitter.decorate(this);
- }
-
- /**
- * Called by nsIWorkerDebuggerManager when a worker get created.
- *
- * Go through all registered connections (in case we have more than one client connected)
- * to eventually instantiate a target actor for this worker.
- *
- * @param {nsIWorkerDebugger} dbg
- */
- _onWorkerRegistered(dbg) {
- // Only consider service workers
- if (dbg.type !== Ci.nsIWorkerDebugger.TYPE_SERVICE) {
- return;
- }
-
- for (const [
- watcherActorID,
- { connection, forwardingPrefix, sessionData },
- ] of this._connections) {
- if (this._shouldHandleWorker(sessionData, dbg)) {
- this._createWorkerTargetActor({
- dbg,
- connection,
- forwardingPrefix,
- watcherActorID,
- });
- }
- }
- }
-
- /**
- * Called by nsIWorkerDebuggerManager when a worker get destroyed.
- *
- * Go through all registered connections (in case we have more than one client connected)
- * to destroy the related target which may have been created for this worker.
- *
- * @param {nsIWorkerDebugger} dbg
- */
- _onWorkerUnregistered(dbg) {
- // Only consider service workers
- if (dbg.type !== Ci.nsIWorkerDebugger.TYPE_SERVICE) {
- return;
- }
-
- for (const [watcherActorID, watcherConnectionData] of this._connections) {
- this._destroyServiceWorkerTargetForWatcher(
- watcherActorID,
- watcherConnectionData,
- dbg
- );
- }
- }
-
- /**
- * To be called when we know a Service Worker target should be destroyed for a specific connection
- * for which we pass the related "watcher connection data".
- *
- * @param {String} watcherActorID
- * Watcher actor ID for which we should unregister this service worker.
- * @param {Object} watcherConnectionData
- * The metadata object for a given watcher, stored in the _connections Map.
- * @param {nsIWorkerDebugger} dbg
- */
- _destroyServiceWorkerTargetForWatcher(
- watcherActorID,
- watcherConnectionData,
- dbg
- ) {
- const { workers, forwardingPrefix } = watcherConnectionData;
-
- // Check if the worker registration was handled for this watcher.
- const unregisteredActorIndex = workers.findIndex(worker => {
- try {
- // Accessing the WorkerDebugger id might throw (NS_ERROR_UNEXPECTED).
- return worker.dbg.id === dbg.id;
- } catch (e) {
- return false;
- }
- });
-
- // Ignore this worker if it wasn't registered for this watcher.
- if (unregisteredActorIndex === -1) {
- return;
- }
-
- const { serviceWorkerTargetForm, transport } =
- workers[unregisteredActorIndex];
-
- // Remove the entry from this._connection dictionnary
- workers.splice(unregisteredActorIndex, 1);
-
- // Close the transport made against the worker thread.
- transport.close();
-
- // Note that we do not need to post the "disconnect" message from this destruction codepath
- // as this method is only called when the worker is unregistered and so,
- // we can't send any message anyway, and the worker is being destroyed anyway.
-
- // Also notify the parent process that this worker target got destroyed.
- // As the worker thread may be already destroyed, it may not have time to send a destroy event.
- try {
- this.sendAsyncMessage(
- "DevToolsServiceWorkerChild:serviceWorkerTargetDestroyed",
- {
- watcherActorID,
- forwardingPrefix,
- serviceWorkerTargetForm,
- }
- );
- } catch (e) {
- // Ignore exception which may happen on content process destruction
- }
- }
-
- /**
- * Function handling messages sent by DevToolsServiceWorkerParent (part of ProcessActor API).
- *
- * @param {Object} message
- * @param {String} message.name
- * @param {*} message.data
- */
- receiveMessage(message) {
- switch (message.name) {
- case "DevToolsServiceWorkerParent:instantiate-already-available": {
- const { watcherActorID, connectionPrefix, sessionData } = message.data;
- return this._watchWorkerTargets({
- watcherActorID,
- parentConnectionPrefix: connectionPrefix,
- sessionData,
- });
- }
- case "DevToolsServiceWorkerParent:destroy": {
- const { watcherActorID } = message.data;
- return this._destroyTargetActors(watcherActorID);
- }
- case "DevToolsServiceWorkerParent:addOrSetSessionDataEntry": {
- const { watcherActorID, type, entries, updateType } = message.data;
- return this._addOrSetSessionDataEntry(
- watcherActorID,
- type,
- entries,
- updateType
- );
- }
- case "DevToolsServiceWorkerParent:removeSessionDataEntry": {
- const { watcherActorID, type, entries } = message.data;
- return this._removeSessionDataEntry(watcherActorID, type, entries);
- }
- case "DevToolsServiceWorkerParent:packet":
- return this.emit("packet-received", message);
- default:
- throw new Error(
- "Unsupported message in DevToolsServiceWorkerParent: " + message.name
- );
- }
- }
-
- /**
- * "chrome-event-target-created" event handler. Supposed to be fired very early when the process starts
- */
- observe() {
- const { sharedData } = Services.cpmm;
- const sessionDataByWatcherActor = sharedData.get(SHARED_DATA_KEY_NAME);
- if (!sessionDataByWatcherActor) {
- throw new Error(
- "Request to instantiate the target(s) for the Service Worker, but `sharedData` is empty about watched targets"
- );
- }
-
- // Create one Target actor for each prefix/client which listen to workers
- for (const [watcherActorID, sessionData] of sessionDataByWatcherActor) {
- const { targets, connectionPrefix } = sessionData;
- if (targets?.includes("service_worker")) {
- this._watchWorkerTargets({
- watcherActorID,
- parentConnectionPrefix: connectionPrefix,
- sessionData,
- });
- }
- }
- }
-
- /**
- * Instantiate targets for existing workers, watch for worker registration and listen
- * for resources on those workers, for given connection and context. Targets are sent
- * to the DevToolsServiceWorkerParent via the DevToolsServiceWorkerChild:serviceWorkerTargetAvailable message.
- *
- * @param {Object} options
- * @param {String} options.watcherActorID: The ID of the WatcherActor who requested to
- * observe and create these target actors.
- * @param {String} options.parentConnectionPrefix: The prefix of the DevToolsServerConnection
- * of the Watcher Actor. This is used to compute a unique ID for the target actor.
- * @param {Object} options.sessionData: Data (targets, resources, …) the watcher wants
- * to be notified about. See WatcherRegistry.getSessionData to see the full list
- * of properties.
- */
- async _watchWorkerTargets({
- watcherActorID,
- parentConnectionPrefix,
- sessionData,
- }) {
- // We might already have been called from observe method if the process was initializing
- if (this._connections.has(watcherActorID)) {
- // In such case, wait for the promise in order to ensure resolving only after
- // we notified about the existing targets
- await this._connections.get(watcherActorID).watchPromise;
- return;
- }
-
- // Compute a unique prefix, just for this Service Worker,
- // which will be used to create a JSWindowActorTransport pair between content and parent processes.
- // This is slightly hacky as we typicaly compute Prefix and Actor ID via `DevToolsServerConnection.allocID()`,
- // but here, we can't have access to any DevTools connection as we are really early in the content process startup
- // WindowGlobalChild's innerWindowId should be unique across processes, so it should be safe?
- // (this.manager == WindowGlobalChild interface)
- const forwardingPrefix =
- parentConnectionPrefix + "serviceWorkerProcess" + this.manager.childID;
-
- const connection = this._createConnection(forwardingPrefix);
-
- // This method will be concurrently called from `observe()` and `DevToolsServiceWorkerParent:instantiate-already-available`
- // When the JSprocessActor initializes itself and when the watcher want to force instantiating existing targets.
- // Wait for the existing promise when the second call arise.
- //
- // Also, _connections has to be populated *before* calling _createWorkerTargetActor,
- // so create a deferred promise right away.
- let resolveWatchPromise;
- const watchPromise = new Promise(
- resolve => (resolveWatchPromise = resolve)
- );
-
- this._connections.set(watcherActorID, {
- connection,
- watchPromise,
- workers: [],
- forwardingPrefix,
- sessionData,
- });
-
- // Listen for new workers that will be spawned.
- if (!this._workerDebuggerListener) {
- this._workerDebuggerListener = {
- onRegister: this._onWorkerRegistered.bind(this),
- onUnregister: this._onWorkerUnregistered.bind(this),
- };
- lazy.wdm.addListener(this._workerDebuggerListener);
- }
-
- const promises = [];
- for (const dbg of lazy.wdm.getWorkerDebuggerEnumerator()) {
- if (!this._shouldHandleWorker(sessionData, dbg)) {
- continue;
- }
- promises.push(
- this._createWorkerTargetActor({
- dbg,
- connection,
- forwardingPrefix,
- watcherActorID,
- })
- );
- }
- await Promise.all(promises);
- resolveWatchPromise();
- }
-
- /**
- * Initialize a DevTools Server and return a new DevToolsServerConnection
- * using this server in order to communicate to the parent process via
- * the JSProcessActor message / queries.
- *
- * @param String forwardingPrefix
- * A unique prefix used to distinguish message coming from distinct service workers.
- * @return DevToolsServerConnection
- * A connection to communicate with the parent process.
- */
- _createConnection(forwardingPrefix) {
- const { DevToolsServer } = lazy.loader.require(
- "devtools/server/devtools-server"
- );
-
- DevToolsServer.init();
-
- // We want a special server without any root actor and only target-scoped actors.
- // We are going to spawn a WorkerTargetActor instance in the next few lines,
- // it is going to act like a root actor without being one.
- DevToolsServer.registerActors({ target: true });
- DevToolsServer.on("connectionchange", this._onConnectionChange);
-
- const connection = DevToolsServer.connectToParentWindowActor(
- this,
- forwardingPrefix
- );
-
- return connection;
- }
-
- /**
- * Indicates whether or not we should handle the worker debugger for a given
- * watcher's session data.
- *
- * @param {Object} sessionData
- * The session data for a given watcher, which includes metadata
- * about the debugged context.
- * @param {WorkerDebugger} dbg
- * The worker debugger we want to check.
- *
- * @returns {Boolean}
- */
- _shouldHandleWorker(sessionData, dbg) {
- if (dbg.type !== Ci.nsIWorkerDebugger.TYPE_SERVICE) {
- return false;
- }
- // We only want to create targets for non-closed service worker
- if (!lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
- return false;
- }
-
- // Accessing `nsIPrincipal.host` may easily throw on non-http URLs.
- // Ignore all non-HTTP as they most likely don't have any valid host name.
- if (!dbg.principal.scheme.startsWith("http")) {
- return false;
- }
-
- const workerHost = dbg.principal.hostPort;
- return workerHost == sessionData["browser-element-host"][0];
- }
-
- async _createWorkerTargetActor({
- dbg,
- connection,
- forwardingPrefix,
- watcherActorID,
- }) {
- // Freeze the worker execution as soon as possible in order to wait for DevTools bootstrap.
- // We typically want to:
- // - startup the Thread Actor,
- // - pass the initial session data which includes breakpoints to the worker thread,
- // - register the breakpoints,
- // before release its execution.
- // `connectToWorker` is going to call setDebuggerReady(true) when all of this is done.
- try {
- dbg.setDebuggerReady(false);
- } catch (e) {
- // This call will throw if the debugger is already "registered"
- // (i.e. if this is called outside of the register listener)
- // See https://searchfox.org/mozilla-central/rev/84922363f4014eae684aabc4f1d06380066494c5/dom/workers/nsIWorkerDebugger.idl#55-66
- }
-
- const watcherConnectionData = this._connections.get(watcherActorID);
- const { sessionData } = watcherConnectionData;
- const workerThreadServerForwardingPrefix = connection.allocID(
- "serviceWorkerTarget"
- );
-
- // Create the actual worker target actor, in the worker thread.
- const { connectToWorker } = lazy.loader.require(
- "devtools/server/connectors/worker-connector"
- );
-
- const onConnectToWorker = connectToWorker(
- connection,
- dbg,
- workerThreadServerForwardingPrefix,
- {
- sessionData,
- sessionContext: sessionData.sessionContext,
- }
- );
-
- try {
- await onConnectToWorker;
- } catch (e) {
- // connectToWorker is supposed to call setDebuggerReady(true) to release the worker execution.
- // But if anything goes wrong and an exception is thrown, ensure releasing its execution,
- // otherwise if devtools is broken, it will freeze the worker indefinitely.
- //
- // onConnectToWorker can reject if the Worker Debugger is closed; so we only want to
- // resume the debugger if it is not closed (otherwise it can cause crashes).
- if (!dbg.isClosed) {
- dbg.setDebuggerReady(true);
- }
- return;
- }
-
- const { workerTargetForm, transport } = await onConnectToWorker;
-
- try {
- this.sendAsyncMessage(
- "DevToolsServiceWorkerChild:serviceWorkerTargetAvailable",
- {
- watcherActorID,
- forwardingPrefix,
- serviceWorkerTargetForm: workerTargetForm,
- }
- );
- } catch (e) {
- // If there was an error while sending the message, we are not going to use this
- // connection to communicate with the worker.
- transport.close();
- return;
- }
-
- // Only add data to the connection if we successfully send the
- // serviceWorkerTargetAvailable message.
- watcherConnectionData.workers.push({
- dbg,
- transport,
- serviceWorkerTargetForm: workerTargetForm,
- workerThreadServerForwardingPrefix,
- });
- }
-
- /**
- * Request the service worker threads to destroy all their service worker Targets currently registered for a given Watcher actor.
- *
- * @param {String} watcherActorID
- */
- _destroyTargetActors(watcherActorID) {
- const watcherConnectionData = this._connections.get(watcherActorID);
- this._connections.delete(watcherActorID);
-
- // This connection has already been cleaned?
- if (!watcherConnectionData) {
- console.error(
- `Trying to destroy a target actor that doesn't exists, or has already been destroyed. Watcher Actor ID:${watcherActorID}`
- );
- return;
- }
-
- for (const {
- dbg,
- transport,
- workerThreadServerForwardingPrefix,
- } of watcherConnectionData.workers) {
- try {
- if (lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
- dbg.postMessage(
- JSON.stringify({
- type: "disconnect",
- forwardingPrefix: workerThreadServerForwardingPrefix,
- })
- );
- }
- } catch (e) {}
-
- transport.close();
- }
-
- watcherConnectionData.connection.close();
- }
-
- /**
- * Destroy the server once its last connection closes. Note that multiple
- * worker scripts may be running in parallel and reuse the same server.
- */
- _onConnectionChange() {
- const { DevToolsServer } = lazy.loader.require(
- "devtools/server/devtools-server"
- );
-
- // Only destroy the server if there is no more connections to it. It may be
- // used to debug another tab running in the same process.
- if (DevToolsServer.hasConnection() || DevToolsServer.keepAlive) {
- return;
- }
-
- if (this._destroyed) {
- return;
- }
- this._destroyed = true;
-
- DevToolsServer.off("connectionchange", this._onConnectionChange);
- DevToolsServer.destroy();
- }
-
- /**
- * Used by DevTools transport layer to communicate with the parent process.
- *
- * @param {String} packet
- * @param {String prefix
- */
- async sendPacket(packet, prefix) {
- return this.sendAsyncMessage("DevToolsServiceWorkerChild:packet", {
- packet,
- prefix,
- });
- }
-
- /**
- * Go through all registered service workers for a given watcher actor
- * to send them new session data entries.
- *
- * See addOrSetSessionDataEntryInWorkerTarget for more info about arguments.
- */
- async _addOrSetSessionDataEntry(watcherActorID, type, entries, updateType) {
- const watcherConnectionData = this._connections.get(watcherActorID);
- if (!watcherConnectionData) {
- return;
- }
-
- lazy.SessionDataHelpers.addOrSetSessionDataEntry(
- watcherConnectionData.sessionData,
- type,
- entries,
- updateType
- );
-
- // This type is really specific to Service Workers and doesn't need to be transferred to the worker threads.
- // We only need to instantiate and destroy the target actors based on this new host.
- if (type == "browser-element-host") {
- this.updateBrowserElementHost(watcherActorID, watcherConnectionData);
- return;
- }
-
- const promises = [];
- for (const {
- dbg,
- workerThreadServerForwardingPrefix,
- } of watcherConnectionData.workers) {
- promises.push(
- addOrSetSessionDataEntryInWorkerTarget({
- dbg,
- workerThreadServerForwardingPrefix,
- type,
- entries,
- updateType,
- })
- );
- }
- await Promise.all(promises);
- }
-
- /**
- * Called whenever the debugged browser element navigates to a new page
- * and the URL's host changes.
- * This is used to maintain the list of active Service Worker targets
- * based on that host name.
- *
- * @param {String} watcherActorID
- * Watcher actor ID for which we should unregister this service worker.
- * @param {Object} watcherConnectionData
- * The metadata object for a given watcher, stored in the _connections Map.
- */
- async updateBrowserElementHost(watcherActorID, watcherConnectionData) {
- const { sessionData, connection, forwardingPrefix } = watcherConnectionData;
-
- // Create target actor matching this new host.
- // Note that we may be navigating to the same host name and the target will already exist.
- const dbgToInstantiate = [];
- for (const dbg of lazy.wdm.getWorkerDebuggerEnumerator()) {
- const alreadyCreated = watcherConnectionData.workers.some(
- info => info.dbg === dbg
- );
- if (this._shouldHandleWorker(sessionData, dbg) && !alreadyCreated) {
- dbgToInstantiate.push(dbg);
- }
- }
- await Promise.all(
- dbgToInstantiate.map(dbg => {
- return this._createWorkerTargetActor({
- dbg,
- connection,
- forwardingPrefix,
- watcherActorID,
- });
- })
- );
- }
-
- /**
- * Go through all registered service workers for a given watcher actor
- * to send them request to clear some session data entries.
- *
- * See addOrSetSessionDataEntryInWorkerTarget for more info about arguments.
- */
- _removeSessionDataEntry(watcherActorID, type, entries) {
- const watcherConnectionData = this._connections.get(watcherActorID);
-
- if (!watcherConnectionData) {
- return;
- }
-
- lazy.SessionDataHelpers.removeSessionDataEntry(
- watcherConnectionData.sessionData,
- type,
- entries
- );
-
- for (const {
- dbg,
- workerThreadServerForwardingPrefix,
- } of watcherConnectionData.workers) {
- if (lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
- dbg.postMessage(
- JSON.stringify({
- type: "remove-session-data-entry",
- forwardingPrefix: workerThreadServerForwardingPrefix,
- dataEntryType: type,
- entries,
- })
- );
- }
- }
- }
-
- _removeExistingWorkerDebuggerListener() {
- if (this._workerDebuggerListener) {
- lazy.wdm.removeListener(this._workerDebuggerListener);
- this._workerDebuggerListener = null;
- }
- }
-
- /**
- * Part of JSActor API
- * https://searchfox.org/mozilla-central/rev/d9f92154813fbd4a528453c33886dc3a74f27abb/dom/chrome-webidl/JSActor.webidl#41-42,52
- *
- * > The didDestroy method, if present, will be called after the actor is no
- * > longer able to receive any more messages.
- */
- didDestroy() {
- this._removeExistingWorkerDebuggerListener();
-
- for (const [watcherActorID, watcherConnectionData] of this._connections) {
- const { connection } = watcherConnectionData;
- this._destroyTargetActors(watcherActorID);
-
- connection.close();
- }
-
- this._connections.clear();
- }
-}
-
-/**
- * Communicate the type and entries to the Worker Target actor, via the WorkerDebugger.
- *
- * @param {WorkerDebugger} dbg
- * @param {String} workerThreadServerForwardingPrefix
- * @param {String} type
- * Session data type name
- * @param {Array} entries
- * Session data entries to add or set.
- * @param {String} updateType
- * Either "add" or "set", to control if we should only add some items,
- * or replace the whole data set with the new entries.
- * @returns {Promise} Returns a Promise that resolves once the data entry were handled
- * by the worker target.
- */
-function addOrSetSessionDataEntryInWorkerTarget({
- dbg,
- workerThreadServerForwardingPrefix,
- type,
- entries,
- updateType,
-}) {
- if (!lazy.DevToolsUtils.isWorkerDebuggerAlive(dbg)) {
- return Promise.resolve();
- }
-
- return new Promise(resolve => {
- // Wait until we're notified by the worker that the resources are watched.
- // This is important so we know existing resources were handled.
- const listener = {
- onMessage: message => {
- message = JSON.parse(message);
- if (message.type === "session-data-entry-added-or-set") {
- resolve();
- dbg.removeListener(listener);
- }
- },
- // Resolve if the worker is being destroyed so we don't have a dangling promise.
- onClose: () => resolve(),
- };
-
- dbg.addListener(listener);
-
- dbg.postMessage(
- JSON.stringify({
- type: "add-or-set-session-data-entry",
- forwardingPrefix: workerThreadServerForwardingPrefix,
- dataEntryType: type,
- entries,
- updateType,
- })
- );
- });
-}