summaryrefslogtreecommitdiffstats
path: root/devtools/client/fronts/targets
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/fronts/targets')
-rw-r--r--devtools/client/fronts/targets/content-process.js57
-rw-r--r--devtools/client/fronts/targets/moz.build12
-rw-r--r--devtools/client/fronts/targets/target-mixin.js630
-rw-r--r--devtools/client/fronts/targets/window-global.js172
-rw-r--r--devtools/client/fronts/targets/worker.js33
5 files changed, 904 insertions, 0 deletions
diff --git a/devtools/client/fronts/targets/content-process.js b/devtools/client/fronts/targets/content-process.js
new file mode 100644
index 0000000000..7629cb8449
--- /dev/null
+++ b/devtools/client/fronts/targets/content-process.js
@@ -0,0 +1,57 @@
+/* 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 {
+ contentProcessTargetSpec,
+} = require("resource://devtools/shared/specs/targets/content-process.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ TargetMixin,
+} = require("resource://devtools/client/fronts/targets/target-mixin.js");
+
+class ContentProcessTargetFront extends TargetMixin(
+ FrontClassWithSpec(contentProcessTargetSpec)
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this.traits = {};
+ }
+
+ form(json) {
+ this.actorID = json.actor;
+ this.processID = json.processID;
+
+ // Save the full form for Target class usage.
+ // Do not use `form` name to avoid colliding with protocol.js's `form` method
+ this.targetForm = json;
+
+ this.remoteType = json.remoteType;
+ this.isXpcShellTarget = json.isXpcShellTarget;
+ }
+
+ get name() {
+ // @backward-compat { version 87 } We now have `remoteType` attribute.
+ if (this.remoteType) {
+ return `(pid ${this.processID}) ${this.remoteType.replace(
+ "webIsolated=",
+ ""
+ )}`;
+ }
+ return `(pid ${this.processID}) Content Process`;
+ }
+
+ reconfigure() {
+ // Toolbox and options panel are calling this method but Worker Target can't be
+ // reconfigured. So we ignore this call here.
+ return Promise.resolve();
+ }
+}
+
+exports.ContentProcessTargetFront = ContentProcessTargetFront;
+registerFront(exports.ContentProcessTargetFront);
diff --git a/devtools/client/fronts/targets/moz.build b/devtools/client/fronts/targets/moz.build
new file mode 100644
index 0000000000..82b82f24a7
--- /dev/null
+++ b/devtools/client/fronts/targets/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(
+ "content-process.js",
+ "target-mixin.js",
+ "window-global.js",
+ "worker.js",
+)
diff --git a/devtools/client/fronts/targets/target-mixin.js b/devtools/client/fronts/targets/target-mixin.js
new file mode 100644
index 0000000000..157e83e73a
--- /dev/null
+++ b/devtools/client/fronts/targets/target-mixin.js
@@ -0,0 +1,630 @@
+/* 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";
+
+loader.lazyRequireGetter(
+ this,
+ "getFront",
+ "resource://devtools/shared/protocol.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "getThreadOptions",
+ "resource://devtools/client/shared/thread-utils.js",
+ true
+);
+
+/**
+ * A Target represents a debuggable context. It can be a browser tab, a tab on
+ * a remote device, like a tab on Firefox for Android. But it can also be an add-on,
+ * as well as firefox parent process, or just one of its content process.
+ * A Target is related to a given TargetActor, for which we derive this class.
+ *
+ * Providing a generalized abstraction of a web-page or web-browser (available
+ * either locally or remotely) is beyond the scope of this class (and maybe
+ * also beyond the scope of this universe) However Target does attempt to
+ * abstract some common events and read-only properties common to many Tools.
+ *
+ * Supported read-only properties:
+ * - name, url
+ *
+ * Target extends EventEmitter and provides support for the following events:
+ * - close: The target window has been closed. All tools attached to this
+ * target should close. This event is not currently cancelable.
+ *
+ * Optional events only dispatched by WindowGlobalTarget:
+ * - will-navigate: The target window will navigate to a different URL
+ * - navigate: The target window has navigated to a different URL
+ */
+function TargetMixin(parentClass) {
+ class Target extends parentClass {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // TargetCommand._onTargetAvailable will set this public attribute.
+ // This is a reference to the related `commands` object and helps all fronts
+ // easily call any command method. Without this bit of magic, Fronts wouldn't
+ // be able to interact with any commands while it is frequently useful.
+ this.commands = null;
+
+ this.destroy = this.destroy.bind(this);
+
+ this.threadFront = null;
+
+ this._client = client;
+
+ // Cache of already created targed-scoped fronts
+ // [typeName:string => Front instance]
+ this.fronts = new Map();
+
+ // `resource-available-form` and `resource-updated-form` events can be emitted
+ // by target actors before the ResourceCommand could add event listeners.
+ // The target front will cache those events until the ResourceCommand has
+ // added the listeners.
+ this._resourceCache = {};
+
+ // In order to avoid destroying the `_resourceCache[event]`, we need to call `super.on()`
+ // instead of `this.on()`.
+ const offResourceAvailable = super.on(
+ "resource-available-form",
+ this._onResourceEvent.bind(this, "resource-available-form")
+ );
+ const offResourceUpdated = super.on(
+ "resource-updated-form",
+ this._onResourceEvent.bind(this, "resource-updated-form")
+ );
+
+ this._offResourceEvent = new Map([
+ ["resource-available-form", offResourceAvailable],
+ ["resource-updated-form", offResourceUpdated],
+ ]);
+
+ // Expose a promise that is resolved once the target front is usable
+ // i.e. once attachAndInitThread has been called and resolved.
+ this.initialized = new Promise(resolve => {
+ this._onInitialized = resolve;
+ });
+ }
+
+ on(eventName, listener) {
+ if (this._offResourceEvent && this._offResourceEvent.has(eventName)) {
+ // If a callsite sets an event listener for resource-(available|update)-form:
+
+ // we want to remove the listener we set here in the constructor…
+ const off = this._offResourceEvent.get(eventName);
+ this._offResourceEvent.delete(eventName);
+ off();
+
+ // …and call the new listener with the resources that were put in the cache.
+ if (this._resourceCache[eventName]) {
+ for (const cache of this._resourceCache[eventName]) {
+ listener(cache);
+ }
+ delete this._resourceCache[eventName];
+ }
+ }
+
+ return super.on(eventName, listener);
+ }
+
+ /**
+ * Boolean flag to help distinguish Target Fronts from other Fronts.
+ * As we are using a Mixin, we can't easily distinguish these fronts via instanceof().
+ */
+ get isTargetFront() {
+ return true;
+ }
+
+ get targetType() {
+ return this._targetType;
+ }
+
+ get isTopLevel() {
+ // We can't use `getTrait` here as this might be called from a destroyed target (e.g.
+ // from an onTargetDestroyed callback that was triggered by a legacy listener), which
+ // means `this.client` would be null, which would make `getTrait` throw (See Bug 1714974)
+ if (!this.targetForm.hasOwnProperty("isTopLevelTarget")) {
+ return !!this._isTopLevel;
+ }
+
+ return this.targetForm.isTopLevelTarget;
+ }
+
+ setTargetType(type) {
+ this._targetType = type;
+ }
+
+ setIsTopLevel(isTopLevel) {
+ if (!this.getTrait("supportsTopLevelTargetFlag")) {
+ this._isTopLevel = isTopLevel;
+ }
+ }
+
+ /**
+ * Get the immediate parent target for this target.
+ *
+ * @return {TargetMixin} the parent target.
+ */
+ async getParentTarget() {
+ return this.commands.targetCommand.getParentTarget(this);
+ }
+
+ /**
+ * Returns a Promise that resolves to a boolean indicating if the provided target is
+ * an ancestor of this instance.
+ *
+ * @param {TargetFront} target: The possible ancestor target.
+ * @returns Promise<Boolean>
+ */
+ async isTargetAnAncestor(target) {
+ const parentTargetFront = await this.getParentTarget();
+ if (!parentTargetFront) {
+ return false;
+ }
+
+ if (parentTargetFront == target) {
+ return true;
+ }
+
+ return parentTargetFront.isTargetAnAncestor(target);
+ }
+
+ /**
+ * Get the target for the given Browsing Context ID.
+ *
+ * @return {TargetMixin} the requested target.
+ */
+ async getWindowGlobalTarget(browsingContextID) {
+ // Just for sanity as commands attribute is set late from TargetCommand._onTargetAvailable
+ // but ideally target front should be used before this happens.
+ if (!this.commands) {
+ return null;
+ }
+ // Tab and Process Descriptors expose a Watcher, which is creating the
+ // targets and should be used to fetch any.
+ const { watcherFront } = this.commands;
+ if (watcherFront) {
+ // Safety check, in theory all watcher should support frames.
+ if (watcherFront.traits.frame) {
+ return watcherFront.getWindowGlobalTarget(browsingContextID);
+ }
+ return null;
+ }
+
+ // For descriptors which don't expose a watcher (e.g. WebExtension)
+ // we used to call RootActor::getBrowsingContextDescriptor, but it was
+ // removed in FF77.
+ // Support for watcher in WebExtension descriptors is Bug 1644341.
+ throw new Error(
+ `Unable to call getWindowGlobalTarget for ${this.actorID}`
+ );
+ }
+
+ /**
+ * Returns a boolean indicating whether or not the specific actor
+ * type exists.
+ *
+ * @param {String} actorName
+ * @return {Boolean}
+ */
+ hasActor(actorName) {
+ if (this.targetForm) {
+ return !!this.targetForm[actorName + "Actor"];
+ }
+ return false;
+ }
+
+ /**
+ * Returns a trait from the target actor if it exists,
+ * if not it will fallback to that on the root actor.
+ *
+ * @param {String} traitName
+ * @return {Mixed}
+ */
+ getTrait(traitName) {
+ // If the targeted actor exposes traits and has a defined value for this
+ // traits, override the root actor traits
+ if (this.targetForm.traits && traitName in this.targetForm.traits) {
+ return this.targetForm.traits[traitName];
+ }
+
+ return this.client.traits[traitName];
+ }
+
+ // Get a Front for a target-scoped actor.
+ // i.e. an actor served by RootActor.listTabs or RootActorActor.getTab requests
+ async getFront(typeName) {
+ if (this.isDestroyed()) {
+ throw new Error(
+ "Target already destroyed, unable to fetch children fronts"
+ );
+ }
+ let front = this.fronts.get(typeName);
+ if (front) {
+ // XXX: This is typically the kind of spot where switching to
+ // `isDestroyed()` is complicated, because `front` is not necessarily a
+ // Front...
+ const isFrontInitializing = typeof front.then === "function";
+ const isFrontAlive = !isFrontInitializing && !front.isDestroyed();
+ if (isFrontInitializing || isFrontAlive) {
+ return front;
+ }
+ }
+
+ front = getFront(this.client, typeName, this.targetForm, this);
+ this.fronts.set(typeName, front);
+ // replace the placeholder with the instance of the front once it has loaded
+ front = await front;
+ this.fronts.set(typeName, front);
+ return front;
+ }
+
+ getCachedFront(typeName) {
+ // do not wait for async fronts;
+ const front = this.fronts.get(typeName);
+ // ensure that the front is a front, and not async front
+ if (front?.actorID) {
+ return front;
+ }
+ return null;
+ }
+
+ get client() {
+ return this._client;
+ }
+
+ // Tells us if the related actor implements WindowGlobalTargetActor
+ // interface and requires to call `attach` request before being used and
+ // `detach` during cleanup.
+ get isBrowsingContext() {
+ return this.typeName === "windowGlobalTarget";
+ }
+
+ get name() {
+ if (this.isWebExtension || this.isContentProcess) {
+ return this.targetForm.name;
+ }
+ return this.title;
+ }
+
+ get title() {
+ return this._title || this.url;
+ }
+
+ get url() {
+ return this._url;
+ }
+
+ get isWorkerTarget() {
+ // XXX Remove the check on `workerDescriptor` as part of Bug 1667404.
+ return (
+ this.typeName === "workerTarget" || this.typeName === "workerDescriptor"
+ );
+ }
+
+ get isWebExtension() {
+ return !!(
+ this.targetForm &&
+ this.targetForm.actor &&
+ (this.targetForm.actor.match(/conn\d+\.webExtension(Target)?\d+/) ||
+ this.targetForm.actor.match(/child\d+\/webExtension(Target)?\d+/))
+ );
+ }
+
+ get isContentProcess() {
+ // browser content toolbox's form will be of the form:
+ // server0.conn0.content-process0/contentProcessTarget7
+ // while xpcshell debugging will be:
+ // server1.conn0.contentProcessTarget7
+ return !!(
+ this.targetForm &&
+ this.targetForm.actor &&
+ this.targetForm.actor.match(
+ /conn\d+\.(content-process\d+\/)?contentProcessTarget\d+/
+ )
+ );
+ }
+
+ get isParentProcess() {
+ return !!(
+ this.targetForm &&
+ this.targetForm.actor &&
+ this.targetForm.actor.match(/conn\d+\.parentProcessTarget\d+/)
+ );
+ }
+
+ getExtensionPathName(url) {
+ // Return the url if the target is not a webextension.
+ if (!this.isWebExtension) {
+ throw new Error("Target is not a WebExtension");
+ }
+
+ try {
+ const parsedURL = new URL(url);
+ // Only moz-extension URL should be shortened into the URL pathname.
+ if (parsedURL.protocol !== "moz-extension:") {
+ return url;
+ }
+ return parsedURL.pathname;
+ } catch (e) {
+ // Return the url if unable to resolve the pathname.
+ return url;
+ }
+ }
+
+ /**
+ * This method attaches the target and then attaches its related thread, sending it
+ * the options it needs (e.g. breakpoints, pause on exception setting, …).
+ * This function can be called multiple times, it will only perform the actual
+ * initialization process once; on subsequent call the original promise (_onThreadInitialized)
+ * will be returned.
+ *
+ * @param {TargetCommand} targetCommand
+ * @returns {Promise} A promise that resolves once the thread is attached and resumed.
+ */
+ attachAndInitThread(targetCommand) {
+ if (this._onThreadInitialized) {
+ return this._onThreadInitialized;
+ }
+
+ this._onThreadInitialized = this._attachAndInitThread(targetCommand);
+ // Resolve the `initialized` promise, while ignoring errors
+ // The empty function passed to catch will avoid spawning a new possibly rejected promise
+ this._onThreadInitialized.catch(() => {}).then(this._onInitialized);
+ return this._onThreadInitialized;
+ }
+
+ /**
+ * This method attach the target and then attach its related thread, sending it the
+ * options it needs (e.g. breakpoints, pause on exception setting, …)
+ *
+ * @private
+ * @param {TargetCommand} targetCommand
+ * @returns {Promise} A promise that resolves once the thread is attached and resumed.
+ */
+ async _attachAndInitThread(targetCommand) {
+ // If the target is destroyed or soon will be, don't go further
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return;
+ }
+
+ // The current class we have is actually the WorkerDescriptorFront,
+ // which will morph into a target by fetching the underlying target's form.
+ // Ideally, worker targets would be spawn by the server, and we would no longer
+ // have the hybrid descriptor/target class which brings lots of complexity and confusion.
+ // To be removed in bug 1651522.
+ if (this.morphWorkerDescriptorIntoWorkerTarget) {
+ await this.morphWorkerDescriptorIntoWorkerTarget();
+ }
+
+ const isBrowserToolbox =
+ targetCommand.descriptorFront.isBrowserProcessDescriptor;
+ const isNonTopLevelFrameTarget =
+ !this.isTopLevel && this.targetType === targetCommand.TYPES.FRAME;
+
+ if (isBrowserToolbox && isNonTopLevelFrameTarget) {
+ // In the BrowserToolbox, non-top-level frame targets are already
+ // debugged via content-process targets.
+ // Do not attach the thread here, as it was already done by the
+ // corresponding content-process target.
+ return;
+ }
+
+ // Avoid attaching any thread actor in the browser console or in
+ // webextension commands in order to avoid triggering any type of
+ // breakpoint.
+ if (targetCommand.descriptorFront.doNotAttachThreadActor) {
+ return;
+ }
+
+ const options = await getThreadOptions();
+ // If the target is destroyed or soon will be, don't go further
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return;
+ }
+ await this.attachThread(options);
+ }
+
+ async attachThread(options = {}) {
+ if (!this.targetForm || !this.targetForm.threadActor) {
+ throw new Error(
+ "TargetMixin sub class should set targetForm.threadActor before calling " +
+ "attachThread"
+ );
+ }
+ this.threadFront = await this.getFront("thread");
+
+ // Avoid attaching if the thread actor was already attached on target creation from the server side.
+ // This doesn't include:
+ // * targets that aren't yet supported by the Watcher (like web extensions),
+ // * workers, which still use a unique codepath for thread actor attach
+ // * all targets when connecting to an older server
+ // If all targets are supported by watcher actor, and workers no longer use
+ // its unique attach sequence, we can assume the thread front is always attached.
+ const isAttached = await this.threadFront.isAttached();
+
+ const isDestroyed =
+ this.isDestroyedOrBeingDestroyed() || this.threadFront.isDestroyed();
+ if (!isAttached && !isDestroyed) {
+ await this.threadFront.attach(options);
+ }
+
+ return this.threadFront;
+ }
+
+ isDestroyedOrBeingDestroyed() {
+ return this.isDestroyed() || this._destroyer;
+ }
+
+ /**
+ * Target is not alive anymore.
+ */
+ destroy() {
+ // If several things call destroy then we give them all the same
+ // destruction promise so we're sure to destroy only once
+ if (this._destroyer) {
+ return this._destroyer;
+ }
+
+ // This pattern allows to immediately return the destroyer promise.
+ // See Bug 1602727 for more details.
+ let destroyerResolve;
+ this._destroyer = new Promise(r => (destroyerResolve = r));
+ this._destroyTarget().then(destroyerResolve);
+
+ return this._destroyer;
+ }
+
+ async _destroyTarget() {
+ // If the target is being attached, try to wait until it's done, to prevent having
+ // pending connection to the server when the toolbox is destroyed.
+ if (this._onThreadInitialized) {
+ try {
+ await this._onThreadInitialized;
+ } catch (e) {
+ // We might still get into cases where attaching fails (e.g. the worker we're
+ // trying to attach to is already closed). Since the target is being destroyed,
+ // we don't need to do anything special here.
+ }
+ }
+
+ for (let [name, front] of this.fronts) {
+ try {
+ // If a Front with an async initialize method is still being instantiated,
+ // we should wait for completion before trying to destroy it.
+ if (front instanceof Promise) {
+ front = await front;
+ }
+ front.destroy();
+ } catch (e) {
+ console.warn("Error while destroying front:", name, e);
+ }
+ }
+ this.fronts.clear();
+
+ this.threadFront = null;
+ this._offResourceEvent = null;
+
+ // This event should be emitted before calling super.destroy(), because
+ // super.destroy() will remove all event listeners attached to this front.
+ this.emit("target-destroyed");
+
+ // Not all targets supports attach/detach. For example content process doesn't.
+ // Also ensure that the front is still active before trying to do the request.
+ if (this.detach && !this.isDestroyed()) {
+ // The client was handed to us, so we are not responsible for closing
+ // it. We just need to detach from the tab, if already attached.
+ // |detach| may fail if the connection is already dead, so proceed with
+ // cleanup directly after this.
+ try {
+ await this.detach();
+ } catch (e) {
+ this.logDetachError(e);
+ }
+ }
+
+ // Do that very last in order to let a chance to dispatch `detach` requests.
+ super.destroy();
+
+ this._cleanup();
+ }
+
+ /**
+ * Detach can fail under regular circumstances, if the target was already
+ * destroyed on the server side. All target fronts should handle detach
+ * error logging in similar ways so this might be used by subclasses
+ * with custom detach() implementations.
+ *
+ * @param {Error} e
+ * The real error object.
+ * @param {String} targetType
+ * The type of the target front ("worker", "browsing-context", ...)
+ */
+ logDetachError(e, targetType) {
+ const ignoredError =
+ e?.message.includes("noSuchActor") ||
+ e?.message.includes("Connection closed");
+
+ // Silence exceptions for already destroyed actors and fronts:
+ // - "noSuchActor" errors from the server
+ // - "Connection closed" errors from the client, when purging requests
+ if (ignoredError) {
+ return;
+ }
+
+ // Properly log any other error.
+ const message = targetType
+ ? `Error while detaching the ${targetType} target:`
+ : "Error while detaching target:";
+ console.warn(message, e);
+ }
+
+ /**
+ * Clean up references to what this target points to.
+ */
+ _cleanup() {
+ this.threadFront = null;
+ this._client = null;
+
+ this._title = null;
+ this._url = null;
+ }
+
+ _onResourceEvent(eventName, resources) {
+ if (!this._resourceCache[eventName]) {
+ this._resourceCache[eventName] = [];
+ }
+ this._resourceCache[eventName].push(resources);
+ }
+
+ toString() {
+ const id = this.targetForm ? this.targetForm.actor : null;
+ return `Target:${id}`;
+ }
+
+ dumpPools() {
+ // NOTE: dumpPools is defined in the Thread actor to avoid
+ // adding it to multiple target specs and actors.
+ return this.threadFront.dumpPools();
+ }
+
+ /**
+ * Log an error of some kind to the tab's console.
+ *
+ * @param {String} text
+ * The text to log.
+ * @param {String} category
+ * The category of the message. @see nsIScriptError.
+ * @returns {Promise}
+ */
+ logErrorInPage(text, category) {
+ if (this.getTrait("logInPage")) {
+ const errorFlag = 0;
+ return this.logInPage({ text, category, flags: errorFlag });
+ }
+ return Promise.resolve();
+ }
+
+ /**
+ * Log a warning of some kind to the tab's console.
+ *
+ * @param {String} text
+ * The text to log.
+ * @param {String} category
+ * The category of the message. @see nsIScriptError.
+ * @returns {Promise}
+ */
+ logWarningInPage(text, category) {
+ if (this.getTrait("logInPage")) {
+ const warningFlag = 1;
+ return this.logInPage({ text, category, flags: warningFlag });
+ }
+ return Promise.resolve();
+ }
+ }
+ return Target;
+}
+exports.TargetMixin = TargetMixin;
diff --git a/devtools/client/fronts/targets/window-global.js b/devtools/client/fronts/targets/window-global.js
new file mode 100644
index 0000000000..8bf69383e0
--- /dev/null
+++ b/devtools/client/fronts/targets/window-global.js
@@ -0,0 +1,172 @@
+/* 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 {
+ windowGlobalTargetSpec,
+} = require("resource://devtools/shared/specs/targets/window-global.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ TargetMixin,
+} = require("resource://devtools/client/fronts/targets/target-mixin.js");
+
+class WindowGlobalTargetFront extends TargetMixin(
+ FrontClassWithSpec(windowGlobalTargetSpec)
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // For targets which support the Watcher and configuration actor, the status
+ // for the `javascriptEnabled` setting will be available on the configuration
+ // front, and the target will only be used to read the initial value from older
+ // servers.
+ // Note: this property is marked as private but is accessed by the
+ // TargetCommand to provide the "isJavascriptEnabled" wrapper. It should NOT be
+ // used anywhere else.
+ this._javascriptEnabled = null;
+
+ // If this target was retrieved via NodeFront connectToFrame, keep a
+ // reference to the parent NodeFront.
+ this._parentNodeFront = null;
+
+ this._onTabNavigated = this._onTabNavigated.bind(this);
+ this._onFrameUpdate = this._onFrameUpdate.bind(this);
+
+ this.on("tabNavigated", this._onTabNavigated);
+ this.on("frameUpdate", this._onFrameUpdate);
+ }
+
+ form(json) {
+ this.actorID = json.actor;
+ this.browsingContextID = json.browsingContextID;
+ this.innerWindowId = json.innerWindowId;
+ this.processID = json.processID;
+
+ // Save the full form for Target class usage.
+ // Do not use `form` name to avoid colliding with protocol.js's `form` method
+ this.targetForm = json;
+
+ this.outerWindowID = json.outerWindowID;
+ this.favicon = json.favicon;
+
+ // Initial value for the page title and url. Since the WindowGlobalTargetActor can
+ // be created very early, those might not represent the actual value we'd want to
+ // display for the user (e.g. the <title> might not have been parsed yet, and the
+ // url could still be about:blank, which is what the platform uses at the very start
+ // of a navigation to a new location).
+ // Those values are set again from the targetCommand when receiving DOCUMENT_EVENT
+ // resource, at which point both values should be in their expected form.
+ this.setTitle(json.title);
+ this.setUrl(json.url);
+ }
+
+ /**
+ * Event listener for `frameUpdate` event.
+ */
+ _onFrameUpdate(packet) {
+ this.emit("frame-update", packet);
+ }
+
+ /**
+ * Event listener for `tabNavigated` event.
+ */
+ _onTabNavigated(packet) {
+ const event = Object.create(null);
+ event.url = packet.url;
+ event.title = packet.title;
+ event.isFrameSwitching = packet.isFrameSwitching;
+
+ // Keep the title unmodified when a developer toolbox switches frame
+ // for a tab (Bug 1261687), but always update the title when the target
+ // is a WebExtension (where the addon name is always included in the title
+ // and the url is supposed to be updated every time the selected frame changes).
+ if (!packet.isFrameSwitching || this.isWebExtension) {
+ this.setTitle(packet.title);
+ this.setUrl(packet.url);
+ }
+
+ // Send any stored event payload (DOMWindow or nsIRequest) for backwards
+ // compatibility with non-remotable tools.
+ if (packet.state == "start") {
+ this.emit("will-navigate", event);
+ } else {
+ this.emit("navigate", event);
+ }
+ }
+
+ getParentNodeFront() {
+ return this._parentNodeFront;
+ }
+
+ setParentNodeFront(nodeFront) {
+ this._parentNodeFront = nodeFront;
+ }
+
+ /**
+ * Set the targetFront url.
+ *
+ * @param {string} url
+ */
+ setUrl(url) {
+ this._url = url;
+ }
+
+ /**
+ * Set the targetFront title.
+ *
+ * @param {string} title
+ */
+ setTitle(title) {
+ this._title = title;
+ }
+
+ async detach() {
+ // When calling this.destroy() at the end of this method,
+ // we will end up calling detach again from TargetMixin.destroy.
+ // Avoid invalid loops and do not try to resolve only once the previous call to detach
+ // is done as it would do async infinite loop that never resolves.
+ if (this._isDetaching) {
+ return;
+ }
+ this._isDetaching = true;
+
+ // Remove listeners set in constructor
+ this.off("tabNavigated", this._onTabNavigated);
+ this.off("frameUpdate", this._onFrameUpdate);
+
+ try {
+ await super.detach();
+ } catch (e) {
+ this.logDetachError(e, "browsing context");
+ }
+
+ // Detach will destroy the target actor, but the server won't emit any
+ // target-destroyed-form in such manual, client side destruction.
+ // So that we have to manually destroy the associated front on the client
+ //
+ // If detach was called by TargetFrontMixin.destroy, avoid recalling it from it
+ // as it would do an async infinite loop which would never resolve.
+ if (!this.isDestroyedOrBeingDestroyed()) {
+ this.destroy();
+ }
+ }
+
+ destroy() {
+ const promise = super.destroy();
+ this._parentNodeFront = null;
+
+ // As detach isn't necessarily called on target's destroy
+ // (it isn't for local tabs), ensure removing listeners set in constructor.
+ this.off("tabNavigated", this._onTabNavigated);
+ this.off("frameUpdate", this._onFrameUpdate);
+
+ return promise;
+ }
+}
+
+exports.WindowGlobalTargetFront = WindowGlobalTargetFront;
+registerFront(exports.WindowGlobalTargetFront);
diff --git a/devtools/client/fronts/targets/worker.js b/devtools/client/fronts/targets/worker.js
new file mode 100644
index 0000000000..3ca053d468
--- /dev/null
+++ b/devtools/client/fronts/targets/worker.js
@@ -0,0 +1,33 @@
+/* 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 {
+ workerTargetSpec,
+} = require("resource://devtools/shared/specs/targets/worker.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ TargetMixin,
+} = require("resource://devtools/client/fronts/targets/target-mixin.js");
+
+class WorkerTargetFront extends TargetMixin(
+ FrontClassWithSpec(workerTargetSpec)
+) {
+ form(json) {
+ this.actorID = json.actor;
+
+ // Save the full form for Target class usage.
+ // Do not use `form` name to avoid colliding with protocol.js's `form` method
+ this.targetForm = json;
+
+ this._title = json.title;
+ this._url = json.url;
+ }
+}
+
+exports.WorkerTargetFront = WorkerTargetFront;
+registerFront(exports.WorkerTargetFront);