diff options
Diffstat (limited to 'devtools/server/actors/descriptors')
-rw-r--r-- | devtools/server/actors/descriptors/moz.build | 12 | ||||
-rw-r--r-- | devtools/server/actors/descriptors/process.js | 169 | ||||
-rw-r--r-- | devtools/server/actors/descriptors/tab.js | 216 | ||||
-rw-r--r-- | devtools/server/actors/descriptors/webextension.js | 252 | ||||
-rw-r--r-- | devtools/server/actors/descriptors/worker.js | 253 |
5 files changed, 902 insertions, 0 deletions
diff --git a/devtools/server/actors/descriptors/moz.build b/devtools/server/actors/descriptors/moz.build new file mode 100644 index 0000000000..bf297b3dcb --- /dev/null +++ b/devtools/server/actors/descriptors/moz.build @@ -0,0 +1,12 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +DevToolsModules( + "process.js", + "tab.js", + "webextension.js", + "worker.js", +) diff --git a/devtools/server/actors/descriptors/process.js b/devtools/server/actors/descriptors/process.js new file mode 100644 index 0000000000..b15e6b0b19 --- /dev/null +++ b/devtools/server/actors/descriptors/process.js @@ -0,0 +1,169 @@ +/* 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/. */ + +"use strict"; + +const Services = require("Services"); +const { DevToolsServer } = require("devtools/server/devtools-server"); +const { Cc, Ci } = require("chrome"); + +const { ActorClassWithSpec, Actor } = require("devtools/shared/protocol"); +const { + processDescriptorSpec, +} = require("devtools/shared/specs/descriptors/process"); + +loader.lazyRequireGetter( + this, + "ContentProcessTargetActor", + "devtools/server/actors/targets/content-process", + true +); +loader.lazyRequireGetter( + this, + "ParentProcessTargetActor", + "devtools/server/actors/targets/parent-process", + true +); +loader.lazyRequireGetter( + this, + "connectToContentProcess", + "devtools/server/connectors/content-process-connector", + true +); +loader.lazyRequireGetter( + this, + "WatcherActor", + "devtools/server/actors/watcher", + true +); + +const ProcessDescriptorActor = ActorClassWithSpec(processDescriptorSpec, { + initialize(connection, options = {}) { + if ("id" in options && typeof options.id != "number") { + throw Error("process connect requires a valid `id` attribute."); + } + Actor.prototype.initialize.call(this, connection); + this.id = options.id; + this._browsingContextTargetActor = null; + this.isParent = options.parent; + this.destroy = this.destroy.bind(this); + }, + + get browsingContextID() { + if (this._browsingContextTargetActor) { + return this._browsingContextTargetActor.docShell.browsingContext.id; + } + return null; + }, + + _parentProcessConnect() { + const env = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment + ); + const isXpcshell = env.exists("XPCSHELL_TEST_PROFILE_DIR"); + let targetActor; + if (isXpcshell) { + // Check if we are running on xpcshell. + // When running on xpcshell, there is no valid browsing context to attach to + // and so ParentProcessTargetActor doesn't make sense as it inherits from + // BrowsingContextTargetActor. So instead use ContentProcessTargetActor, which + // matches xpcshell needs. + targetActor = new ContentProcessTargetActor(this.conn); + } else { + // Create the target actor for the parent process, which is in the same process + // as this target. Because we are in the same process, we have a true actor that + // should be managed by the ProcessDescriptorActor. + targetActor = new ParentProcessTargetActor(this.conn); + // this is a special field that only parent process with a browsing context + // have, as they are the only processes at the moment that have child + // browsing contexts + this._browsingContextTargetActor = targetActor; + } + this.manage(targetActor); + // to be consistent with the return value of the _childProcessConnect, we are returning + // the form here. This might be memoized in the future + return targetActor.form(); + }, + + /** + * Connect to a remote process actor, always a ContentProcess target. + */ + async _childProcessConnect() { + const { id } = this; + const mm = this._lookupMessageManager(id); + if (!mm) { + return { + error: "noProcess", + message: "There is no process with id '" + id + "'.", + }; + } + const childTargetForm = await connectToContentProcess( + this.conn, + mm, + this.destroy + ); + return childTargetForm; + }, + + _lookupMessageManager(id) { + for (let i = 0; i < Services.ppmm.childCount; i++) { + const mm = Services.ppmm.getChildAt(i); + + // A zero id is used for the parent process, instead of its actual pid. + if (id ? mm.osPid == id : mm.isInProcess) { + return mm; + } + } + return null; + }, + + /** + * Connect the a process actor. + */ + async getTarget() { + if (!DevToolsServer.allowChromeProcess) { + return { + error: "forbidden", + message: "You are not allowed to debug processes.", + }; + } + if (this.isParent) { + return this._parentProcessConnect(); + } + // This is a remote process we are connecting to + return this._childProcessConnect(); + }, + + /** + * Return a Watcher actor, allowing to keep track of targets which + * already exists or will be created. It also helps knowing when they + * are destroyed. + */ + getWatcher() { + if (!this.watcher) { + this.watcher = new WatcherActor(this.conn); + this.manage(this.watcher); + } + return this.watcher; + }, + + form() { + return { + actor: this.actorID, + id: this.id, + isParent: this.isParent, + traits: { + // Supports the Watcher actor. Can be removed as part of Bug 1680280. + watcher: true, + }, + }; + }, + + destroy() { + this._browsingContextTargetActor = null; + Actor.prototype.destroy.call(this); + }, +}); + +exports.ProcessDescriptorActor = ProcessDescriptorActor; diff --git a/devtools/server/actors/descriptors/tab.js b/devtools/server/actors/descriptors/tab.js new file mode 100644 index 0000000000..b08cad801f --- /dev/null +++ b/devtools/server/actors/descriptors/tab.js @@ -0,0 +1,216 @@ +/* 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/. */ + +"use strict"; + +/* + * Descriptor Actor that represents a Tab in the parent process. It + * launches a FrameTargetActor in the content process to do the real work and tunnels the + * data. + * + * See devtools/docs/backend/actor-hierarchy.md for more details. + */ + +const Services = require("Services"); +const { + connectToFrame, +} = require("devtools/server/connectors/frame-connector"); +loader.lazyImporter( + this, + "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm" +); +const { ActorClassWithSpec, Actor } = require("devtools/shared/protocol"); +const { tabDescriptorSpec } = require("devtools/shared/specs/descriptors/tab"); +const { AppConstants } = require("resource://gre/modules/AppConstants.jsm"); + +loader.lazyRequireGetter( + this, + "WatcherActor", + "devtools/server/actors/watcher", + true +); + +/** + * Creates a target actor proxy for handling requests to a single browser frame. + * Both <xul:browser> and <iframe mozbrowser> are supported. + * This actor is a shim that connects to a FrameTargetActor in a remote browser process. + * All RDP packets get forwarded using the message manager. + * + * @param connection The main RDP connection. + * @param browser <xul:browser> or <iframe mozbrowser> element to connect to. + */ +const TabDescriptorActor = ActorClassWithSpec(tabDescriptorSpec, { + initialize(connection, browser) { + Actor.prototype.initialize.call(this, connection); + this._conn = connection; + this._browser = browser; + }, + + form() { + const form = { + actor: this.actorID, + browsingContextID: + this._browser && this._browser.browsingContext + ? this._browser.browsingContext.id + : null, + isZombieTab: this._isZombieTab(), + outerWindowID: this._getOuterWindowId(), + selected: this.selected, + title: this._getTitle(), + traits: { + // Supports the Watcher actor. Can be removed as part of Bug 1680280. + watcher: true, + }, + url: this._getUrl(), + }; + + return form; + }, + + _getTitle() { + // If the content already provides a title, use it. + if (this._browser.contentTitle) { + return this._browser.contentTitle; + } + + // For zombie or lazy tabs (tab created, but content has not been loaded), + // try to retrieve the title from the XUL Tab itself. + // Note: this only works on Firefox desktop. + if (this._tabbrowser) { + const tab = this._tabbrowser.getTabForBrowser(this._browser); + if (tab) { + return tab.label; + } + } + + // No title available. + return null; + }, + + _getUrl() { + if (!this._browser || !this._browser.browsingContext) { + return ""; + } + + const { browsingContext } = this._browser; + return browsingContext.currentWindowGlobal.documentURI.spec; + }, + + _getOuterWindowId() { + if (!this._browser || !this._browser.browsingContext) { + return ""; + } + + const { browsingContext } = this._browser; + return browsingContext.currentWindowGlobal.outerWindowId; + }, + + get selected() { + // getMostRecentBrowserWindow will find the appropriate window on Firefox + // Desktop and on GeckoView. + const topAppWindow = Services.wm.getMostRecentBrowserWindow(); + + const selectedBrowser = topAppWindow?.gBrowser?.selectedBrowser; + if (!selectedBrowser) { + // Note: gBrowser is not available on GeckoView. + // We should find another way to know if this browser is the selected + // browser. See Bug 1631020. + return false; + } + + return this._browser === selectedBrowser; + }, + + async getTarget() { + if (!this._conn) { + return { + error: "tabDestroyed", + message: "Tab destroyed while performing a TabDescriptorActor update", + }; + } + + /* eslint-disable-next-line no-async-promise-executor */ + return new Promise(async (resolve, reject) => { + const onDestroy = () => { + // Reject the update promise if the tab was destroyed while requesting an update + reject({ + error: "tabDestroyed", + message: "Tab destroyed while performing a TabDescriptorActor update", + }); + }; + + try { + // Check if the browser is still connected before calling connectToFrame + if (!this._browser.isConnected) { + onDestroy(); + return; + } + + const connectForm = await connectToFrame( + this._conn, + this._browser, + onDestroy + ); + + resolve(connectForm); + } catch (e) { + reject({ + error: "tabDestroyed", + message: "Tab destroyed while connecting to the frame", + }); + } + }); + }, + + /** + * Return a Watcher actor, allowing to keep track of targets which + * already exists or will be created. It also helps knowing when they + * are destroyed. + */ + getWatcher() { + if (!this.watcher) { + this.watcher = new WatcherActor(this.conn, { browser: this._browser }); + this.manage(this.watcher); + } + return this.watcher; + }, + + get _tabbrowser() { + if (this._browser && typeof this._browser.getTabBrowser == "function") { + return this._browser.getTabBrowser(); + } + return null; + }, + + async getFavicon() { + if (!AppConstants.MOZ_PLACES) { + // PlacesUtils is not supported + return null; + } + + try { + const { data } = await PlacesUtils.promiseFaviconData(this._getUrl()); + return data; + } catch (e) { + // Favicon unavailable for this url. + return null; + } + }, + + _isZombieTab() { + // Note: GeckoView doesn't support zombie tabs + const tabbrowser = this._tabbrowser; + const tab = tabbrowser ? tabbrowser.getTabForBrowser(this._browser) : null; + return tab?.hasAttribute && tab.hasAttribute("pending"); + }, + + destroy() { + this._browser = null; + + Actor.prototype.destroy.call(this); + }, +}); + +exports.TabDescriptorActor = TabDescriptorActor; diff --git a/devtools/server/actors/descriptors/webextension.js b/devtools/server/actors/descriptors/webextension.js new file mode 100644 index 0000000000..bd4079c676 --- /dev/null +++ b/devtools/server/actors/descriptors/webextension.js @@ -0,0 +1,252 @@ +/* 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/. */ + +"use strict"; + +/* + * Represents a WebExtension add-on in the parent process. This gives some metadata about + * the add-on and watches for uninstall events. This uses a proxy to access the + * WebExtension in the WebExtension process via the message manager. + * + * See devtools/docs/backend/actor-hierarchy.md for more details. + */ + +const protocol = require("devtools/shared/protocol"); +const { + webExtensionDescriptorSpec, +} = require("devtools/shared/specs/descriptors/webextension"); +const { + connectToFrame, +} = require("devtools/server/connectors/frame-connector"); + +loader.lazyImporter( + this, + "AddonManager", + "resource://gre/modules/AddonManager.jsm" +); +loader.lazyImporter( + this, + "ExtensionParent", + "resource://gre/modules/ExtensionParent.jsm" +); + +/** + * Creates the actor that represents the addon in the parent process, which connects + * itself to a WebExtensionTargetActor counterpart which is created in the extension + * process (or in the main process if the WebExtensions OOP mode is disabled). + * + * The WebExtensionDescriptorActor subscribes itself as an AddonListener on the AddonManager + * and forwards this events to child actor (e.g. on addon reload or when the addon is + * uninstalled completely) and connects to the child extension process using a `browser` + * element provided by the extension internals (it is not related to any single extension, + * but it will be created automatically to the currently selected "WebExtensions OOP mode" + * and it persist across the extension reloads (it is destroyed once the actor exits). + * WebExtensionDescriptorActor is a child of RootActor, it can be retrieved via + * RootActor.listAddons request. + * + * @param {DevToolsServerConnection} conn + * The connection to the client. + * @param {AddonWrapper} addon + * The target addon. + */ +const WebExtensionDescriptorActor = protocol.ActorClassWithSpec( + webExtensionDescriptorSpec, + { + initialize(conn, addon) { + protocol.Actor.prototype.initialize.call(this, conn); + this.addon = addon; + this.addonId = addon.id; + this._childFormPromise = null; + + // Called when the debug browser element has been destroyed + this._extensionFrameDisconnect = this._extensionFrameDisconnect.bind( + this + ); + this._onChildExit = this._onChildExit.bind(this); + AddonManager.addAddonListener(this); + }, + + form() { + const policy = ExtensionParent.WebExtensionPolicy.getByID(this.addonId); + return { + actor: this.actorID, + debuggable: this.addon.isDebuggable, + hidden: this.addon.hidden, + // iconDataURL is available after calling loadIconDataURL + iconDataURL: this._iconDataURL, + iconURL: this.addon.iconURL, + id: this.addonId, + isSystem: this.addon.isSystem, + isWebExtension: this.addon.isWebExtension, + manifestURL: policy && policy.getURL("manifest.json"), + name: this.addon.name, + temporarilyInstalled: this.addon.temporarilyInstalled, + traits: {}, + url: this.addon.sourceURI ? this.addon.sourceURI.spec : undefined, + warnings: ExtensionParent.DebugUtils.getExtensionManifestWarnings( + this.addonId + ), + }; + }, + + async getTarget() { + const form = await this._extensionFrameConnect(); + // Merge into the child actor form, some addon metadata + // (e.g. the addon name shown in the addon debugger window title). + return Object.assign(form, { + iconURL: this.addon.iconURL, + id: this.addon.id, + // Set the isOOP attribute on the connected child actor form. + isOOP: this.isOOP, + name: this.addon.name, + }); + }, + + getChildren() { + return []; + }, + + async _extensionFrameConnect() { + if (this._browser) { + throw new Error( + "This actor is already connected to the extension process" + ); + } + + this._browser = await ExtensionParent.DebugUtils.getExtensionProcessBrowser( + this + ); + + this._form = await connectToFrame( + this.conn, + this._browser, + this._extensionFrameDisconnect, + { addonId: this.addonId } + ); + + this._childActorID = this._form.actor; + + // Exit the proxy child actor if the child actor has been destroyed. + this._mm.addMessageListener("debug:webext_child_exit", this._onChildExit); + + return this._form; + }, + + /** WebExtension Actor Methods **/ + async reload() { + await this.addon.reload(); + return {}; + }, + + // This function will be called from RootActor in case that the devtools client + // retrieves list of addons with `iconDataURL` option. + async loadIconDataURL() { + this._iconDataURL = await this.getIconDataURL(); + }, + + async getIconDataURL() { + if (!this.addon.iconURL) { + return null; + } + + const xhr = new XMLHttpRequest(); + xhr.responseType = "blob"; + xhr.open("GET", this.addon.iconURL, true); + + if (this.addon.iconURL.toLowerCase().endsWith(".svg")) { + // Maybe SVG, thus force to change mime type. + xhr.overrideMimeType("image/svg+xml"); + } + + try { + const blob = await new Promise((resolve, reject) => { + xhr.onload = () => resolve(xhr.response); + xhr.onerror = reject; + xhr.send(); + }); + + const reader = new FileReader(); + return await new Promise((resolve, reject) => { + reader.onloadend = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + } catch (_) { + console.warn(`Failed to create data url from [${this.addon.iconURL}]`); + return null; + } + }, + + // TODO: check if we need this, as it is only used in a test + get isOOP() { + return this._browser ? this._browser.isRemoteBrowser : undefined; + }, + + // Private Methods + get _mm() { + return ( + this._browser && + (this._browser.messageManager || + this._browser.frameLoader.messageManager) + ); + }, + + _extensionFrameDisconnect() { + AddonManager.removeAddonListener(this); + + this.addon = null; + if (this._mm) { + this._mm.removeMessageListener( + "debug:webext_child_exit", + this._onChildExit + ); + + this._mm.sendAsyncMessage("debug:webext_parent_exit", { + actor: this._childActorID, + }); + + ExtensionParent.DebugUtils.releaseExtensionProcessBrowser(this); + } + + this._browser = null; + this._childActorID = null; + }, + + /** + * Handle the child actor exit. + */ + _onChildExit(msg) { + if (msg.json.actor !== this._childActorID) { + return; + } + + this._extensionFrameDisconnect(); + }, + + // AddonManagerListener callbacks. + onInstalled(addon) { + if (addon.id != this.addonId) { + return; + } + + // Update the AddonManager's addon object on reload/update. + this.addon = addon; + }, + + onUninstalled(addon) { + if (addon != this.addon) { + return; + } + + this._extensionFrameDisconnect(); + }, + + destroy() { + this._extensionFrameDisconnect(); + protocol.Actor.prototype.destroy.call(this); + }, + } +); + +exports.WebExtensionDescriptorActor = WebExtensionDescriptorActor; diff --git a/devtools/server/actors/descriptors/worker.js b/devtools/server/actors/descriptors/worker.js new file mode 100644 index 0000000000..e1f3275b53 --- /dev/null +++ b/devtools/server/actors/descriptors/worker.js @@ -0,0 +1,253 @@ +/* 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/. */ + +"use strict"; + +/* + * Target actor for any of the various kinds of workers. + * + * See devtools/docs/backend/actor-hierarchy.md for more details. + */ + +const { Ci } = require("chrome"); +const ChromeUtils = require("ChromeUtils"); +const { DevToolsServer } = require("devtools/server/devtools-server"); +const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm"); +const protocol = require("devtools/shared/protocol"); +const { + workerDescriptorSpec, +} = require("devtools/shared/specs/descriptors/worker"); + +loader.lazyRequireGetter(this, "ChromeUtils"); + +loader.lazyRequireGetter( + this, + "connectToWorker", + "devtools/server/connectors/worker-connector", + true +); + +XPCOMUtils.defineLazyServiceGetter( + this, + "swm", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager" +); + +const WorkerDescriptorActor = protocol.ActorClassWithSpec( + workerDescriptorSpec, + { + initialize(conn, dbg) { + protocol.Actor.prototype.initialize.call(this, conn); + this._dbg = dbg; + this._attached = false; + this._threadActor = null; + this._transport = null; + + this._dbgListener = { + onClose: this._onWorkerClose.bind(this), + onError: this._onWorkerError.bind(this), + }; + }, + + form() { + const form = { + actor: this.actorID, + consoleActor: this._consoleActor, + threadActor: this._threadActor, + id: this._dbg.id, + url: this._dbg.url, + traits: {}, + type: this._dbg.type, + }; + if (this._dbg.type === Ci.nsIWorkerDebugger.TYPE_SERVICE) { + /** + * With parent-intercept mode, the ServiceWorkerManager in content + * processes don't maintain ServiceWorkerRegistrations; record the + * ServiceWorker's ID, and this data will be merged with the + * corresponding registration in the parent process. + */ + if ( + !swm.isParentInterceptEnabled() || + !DevToolsServer.isInChildProcess + ) { + const registration = this._getServiceWorkerRegistrationInfo(); + form.scope = registration.scope; + const newestWorker = + registration.activeWorker || + registration.waitingWorker || + registration.installingWorker; + form.fetch = newestWorker?.handlesFetchEvents; + } + } + return form; + }, + + attach() { + if (this._dbg.isClosed) { + return { error: "closed" }; + } + + if (!this._attached) { + const isServiceWorker = + this._dbg.type == Ci.nsIWorkerDebugger.TYPE_SERVICE; + if (isServiceWorker) { + this._preventServiceWorkerShutdown(); + } + this._dbg.addListener(this._dbgListener); + this._attached = true; + } + + return { + type: "attached", + url: this._dbg.url, + }; + }, + + detach() { + if (!this._attached) { + return { error: "wrongState" }; + } + + this._detach(); + + return { type: "detached" }; + }, + + destroy() { + if (this._attached) { + this._detach(); + } + protocol.Actor.prototype.destroy.call(this); + }, + + async getTarget() { + if (!this._attached) { + return { error: "wrongState" }; + } + + if (this._threadActor !== null) { + return { + type: "connected", + threadActor: this._threadActor, + }; + } + + try { + const { transport, workerTargetForm } = await connectToWorker( + this.conn, + this._dbg, + this.actorID, + {} + ); + + this._threadActor = workerTargetForm.threadActor; + this._consoleActor = workerTargetForm.consoleActor; + this._transport = transport; + + return { + type: "connected", + threadActor: this._threadActor, + consoleActor: this._consoleActor, + url: this._dbg.url, + }; + } catch (error) { + return { error: error.toString() }; + } + }, + + push() { + if (this._dbg.type !== Ci.nsIWorkerDebugger.TYPE_SERVICE) { + return { error: "wrongType" }; + } + const registration = this._getServiceWorkerRegistrationInfo(); + const originAttributes = ChromeUtils.originAttributesToSuffix( + this._dbg.principal.originAttributes + ); + swm.sendPushEvent(originAttributes, registration.scope); + return { type: "pushed" }; + }, + + _onWorkerClose() { + if (this._attached) { + this._detach(); + } + + this.conn.sendActorEvent(this.actorID, "close"); + }, + + _onWorkerError(filename, lineno, message) { + reportError("ERROR:" + filename + ":" + lineno + ":" + message + "\n"); + }, + + _getServiceWorkerRegistrationInfo() { + return swm.getRegistrationByPrincipal(this._dbg.principal, this._dbg.url); + }, + + _getServiceWorkerInfo() { + const registration = this._getServiceWorkerRegistrationInfo(); + return registration.getWorkerByID(this._dbg.serviceWorkerID); + }, + + _detach() { + if (this._threadActor !== null) { + this._transport.close(); + this._transport = null; + this._threadActor = null; + } + + // If the worker is already destroyed, nsIWorkerDebugger.type throws + // (_dbg.closed appears to be false when it throws) + let type; + try { + type = this._dbg.type; + } catch (e) { + // nothing + } + + const isServiceWorker = type == Ci.nsIWorkerDebugger.TYPE_SERVICE; + if (isServiceWorker) { + this._allowServiceWorkerShutdown(); + } + + this._dbg.removeListener(this._dbgListener); + this._attached = false; + }, + + /** + * Automatically disable the internal sw timeout that shut them down by calling + * nsIWorkerInfo.attachDebugger(). + * This can be removed when Bug 1496997 lands. + */ + _preventServiceWorkerShutdown() { + if (swm.isParentInterceptEnabled()) { + // In parentIntercept mode, the worker target actor cannot call attachDebugger + // because this API can only be called from the parent process. This will be + // done by the worker target front. + return; + } + + const worker = this._getServiceWorkerInfo(); + if (worker) { + worker.attachDebugger(); + } + }, + + /** + * Allow the service worker to time out. See _preventServiceWorkerShutdown. + */ + _allowServiceWorkerShutdown() { + if (swm.isParentInterceptEnabled()) { + return; + } + + const worker = this._getServiceWorkerInfo(); + if (worker) { + worker.detachDebugger(); + } + }, + } +); + +exports.WorkerDescriptorActor = WorkerDescriptorActor; |