summaryrefslogtreecommitdiffstats
path: root/devtools/client/fronts
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/fronts')
-rw-r--r--devtools/client/fronts/accessibility.js583
-rw-r--r--devtools/client/fronts/addon/addons.js25
-rw-r--r--devtools/client/fronts/addon/moz.build10
-rw-r--r--devtools/client/fronts/addon/webextension-inspected-window.js31
-rw-r--r--devtools/client/fronts/animation.js214
-rw-r--r--devtools/client/fronts/array-buffer.js26
-rw-r--r--devtools/client/fronts/blackboxing.js17
-rw-r--r--devtools/client/fronts/breakpoint-list.js17
-rw-r--r--devtools/client/fronts/changes.js33
-rw-r--r--devtools/client/fronts/compatibility.js18
-rw-r--r--devtools/client/fronts/css-properties.js278
-rw-r--r--devtools/client/fronts/descriptors/descriptor-mixin.js60
-rw-r--r--devtools/client/fronts/descriptors/descriptor-types.js17
-rw-r--r--devtools/client/fronts/descriptors/moz.build14
-rw-r--r--devtools/client/fronts/descriptors/process.js142
-rw-r--r--devtools/client/fronts/descriptors/tab.js330
-rw-r--r--devtools/client/fronts/descriptors/webextension.js173
-rw-r--r--devtools/client/fronts/descriptors/worker.js146
-rw-r--r--devtools/client/fronts/device.js23
-rw-r--r--devtools/client/fronts/frame.js27
-rw-r--r--devtools/client/fronts/highlighters.js53
-rw-r--r--devtools/client/fronts/inspector.js282
-rw-r--r--devtools/client/fronts/inspector/moz.build9
-rw-r--r--devtools/client/fronts/inspector/rule-rewriter.js745
-rw-r--r--devtools/client/fronts/layout.js184
-rw-r--r--devtools/client/fronts/manifest.js25
-rw-r--r--devtools/client/fronts/memory.js116
-rw-r--r--devtools/client/fronts/moz.build58
-rw-r--r--devtools/client/fronts/network-content.js24
-rw-r--r--devtools/client/fronts/network-parent.js27
-rw-r--r--devtools/client/fronts/node.js629
-rw-r--r--devtools/client/fronts/object.js465
-rw-r--r--devtools/client/fronts/page-style.js123
-rw-r--r--devtools/client/fronts/perf.js22
-rw-r--r--devtools/client/fronts/preference.js33
-rw-r--r--devtools/client/fronts/private-properties-iterator.js60
-rw-r--r--devtools/client/fronts/property-iterator.js67
-rw-r--r--devtools/client/fronts/reflow.js31
-rw-r--r--devtools/client/fronts/responsive.js28
-rw-r--r--devtools/client/fronts/root.js332
-rw-r--r--devtools/client/fronts/screenshot-content.js25
-rw-r--r--devtools/client/fronts/screenshot.js25
-rw-r--r--devtools/client/fronts/source.js102
-rw-r--r--devtools/client/fronts/storage.js55
-rw-r--r--devtools/client/fronts/string.js62
-rw-r--r--devtools/client/fronts/style-rule.js281
-rw-r--r--devtools/client/fronts/style-sheets.js37
-rw-r--r--devtools/client/fronts/symbol-iterator.js56
-rw-r--r--devtools/client/fronts/target-configuration.js33
-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
-rw-r--r--devtools/client/fronts/thread-configuration.js25
-rw-r--r--devtools/client/fronts/thread.js285
-rw-r--r--devtools/client/fronts/tracer.js22
-rw-r--r--devtools/client/fronts/walker.js461
-rw-r--r--devtools/client/fronts/watcher.js202
-rw-r--r--devtools/client/fronts/webconsole.js315
-rw-r--r--devtools/client/fronts/worker/moz.build11
-rw-r--r--devtools/client/fronts/worker/push-subscription.js30
-rw-r--r--devtools/client/fronts/worker/service-worker-registration.js79
-rw-r--r--devtools/client/fronts/worker/service-worker.js62
64 files changed, 8569 insertions, 0 deletions
diff --git a/devtools/client/fronts/accessibility.js b/devtools/client/fronts/accessibility.js
new file mode 100644
index 0000000000..3a60856164
--- /dev/null
+++ b/devtools/client/fronts/accessibility.js
@@ -0,0 +1,583 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ accessibleSpec,
+ accessibleWalkerSpec,
+ accessibilitySpec,
+ parentAccessibilitySpec,
+ simulatorSpec,
+} = require("resource://devtools/shared/specs/accessibility.js");
+const events = require("resource://devtools/shared/event-emitter.js");
+
+class AccessibleFront extends FrontClassWithSpec(accessibleSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this.before("audited", this.audited.bind(this));
+ this.before("name-change", this.nameChange.bind(this));
+ this.before("value-change", this.valueChange.bind(this));
+ this.before("description-change", this.descriptionChange.bind(this));
+ this.before("shortcut-change", this.shortcutChange.bind(this));
+ this.before("reorder", this.reorder.bind(this));
+ this.before("text-change", this.textChange.bind(this));
+ this.before("index-in-parent-change", this.indexInParentChange.bind(this));
+ this.before("states-change", this.statesChange.bind(this));
+ this.before("actions-change", this.actionsChange.bind(this));
+ this.before("attributes-change", this.attributesChange.bind(this));
+ }
+
+ marshallPool() {
+ return this.getParent();
+ }
+
+ get useChildTargetToFetchChildren() {
+ return this._form.useChildTargetToFetchChildren;
+ }
+
+ get role() {
+ return this._form.role;
+ }
+
+ get name() {
+ return this._form.name;
+ }
+
+ get value() {
+ return this._form.value;
+ }
+
+ get description() {
+ return this._form.description;
+ }
+
+ get keyboardShortcut() {
+ return this._form.keyboardShortcut;
+ }
+
+ get childCount() {
+ return this._form.childCount;
+ }
+
+ get domNodeType() {
+ return this._form.domNodeType;
+ }
+
+ get indexInParent() {
+ return this._form.indexInParent;
+ }
+
+ get states() {
+ return this._form.states;
+ }
+
+ get actions() {
+ return this._form.actions;
+ }
+
+ get attributes() {
+ return this._form.attributes;
+ }
+
+ get checks() {
+ return this._form.checks;
+ }
+
+ form(form) {
+ this.actorID = form.actor;
+ this._form = this._form || {};
+ Object.assign(this._form, form);
+ }
+
+ nameChange(name, parent) {
+ this._form.name = name;
+ // Name change event affects the tree rendering, we fire this event on
+ // accessibility walker as the point of interaction for UI.
+ const accessibilityWalkerFront = this.getParent();
+ if (accessibilityWalkerFront) {
+ events.emit(accessibilityWalkerFront, "name-change", this, parent);
+ }
+ }
+
+ valueChange(value) {
+ this._form.value = value;
+ }
+
+ descriptionChange(description) {
+ this._form.description = description;
+ }
+
+ shortcutChange(keyboardShortcut) {
+ this._form.keyboardShortcut = keyboardShortcut;
+ }
+
+ reorder(childCount) {
+ this._form.childCount = childCount;
+ // Reorder event affects the tree rendering, we fire this event on
+ // accessibility walker as the point of interaction for UI.
+ const accessibilityWalkerFront = this.getParent();
+ if (accessibilityWalkerFront) {
+ events.emit(accessibilityWalkerFront, "reorder", this);
+ }
+ }
+
+ textChange() {
+ // Text event affects the tree rendering, we fire this event on
+ // accessibility walker as the point of interaction for UI.
+ const accessibilityWalkerFront = this.getParent();
+ if (accessibilityWalkerFront) {
+ events.emit(accessibilityWalkerFront, "text-change", this);
+ }
+ }
+
+ indexInParentChange(indexInParent) {
+ this._form.indexInParent = indexInParent;
+ }
+
+ statesChange(states) {
+ this._form.states = states;
+ }
+
+ actionsChange(actions) {
+ this._form.actions = actions;
+ }
+
+ attributesChange(attributes) {
+ this._form.attributes = attributes;
+ }
+
+ audited(checks) {
+ this._form.checks = this._form.checks || {};
+ Object.assign(this._form.checks, checks);
+ }
+
+ hydrate() {
+ return super.hydrate().then(properties => {
+ Object.assign(this._form, properties);
+ });
+ }
+
+ async children() {
+ if (!this.useChildTargetToFetchChildren) {
+ return super.children();
+ }
+
+ const { walker: domWalkerFront } = await this.targetFront.getFront(
+ "inspector"
+ );
+ const node = await domWalkerFront.getNodeFromActor(this.actorID, [
+ "rawAccessible",
+ "DOMNode",
+ ]);
+ // We are using DOM inspector/walker API here because we want to keep both
+ // the accessiblity tree and the DOM tree in sync. This is necessary for
+ // several features that the accessibility panel provides such as inspecting
+ // a corresponding DOM node or any other functionality that requires DOM
+ // node ancestries to be resolved all the way up to the top level document.
+ const {
+ nodes: [documentNodeFront],
+ } = await domWalkerFront.children(node);
+ const accessibilityFront = await documentNodeFront.targetFront.getFront(
+ "accessibility"
+ );
+
+ return accessibilityFront.accessibleWalkerFront.children();
+ }
+
+ /**
+ * Helper function that helps with building a complete snapshot of
+ * accessibility tree starting at the level of current accessible front. It
+ * accumulates subtrees from possible out of process frames that are children
+ * of the current accessible front.
+ * @param {JSON} snapshot
+ * Snapshot of the current accessible front or one of its in process
+ * children when recursing.
+ *
+ * @return {JSON}
+ * Complete snapshot of current accessible front.
+ */
+ async _accumulateSnapshot(snapshot) {
+ const { childCount, useChildTargetToFetchChildren } = snapshot;
+ // No children, we are done.
+ if (childCount === 0) {
+ return snapshot;
+ }
+
+ // If current accessible is not a remote frame, continue accumulating inside
+ // its children.
+ if (!useChildTargetToFetchChildren) {
+ const childSnapshots = [];
+ for (const childSnapshot of snapshot.children) {
+ childSnapshots.push(this._accumulateSnapshot(childSnapshot));
+ }
+ await Promise.all(childSnapshots);
+ return snapshot;
+ }
+
+ // When we have a remote frame, we need to obtain an accessible front for a
+ // remote frame document and retrieve its snapshot.
+ const inspectorFront = await this.targetFront.getFront("inspector");
+ const frameNodeFront =
+ await inspectorFront.getNodeActorFromContentDomReference(
+ snapshot.contentDOMReference
+ );
+ // Remove contentDOMReference and useChildTargetToFetchChildren properties.
+ delete snapshot.contentDOMReference;
+ delete snapshot.useChildTargetToFetchChildren;
+ if (!frameNodeFront) {
+ return snapshot;
+ }
+
+ // Remote frame lives in the same process as the current accessible
+ // front we can retrieve the accessible front directly.
+ const frameAccessibleFront = await this.parentFront.getAccessibleFor(
+ frameNodeFront
+ );
+ if (!frameAccessibleFront) {
+ return snapshot;
+ }
+
+ const [docAccessibleFront] = await frameAccessibleFront.children();
+ const childSnapshot = await docAccessibleFront.snapshot();
+ snapshot.children.push(childSnapshot);
+
+ return snapshot;
+ }
+
+ /**
+ * Retrieves a complete JSON snapshot for an accessible subtree of a given
+ * accessible front (inclduing OOP frames).
+ */
+ async snapshot() {
+ const snapshot = await super.snapshot();
+ await this._accumulateSnapshot(snapshot);
+ return snapshot;
+ }
+}
+
+class AccessibleWalkerFront extends FrontClassWithSpec(accessibleWalkerSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this.documentReady = this.documentReady.bind(this);
+ this.on("document-ready", this.documentReady);
+ }
+
+ destroy() {
+ this.off("document-ready", this.documentReady);
+ super.destroy();
+ }
+
+ form(json) {
+ this.actorID = json.actor;
+ }
+
+ documentReady() {
+ if (this.targetFront.isTopLevel) {
+ this.emit("top-level-document-ready");
+ }
+ }
+
+ pick(doFocus) {
+ if (doFocus) {
+ return this.pickAndFocus();
+ }
+
+ return super.pick();
+ }
+
+ /**
+ * Get the accessible object ancestry starting from the given accessible to
+ * the top level document. The top level document is in the top level content process.
+ * @param {Object} accessible
+ * Accessible front to determine the ancestry for.
+ *
+ * @return {Array} ancestry
+ * List of ancestry objects which consist of an accessible with its
+ * children.
+ */
+ async getAncestry(accessible) {
+ const ancestry = await super.getAncestry(accessible);
+
+ const parentTarget = await this.targetFront.getParentTarget();
+ if (!parentTarget) {
+ return ancestry;
+ }
+
+ // Get an accessible front for the parent frame. We go through the
+ // inspector's walker to keep both inspector and accessibility trees in
+ // sync.
+ const { walker: domWalkerFront } = await this.targetFront.getFront(
+ "inspector"
+ );
+ const frameNodeFront = (await domWalkerFront.getRootNode()).parentNode();
+ const accessibilityFront = await parentTarget.getFront("accessibility");
+ const { accessibleWalkerFront } = accessibilityFront;
+ const frameAccessibleFront = await accessibleWalkerFront.getAccessibleFor(
+ frameNodeFront
+ );
+
+ if (!frameAccessibleFront) {
+ // Most likely we are inside a hidden frame.
+ return Promise.reject(
+ `Can't get the ancestry for an accessible front ${accessible.actorID}. It is in the detached tree.`
+ );
+ }
+
+ // Compose the final ancestry out of ancestry for the given accessible in
+ // the current process and recursively get the ancestry for the frame
+ // accessible.
+ ancestry.push(
+ {
+ accessible: frameAccessibleFront,
+ children: await frameAccessibleFront.children(),
+ },
+ ...(await accessibleWalkerFront.getAncestry(frameAccessibleFront))
+ );
+
+ return ancestry;
+ }
+
+ /**
+ * Run an accessibility audit for a document that accessibility walker is
+ * responsible for (in process). In addition to plainly running an audit (in
+ * cases when the document is in the OOP frame), this method also updates
+ * relative ancestries of audited accessible objects all the way up to the top
+ * level document for the toolbox.
+ * @param {Object} options
+ * - {Array} types
+ * types of the accessibility issues to audit for
+ * - {Function} onProgress
+ * callback function for a progress audit-event
+ * - {Boolean} retrieveAncestries (defaults to true)
+ * Set to false to _not_ retrieve ancestries of audited accessible objects.
+ * This is used when a specific document is selected in the iframe picker
+ * and we want to treat it as the root of the accessibility panel tree.
+ */
+ async audit({ types, onProgress, retrieveAncestries = true }) {
+ const onAudit = new Promise(resolve => {
+ const auditEventHandler = ({ type, ancestries, progress }) => {
+ switch (type) {
+ case "error":
+ this.off("audit-event", auditEventHandler);
+ resolve({ error: true });
+ break;
+ case "completed":
+ this.off("audit-event", auditEventHandler);
+ resolve({ ancestries });
+ break;
+ case "progress":
+ onProgress(progress);
+ break;
+ default:
+ break;
+ }
+ };
+
+ this.on("audit-event", auditEventHandler);
+ super.startAudit({ types });
+ });
+
+ const audit = await onAudit;
+ // If audit resulted in an error, if there's nothing to report or if the callsite
+ // explicitly asked to not retrieve ancestries, we are done.
+ // (no need to check for ancestry across the remote frame hierarchy).
+ // See also https://bugzilla.mozilla.org/show_bug.cgi?id=1641551 why the rest of
+ // the code path is only supported when content toolbox fission is enabled.
+ if (audit.error || audit.ancestries.length === 0 || !retrieveAncestries) {
+ return audit;
+ }
+
+ const parentTarget = await this.targetFront.getParentTarget();
+ // If there is no parent target, we do not need to update ancestries as we
+ // are in the top level document.
+ if (!parentTarget) {
+ return audit;
+ }
+
+ // Retrieve an ancestry (cross process) for a current root document and make
+ // audit report ancestries relative to it.
+ const [docAccessibleFront] = await this.children();
+ let docAccessibleAncestry;
+ try {
+ docAccessibleAncestry = await this.getAncestry(docAccessibleFront);
+ } catch (e) {
+ // We are in a detached subtree. We do not consider this an error, instead
+ // we need to ignore the audit for this frame and return an empty report.
+ return { ancestries: [] };
+ }
+ for (const ancestry of audit.ancestries) {
+ // Compose the final ancestries out of the ones in the audit report
+ // relative to this document and the ancestry of the document itself
+ // (cross process).
+ ancestry.push(...docAccessibleAncestry);
+ }
+
+ return audit;
+ }
+
+ /**
+ * A helper wrapper function to show tabbing order overlay for a given target.
+ * The only additional work done is resolving domnode front from a
+ * ContentDOMReference received from a remote target.
+ *
+ * @param {Object} startElm
+ * domnode front to be used as the starting point for generating the
+ * tabbing order.
+ * @param {Number} startIndex
+ * Starting index for the tabbing order.
+ */
+ async _showTabbingOrder(startElm, startIndex) {
+ const { contentDOMReference, index } = await super.showTabbingOrder(
+ startElm,
+ startIndex
+ );
+ let elm;
+ if (contentDOMReference) {
+ const inspectorFront = await this.targetFront.getFront("inspector");
+ elm = await inspectorFront.getNodeActorFromContentDomReference(
+ contentDOMReference
+ );
+ }
+
+ return { elm, index };
+ }
+
+ /**
+ * Show tabbing order overlay for a given target.
+ *
+ * @param {Object} startElm
+ * domnode front to be used as the starting point for generating the
+ * tabbing order.
+ * @param {Number} startIndex
+ * Starting index for the tabbing order.
+ *
+ * @return {JSON}
+ * Tabbing order information for the last element in the tabbing
+ * order. It includes a domnode front and a tabbing index. If we are
+ * at the end of the tabbing order for the top level content document,
+ * the domnode front will be null. If focus manager discovered a
+ * remote IFRAME, then the domnode front is for the IFRAME itself.
+ */
+ async showTabbingOrder(startElm, startIndex) {
+ let { elm: currentElm, index: currentIndex } = await this._showTabbingOrder(
+ startElm,
+ startIndex
+ );
+
+ // If no remote frames were found, currentElm will be null.
+ while (currentElm) {
+ // Safety check to ensure that the currentElm is a remote frame.
+ if (currentElm.useChildTargetToFetchChildren) {
+ const { walker: domWalkerFront } =
+ await currentElm.targetFront.getFront("inspector");
+ const {
+ nodes: [childDocumentNodeFront],
+ } = await domWalkerFront.children(currentElm);
+ const { accessibleWalkerFront } =
+ await childDocumentNodeFront.targetFront.getFront("accessibility");
+ // Show tabbing order in the remote target, while updating the tabbing
+ // index.
+ ({ index: currentIndex } = await accessibleWalkerFront.showTabbingOrder(
+ childDocumentNodeFront,
+ currentIndex
+ ));
+ }
+
+ // Finished with the remote frame, continue in tabbing order, from the
+ // remote frame.
+ ({ elm: currentElm, index: currentIndex } = await this._showTabbingOrder(
+ currentElm,
+ currentIndex
+ ));
+ }
+
+ return { elm: currentElm, index: currentIndex };
+ }
+}
+
+class AccessibilityFront extends FrontClassWithSpec(accessibilitySpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this.before("init", this.init.bind(this));
+ this.before("shutdown", this.shutdown.bind(this));
+
+ // Attribute name from which to retrieve the actorID out of the target
+ // actor's form
+ this.formAttributeName = "accessibilityActor";
+ }
+
+ async initialize() {
+ this.accessibleWalkerFront = await super.getWalker();
+ this.simulatorFront = await super.getSimulator();
+ const { enabled } = await super.bootstrap();
+ this.enabled = enabled;
+
+ try {
+ this._traits = await this.getTraits();
+ } catch (e) {
+ // @backward-compat { version 84 } getTraits isn't available on older server.
+ this._traits = {};
+ }
+ }
+
+ get traits() {
+ return this._traits;
+ }
+
+ init() {
+ this.enabled = true;
+ }
+
+ shutdown() {
+ this.enabled = false;
+ }
+}
+
+class ParentAccessibilityFront extends FrontClassWithSpec(
+ parentAccessibilitySpec
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this.before("can-be-enabled-change", this.canBeEnabled.bind(this));
+ this.before("can-be-disabled-change", this.canBeDisabled.bind(this));
+
+ // Attribute name from which to retrieve the actorID out of the target
+ // actor's form
+ this.formAttributeName = "parentAccessibilityActor";
+ }
+
+ async initialize() {
+ ({ canBeEnabled: this.canBeEnabled, canBeDisabled: this.canBeDisabled } =
+ await super.bootstrap());
+ }
+
+ canBeEnabled(canBeEnabled) {
+ this.canBeEnabled = canBeEnabled;
+ }
+
+ canBeDisabled(canBeDisabled) {
+ this.canBeDisabled = canBeDisabled;
+ }
+}
+
+const SimulatorFront = FrontClassWithSpec(simulatorSpec);
+
+exports.AccessibleFront = AccessibleFront;
+registerFront(AccessibleFront);
+exports.AccessibleWalkerFront = AccessibleWalkerFront;
+registerFront(AccessibleWalkerFront);
+exports.AccessibilityFront = AccessibilityFront;
+registerFront(AccessibilityFront);
+exports.ParentAccessibilityFront = ParentAccessibilityFront;
+registerFront(ParentAccessibilityFront);
+exports.SimulatorFront = SimulatorFront;
+registerFront(SimulatorFront);
diff --git a/devtools/client/fronts/addon/addons.js b/devtools/client/fronts/addon/addons.js
new file mode 100644
index 0000000000..84d495b850
--- /dev/null
+++ b/devtools/client/fronts/addon/addons.js
@@ -0,0 +1,25 @@
+/* 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 {
+ addonsSpec,
+} = require("resource://devtools/shared/specs/addon/addons.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+
+class AddonsFront extends FrontClassWithSpec(addonsSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "addonsActor";
+ }
+}
+
+exports.AddonsFront = AddonsFront;
+registerFront(AddonsFront);
diff --git a/devtools/client/fronts/addon/moz.build b/devtools/client/fronts/addon/moz.build
new file mode 100644
index 0000000000..e382173641
--- /dev/null
+++ b/devtools/client/fronts/addon/moz.build
@@ -0,0 +1,10 @@
+# -*- 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(
+ "addons.js",
+ "webextension-inspected-window.js",
+)
diff --git a/devtools/client/fronts/addon/webextension-inspected-window.js b/devtools/client/fronts/addon/webextension-inspected-window.js
new file mode 100644
index 0000000000..cf0674dfa8
--- /dev/null
+++ b/devtools/client/fronts/addon/webextension-inspected-window.js
@@ -0,0 +1,31 @@
+/* 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 {
+ webExtensionInspectedWindowSpec,
+} = require("resource://devtools/shared/specs/addon/webextension-inspected-window.js");
+
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+
+/**
+ * The corresponding Front object for the WebExtensionInspectedWindowActor.
+ */
+class WebExtensionInspectedWindowFront extends FrontClassWithSpec(
+ webExtensionInspectedWindowSpec
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "webExtensionInspectedWindowActor";
+ }
+}
+
+exports.WebExtensionInspectedWindowFront = WebExtensionInspectedWindowFront;
+registerFront(WebExtensionInspectedWindowFront);
diff --git a/devtools/client/fronts/animation.js b/devtools/client/fronts/animation.js
new file mode 100644
index 0000000000..7dd17fc6e5
--- /dev/null
+++ b/devtools/client/fronts/animation.js
@@ -0,0 +1,214 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ animationPlayerSpec,
+ animationsSpec,
+} = require("resource://devtools/shared/specs/animation.js");
+
+class AnimationPlayerFront extends FrontClassWithSpec(animationPlayerSpec) {
+ constructor(conn, targetFront, parentFront) {
+ super(conn, targetFront, parentFront);
+
+ this.state = {};
+ this.before("changed", this.onChanged.bind(this));
+ }
+
+ form(form) {
+ this._form = form;
+ this.state = this.initialState;
+ }
+
+ /**
+ * If the AnimationsActor was given a reference to the WalkerActor previously
+ * then calling this getter will return the animation target NodeFront.
+ */
+ get animationTargetNodeFront() {
+ if (!this._form.animationTargetNodeActorID) {
+ return null;
+ }
+
+ return this.conn.getFrontByID(this._form.animationTargetNodeActorID);
+ }
+
+ /**
+ * Getter for the initial state of the player. Up to date states can be
+ * retrieved by calling the getCurrentState method.
+ */
+ get initialState() {
+ return {
+ type: this._form.type,
+ startTime: this._form.startTime,
+ currentTime: this._form.currentTime,
+ playState: this._form.playState,
+ playbackRate: this._form.playbackRate,
+ name: this._form.name,
+ duration: this._form.duration,
+ delay: this._form.delay,
+ endDelay: this._form.endDelay,
+ iterationCount: this._form.iterationCount,
+ iterationStart: this._form.iterationStart,
+ easing: this._form.easing,
+ fill: this._form.fill,
+ direction: this._form.direction,
+ animationTimingFunction: this._form.animationTimingFunction,
+ isRunningOnCompositor: this._form.isRunningOnCompositor,
+ propertyState: this._form.propertyState,
+ documentCurrentTime: this._form.documentCurrentTime,
+ createdTime: this._form.createdTime,
+ currentTimeAtCreated: this._form.currentTimeAtCreated,
+ absoluteValues: this.calculateAbsoluteValues(this._form),
+ properties: this._form.properties,
+ };
+ }
+
+ /**
+ * Executed when the AnimationPlayerActor emits a "changed" event. Used to
+ * update the local knowledge of the state.
+ */
+ onChanged(partialState) {
+ const { state } = this.reconstructState(partialState);
+ this.state = state;
+ }
+
+ /**
+ * Refresh the current state of this animation on the client from information
+ * found on the server. Doesn't return anything, just stores the new state.
+ */
+ async refreshState() {
+ const data = await this.getCurrentState();
+ if (this.currentStateHasChanged) {
+ this.state = data;
+ }
+ }
+
+ /**
+ * getCurrentState interceptor re-constructs incomplete states since the actor
+ * only sends the values that have changed.
+ */
+ getCurrentState() {
+ this.currentStateHasChanged = false;
+ return super.getCurrentState().then(partialData => {
+ const { state, hasChanged } = this.reconstructState(partialData);
+ this.currentStateHasChanged = hasChanged;
+ return state;
+ });
+ }
+
+ reconstructState(data) {
+ let hasChanged = false;
+
+ for (const key in this.state) {
+ if (typeof data[key] === "undefined") {
+ data[key] = this.state[key];
+ } else if (data[key] !== this.state[key]) {
+ hasChanged = true;
+ }
+ }
+
+ data.absoluteValues = this.calculateAbsoluteValues(data);
+ return { state: data, hasChanged };
+ }
+
+ calculateAbsoluteValues(data) {
+ const {
+ createdTime,
+ currentTime,
+ currentTimeAtCreated,
+ delay,
+ duration,
+ endDelay = 0,
+ fill,
+ iterationCount,
+ playbackRate,
+ } = data;
+
+ const toRate = v => v / Math.abs(playbackRate);
+ const isPositivePlaybackRate = playbackRate > 0;
+ let absoluteDelay = 0;
+ let absoluteEndDelay = 0;
+ let isDelayFilled = false;
+ let isEndDelayFilled = false;
+
+ if (isPositivePlaybackRate) {
+ absoluteDelay = toRate(delay);
+ absoluteEndDelay = toRate(endDelay);
+ isDelayFilled = fill === "both" || fill === "backwards";
+ isEndDelayFilled = fill === "both" || fill === "forwards";
+ } else {
+ absoluteDelay = toRate(endDelay);
+ absoluteEndDelay = toRate(delay);
+ isDelayFilled = fill === "both" || fill === "forwards";
+ isEndDelayFilled = fill === "both" || fill === "backwards";
+ }
+
+ let endTime = 0;
+
+ if (duration === Infinity) {
+ // Set endTime so as to enable the scrubber with keeping the consinstency of UI
+ // even the duration was Infinity. In case of delay is longer than zero, handle
+ // the graph duration as double of the delay amount. In case of no delay, handle
+ // the duration as 1ms which is short enough so as to make the scrubber movable
+ // and the limited duration is prioritized.
+ endTime = absoluteDelay > 0 ? absoluteDelay * 2 : 1;
+ } else {
+ endTime =
+ absoluteDelay +
+ toRate(duration * (iterationCount || 1)) +
+ absoluteEndDelay;
+ }
+
+ const absoluteCreatedTime = isPositivePlaybackRate
+ ? createdTime
+ : createdTime - endTime;
+ const absoluteCurrentTimeAtCreated = isPositivePlaybackRate
+ ? currentTimeAtCreated
+ : endTime - currentTimeAtCreated;
+ const animationCurrentTime = isPositivePlaybackRate
+ ? currentTime
+ : endTime - currentTime;
+ const absoluteCurrentTime =
+ absoluteCreatedTime + toRate(animationCurrentTime);
+ const absoluteStartTime = absoluteCreatedTime + Math.min(absoluteDelay, 0);
+ const absoluteStartTimeAtCreated =
+ absoluteCreatedTime + absoluteCurrentTimeAtCreated;
+ // To show whole graph with endDelay, we add negative endDelay amount to endTime.
+ const endTimeWithNegativeEndDelay = endTime - Math.min(absoluteEndDelay, 0);
+ const absoluteEndTime = absoluteCreatedTime + endTimeWithNegativeEndDelay;
+
+ return {
+ createdTime: absoluteCreatedTime,
+ currentTime: absoluteCurrentTime,
+ currentTimeAtCreated: absoluteCurrentTimeAtCreated,
+ delay: absoluteDelay,
+ endDelay: absoluteEndDelay,
+ endTime: absoluteEndTime,
+ isDelayFilled,
+ isEndDelayFilled,
+ startTime: absoluteStartTime,
+ startTimeAtCreated: absoluteStartTimeAtCreated,
+ };
+ }
+}
+
+exports.AnimationPlayerFront = AnimationPlayerFront;
+registerFront(AnimationPlayerFront);
+
+class AnimationsFront extends FrontClassWithSpec(animationsSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "animationsActor";
+ }
+}
+
+exports.AnimationsFront = AnimationsFront;
+registerFront(AnimationsFront);
diff --git a/devtools/client/fronts/array-buffer.js b/devtools/client/fronts/array-buffer.js
new file mode 100644
index 0000000000..c7741ffe3f
--- /dev/null
+++ b/devtools/client/fronts/array-buffer.js
@@ -0,0 +1,26 @@
+/* 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 {
+ arrayBufferSpec,
+} = require("resource://devtools/shared/specs/array-buffer.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+
+/**
+ * A ArrayBufferClient provides a way to access ArrayBuffer from the
+ * devtools server.
+ */
+class ArrayBufferFront extends FrontClassWithSpec(arrayBufferSpec) {
+ form(json) {
+ this.length = json.length;
+ }
+}
+
+exports.ArrayBufferFront = ArrayBufferFront;
+registerFront(ArrayBufferFront);
diff --git a/devtools/client/fronts/blackboxing.js b/devtools/client/fronts/blackboxing.js
new file mode 100644
index 0000000000..ab26c484cc
--- /dev/null
+++ b/devtools/client/fronts/blackboxing.js
@@ -0,0 +1,17 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ blackboxingSpec,
+} = require("resource://devtools/shared/specs/blackboxing.js");
+
+class BlackboxingFront extends FrontClassWithSpec(blackboxingSpec) {}
+
+registerFront(BlackboxingFront);
diff --git a/devtools/client/fronts/breakpoint-list.js b/devtools/client/fronts/breakpoint-list.js
new file mode 100644
index 0000000000..c8fc57226b
--- /dev/null
+++ b/devtools/client/fronts/breakpoint-list.js
@@ -0,0 +1,17 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ breakpointListSpec,
+} = require("resource://devtools/shared/specs/breakpoint-list.js");
+
+class BreakpointListFront extends FrontClassWithSpec(breakpointListSpec) {}
+
+registerFront(BreakpointListFront);
diff --git a/devtools/client/fronts/changes.js b/devtools/client/fronts/changes.js
new file mode 100644
index 0000000000..4737156dcf
--- /dev/null
+++ b/devtools/client/fronts/changes.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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const { changesSpec } = require("resource://devtools/shared/specs/changes.js");
+
+/**
+ * ChangesFront, the front object for the ChangesActor
+ */
+class ChangesFront extends FrontClassWithSpec(changesSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "changesActor";
+ }
+
+ async initialize() {
+ // Ensure the corresponding ChangesActor is immediately available and ready to track
+ // changes by calling a method on it. Actors are lazy and won't be created until
+ // actually used.
+ await super.start();
+ }
+}
+
+exports.ChangesFront = ChangesFront;
+registerFront(ChangesFront);
diff --git a/devtools/client/fronts/compatibility.js b/devtools/client/fronts/compatibility.js
new file mode 100644
index 0000000000..292411e6af
--- /dev/null
+++ b/devtools/client/fronts/compatibility.js
@@ -0,0 +1,18 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ compatibilitySpec,
+} = require("resource://devtools/shared/specs/compatibility.js");
+
+class CompatibilityFront extends FrontClassWithSpec(compatibilitySpec) {}
+
+exports.CompatibilityFront = CompatibilityFront;
+registerFront(CompatibilityFront);
diff --git a/devtools/client/fronts/css-properties.js b/devtools/client/fronts/css-properties.js
new file mode 100644
index 0000000000..6c16df4cea
--- /dev/null
+++ b/devtools/client/fronts/css-properties.js
@@ -0,0 +1,278 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ cssPropertiesSpec,
+} = require("resource://devtools/shared/specs/css-properties.js");
+
+loader.lazyRequireGetter(
+ this,
+ "cssColors",
+ "resource://devtools/shared/css/color-db.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "CSS_PROPERTIES_DB",
+ "resource://devtools/shared/css/properties-db.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "CSS_TYPES",
+ "resource://devtools/shared/css/constants.js",
+ true
+);
+
+/**
+ * Build up a regular expression that matches a CSS variable token. This is an
+ * ident token that starts with two dashes "--".
+ *
+ * https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
+ */
+var NON_ASCII = "[^\\x00-\\x7F]";
+var ESCAPE = "\\\\[^\n\r]";
+var VALID_CHAR = ["[_a-z0-9-]", NON_ASCII, ESCAPE].join("|");
+var IS_VARIABLE_TOKEN = new RegExp(`^--(${VALID_CHAR})*$`, "i");
+
+/**
+ * The CssProperties front provides a mechanism to have a one-time asynchronous
+ * load of a CSS properties database. This is then fed into the CssProperties
+ * interface that provides synchronous methods for finding out what CSS
+ * properties the current server supports.
+ */
+class CssPropertiesFront extends FrontClassWithSpec(cssPropertiesSpec) {
+ constructor(client, targetFront) {
+ super(client, targetFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "cssPropertiesActor";
+ }
+
+ async initialize() {
+ const db = await super.getCSSDatabase();
+ this.cssProperties = new CssProperties(normalizeCssData(db));
+ }
+
+ destroy() {
+ this.cssProperties = null;
+ super.destroy();
+ }
+}
+
+/**
+ * Ask questions to a CSS database. This class does not care how the database
+ * gets loaded in, only the questions that you can ask to it.
+ * Prototype functions are bound to 'this' so they can be passed around as helper
+ * functions.
+ *
+ * @param {Object} db
+ * A database of CSS properties
+ * @param {Object} inheritedList
+ * The key is the property name, the value is whether or not
+ * that property is inherited.
+ */
+function CssProperties(db) {
+ this.properties = db.properties;
+ this.pseudoElements = db.pseudoElements;
+
+ this.isKnown = this.isKnown.bind(this);
+ this.isInherited = this.isInherited.bind(this);
+ this.supportsType = this.supportsType.bind(this);
+}
+
+CssProperties.prototype = {
+ /**
+ * Checks to see if the property is known by the browser. This function has
+ * `this` already bound so that it can be passed around by reference.
+ *
+ * @param {String} property The property name to be checked.
+ * @return {Boolean}
+ */
+ isKnown(property) {
+ // Custom Property Names (aka CSS Variables) are case-sensitive; do not lowercase.
+ property = property.startsWith("--") ? property : property.toLowerCase();
+ return !!this.properties[property] || isCssVariable(property);
+ },
+
+ /**
+ * Checks to see if the property is an inherited one.
+ *
+ * @param {String} property The property name to be checked.
+ * @return {Boolean}
+ */
+ isInherited(property) {
+ return (
+ (this.properties[property] && this.properties[property].isInherited) ||
+ isCssVariable(property)
+ );
+ },
+
+ /**
+ * Checks if the property supports the given CSS type.
+ *
+ * @param {String} property The property to be checked.
+ * @param {String} type One of the values from InspectorPropertyType.
+ * @return {Boolean}
+ */
+ supportsType(property, type) {
+ const id = CSS_TYPES[type];
+ return (
+ this.properties[property] &&
+ (this.properties[property].supports.includes(type) ||
+ this.properties[property].supports.includes(id))
+ );
+ },
+
+ /**
+ * Gets the CSS values for a given property name.
+ *
+ * @param {String} property The property to use.
+ * @return {Array} An array of strings.
+ */
+ getValues(property) {
+ return this.properties[property] ? this.properties[property].values : [];
+ },
+
+ /**
+ * Gets the CSS property names.
+ *
+ * @return {Array} An array of strings.
+ */
+ getNames(property) {
+ return Object.keys(this.properties);
+ },
+
+ /**
+ * Return a list of subproperties for the given property. If |name|
+ * does not name a valid property, an empty array is returned. If
+ * the property is not a shorthand property, then array containing
+ * just the property itself is returned.
+ *
+ * @param {String} name The property to query
+ * @return {Array} An array of subproperty names.
+ */
+ getSubproperties(name) {
+ // Custom Property Names (aka CSS Variables) are case-sensitive; do not lowercase.
+ name = name.startsWith("--") ? name : name.toLowerCase();
+ if (this.isKnown(name)) {
+ if (this.properties[name] && this.properties[name].subproperties) {
+ return this.properties[name].subproperties;
+ }
+ return [name];
+ }
+ return [];
+ },
+};
+
+/**
+ * Check that this is a CSS variable.
+ *
+ * @param {String} input
+ * @return {Boolean}
+ */
+function isCssVariable(input) {
+ return !!input.match(IS_VARIABLE_TOKEN);
+}
+
+/**
+ * Get a client-side CssProperties. This is useful for dependencies in tests, or parts
+ * of the codebase that don't particularly need to match every known CSS property on
+ * the target.
+ * @return {CssProperties}
+ */
+function getClientCssProperties() {
+ return new CssProperties(normalizeCssData(CSS_PROPERTIES_DB));
+}
+
+/**
+ * Even if the target has the cssProperties actor, the returned data may not be in the
+ * same shape or have all of the data we need. This normalizes the data and fills in
+ * any missing information like color values.
+ *
+ * @return {Object} The normalized CSS database.
+ */
+function normalizeCssData(db) {
+ // If there is a `from` attributes, it means that it comes from RDP
+ // and it is not the client CSS_PROPERTIES_DB object.
+ // (prevent comparing to CSS_PROPERTIES_DB to avoid loading client database)
+ if (typeof db.from == "string") {
+ const missingSupports = !db.properties.color.supports;
+ const missingValues = !db.properties.color.values;
+ const missingSubproperties = !db.properties.background.subproperties;
+ const missingIsInherited = !db.properties.font.isInherited;
+
+ const missingSomething =
+ missingSupports ||
+ missingValues ||
+ missingSubproperties ||
+ missingIsInherited;
+
+ if (missingSomething) {
+ for (const name in db.properties) {
+ // Skip the current property if we can't find it in CSS_PROPERTIES_DB.
+ if (typeof CSS_PROPERTIES_DB.properties[name] !== "object") {
+ continue;
+ }
+
+ // Add "supports" information to the css properties if it's missing.
+ if (missingSupports) {
+ db.properties[name].supports =
+ CSS_PROPERTIES_DB.properties[name].supports;
+ }
+ // Add "values" information to the css properties if it's missing.
+ if (missingValues) {
+ db.properties[name].values =
+ CSS_PROPERTIES_DB.properties[name].values;
+ }
+ // Add "subproperties" information to the css properties if it's missing.
+ if (missingSubproperties) {
+ db.properties[name].subproperties =
+ CSS_PROPERTIES_DB.properties[name].subproperties;
+ }
+ // Add "isInherited" information to the css properties if it's missing.
+ if (missingIsInherited) {
+ db.properties[name].isInherited =
+ CSS_PROPERTIES_DB.properties[name].isInherited;
+ }
+ }
+ }
+ }
+
+ reattachCssColorValues(db);
+
+ return db;
+}
+
+/**
+ * Color values are omitted to save on space. Add them back here.
+ * @param {Object} The CSS database.
+ */
+function reattachCssColorValues(db) {
+ if (db.properties.color.values[0] === "COLOR") {
+ const colors = Object.keys(cssColors);
+
+ for (const name in db.properties) {
+ const property = db.properties[name];
+ // "values" can be undefined if {name} was not found in CSS_PROPERTIES_DB.
+ if (property.values && property.values[0] === "COLOR") {
+ property.values.shift();
+ property.values = property.values.concat(colors).sort();
+ }
+ }
+ }
+}
+
+module.exports = {
+ CssPropertiesFront,
+ getClientCssProperties,
+ isCssVariable,
+};
+registerFront(CssPropertiesFront);
diff --git a/devtools/client/fronts/descriptors/descriptor-mixin.js b/devtools/client/fronts/descriptors/descriptor-mixin.js
new file mode 100644
index 0000000000..ac38e104b5
--- /dev/null
+++ b/devtools/client/fronts/descriptors/descriptor-mixin.js
@@ -0,0 +1,60 @@
+/* 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";
+
+/**
+ * A Descriptor 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.
+ * It can be very similar to a Target. The key difference is the lifecycle of these two classes.
+ * The descriptor is meant to be always alive and meaningful/usable until the end of the RDP connection.
+ * Typically a Tab Descriptor will describe the tab and not the one document currently loaded in this tab,
+ * while the Target, will describe this one document and a new Target may be created on each navigation.
+ */
+function DescriptorMixin(parentClass) {
+ class Descriptor extends parentClass {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this._client = client;
+
+ // Pass a true value in order to distinguish this event reception
+ // from any manual destroy caused by the frontend
+ this.on(
+ "descriptor-destroyed",
+ this.destroy.bind(this, { isServerDestroyEvent: true })
+ );
+ }
+
+ get client() {
+ return this._client;
+ }
+
+ async destroy({ isServerDestroyEvent } = {}) {
+ if (this.isDestroyed()) {
+ return;
+ }
+ // This workaround is mostly done for Workers, as WorkerDescriptor
+ // extends the Target class, which causes some issue down the road:
+ // In Target.destroy, we call WorkerDescriptorActor.detach *before* calling super.destroy(),
+ // and so hold on before calling Front.destroy() which would reject all pending requests, including detach().
+ // When calling detach, the server will emit "descriptor-destroyed", which will call Target.destroy again,
+ // but will still be blocked on detach resolution and won't call Front.destroy, and won't reject pending requests either.
+ //
+ // So call Front.baseFrontClassDestroyed manually from here, so that we ensure rejecting the pending detach request
+ // and unblock Target.destroy resolution.
+ //
+ // Here is the inheritance chain for WorkerDescriptor:
+ // WorkerDescriptor -> Descriptor (from descriptor-mixin.js) -> Target (from target-mixin.js) -> Front (protocol.js) -> Pool (protocol.js) -> EventEmitter
+ if (isServerDestroyEvent) {
+ this.baseFrontClassDestroy();
+ }
+
+ await super.destroy();
+ }
+ }
+ return Descriptor;
+}
+exports.DescriptorMixin = DescriptorMixin;
diff --git a/devtools/client/fronts/descriptors/descriptor-types.js b/devtools/client/fronts/descriptors/descriptor-types.js
new file mode 100644
index 0000000000..00164509ab
--- /dev/null
+++ b/devtools/client/fronts/descriptors/descriptor-types.js
@@ -0,0 +1,17 @@
+/* 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";
+
+/**
+ * List of all descriptor types.
+ *
+ * This will be expose via `descriptorType` on all descriptor fronts.
+ */
+module.exports = {
+ PROCESS: "process",
+ WORKER: "worker",
+ TAB: "tab",
+ EXTENSION: "extension",
+};
diff --git a/devtools/client/fronts/descriptors/moz.build b/devtools/client/fronts/descriptors/moz.build
new file mode 100644
index 0000000000..665a0f252a
--- /dev/null
+++ b/devtools/client/fronts/descriptors/moz.build
@@ -0,0 +1,14 @@
+# -*- 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(
+ "descriptor-mixin.js",
+ "descriptor-types.js",
+ "process.js",
+ "tab.js",
+ "webextension.js",
+ "worker.js",
+)
diff --git a/devtools/client/fronts/descriptors/process.js b/devtools/client/fronts/descriptors/process.js
new file mode 100644
index 0000000000..6efc1bd4d7
--- /dev/null
+++ b/devtools/client/fronts/descriptors/process.js
@@ -0,0 +1,142 @@
+/* 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 {
+ processDescriptorSpec,
+} = require("resource://devtools/shared/specs/descriptors/process.js");
+const {
+ WindowGlobalTargetFront,
+} = require("resource://devtools/client/fronts/targets/window-global.js");
+const {
+ ContentProcessTargetFront,
+} = require("resource://devtools/client/fronts/targets/content-process.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ DescriptorMixin,
+} = require("resource://devtools/client/fronts/descriptors/descriptor-mixin.js");
+const DESCRIPTOR_TYPES = require("resource://devtools/client/fronts/descriptors/descriptor-types.js");
+
+class ProcessDescriptorFront extends DescriptorMixin(
+ FrontClassWithSpec(processDescriptorSpec)
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this._isParent = false;
+ this._processTargetFront = null;
+ this._targetFrontPromise = null;
+ }
+
+ descriptorType = DESCRIPTOR_TYPES.PROCESS;
+
+ form(json) {
+ this.id = json.id;
+ this._isParent = json.isParent;
+ this._isWindowlessParent = json.isWindowlessParent;
+ this.traits = json.traits || {};
+ }
+
+ async _createProcessTargetFront(form) {
+ let front = null;
+ // the request to getTarget may return a ContentProcessTargetActor or a
+ // ParentProcessTargetActor. In most cases getProcess(0) will return the
+ // main process target actor, which is a ParentProcessTargetActor, but
+ // not in xpcshell, which uses a ContentProcessTargetActor. So select
+ // the right front based on the actor ID.
+ if (form.actor.includes("parentProcessTarget")) {
+ // ParentProcessTargetActor doesn't have a specific front, instead it uses
+ // WindowGlobalTargetFront on the client side.
+ front = new WindowGlobalTargetFront(this._client, null, this);
+ } else {
+ front = new ContentProcessTargetFront(this._client, null, this);
+ }
+ // As these fronts aren't instantiated by protocol.js, we have to set their actor ID
+ // manually like that:
+ front.actorID = form.actor;
+ front.form(form);
+
+ // @backward-compat { version 84 } Older server don't send the processID in the form
+ if (!front.processID) {
+ front.processID = this.id;
+ }
+
+ this.manage(front);
+ return front;
+ }
+
+ /**
+ * This flag should be true for parent process descriptors of a regular
+ * browser instance, where you can expect the target to be associated with a
+ * window global.
+ *
+ * This will typically be true for the descriptor used by the Browser Toolbox
+ * or the Browser Console opened against a regular Firefox instance.
+ *
+ * On the contrary this will be false for parent process descriptors created
+ * for xpcshell debugging or for background task debugging.
+ */
+ get isBrowserProcessDescriptor() {
+ return this._isParent && !this._isWindowlessParent;
+ }
+
+ get isParentProcessDescriptor() {
+ return this._isParent;
+ }
+
+ get isProcessDescriptor() {
+ return true;
+ }
+
+ getCachedTarget() {
+ return this._processTargetFront;
+ }
+
+ async getTarget() {
+ // Only return the cached Target if it is still alive.
+ if (this._processTargetFront && !this._processTargetFront.isDestroyed()) {
+ return this._processTargetFront;
+ }
+ // Otherwise, ensure that we don't try to spawn more than one Target by
+ // returning the pending promise
+ if (this._targetFrontPromise) {
+ return this._targetFrontPromise;
+ }
+ this._targetFrontPromise = (async () => {
+ let targetFront = null;
+ try {
+ const targetForm = await super.getTarget();
+ targetFront = await this._createProcessTargetFront(targetForm);
+ } catch (e) {
+ // This is likely to happen if we get a lot of events which drop previous
+ // processes.
+ console.log(
+ `Request to connect to ProcessDescriptor "${this.id}" failed: ${e}`
+ );
+ }
+ // Save the reference to the target only after the call to attach
+ // so that getTarget always returns the attached target in case of concurrent calls
+ this._processTargetFront = targetFront;
+ // clear the promise if we are finished so that we can re-connect if
+ // necessary
+ this._targetFrontPromise = null;
+ return targetFront;
+ })();
+ return this._targetFrontPromise;
+ }
+
+ destroy() {
+ if (this._processTargetFront) {
+ this._processTargetFront.destroy();
+ this._processTargetFront = null;
+ }
+ this._targetFrontPromise = null;
+ super.destroy();
+ }
+}
+
+exports.ProcessDescriptorFront = ProcessDescriptorFront;
+registerFront(ProcessDescriptorFront);
diff --git a/devtools/client/fronts/descriptors/tab.js b/devtools/client/fronts/descriptors/tab.js
new file mode 100644
index 0000000000..097aed7674
--- /dev/null
+++ b/devtools/client/fronts/descriptors/tab.js
@@ -0,0 +1,330 @@
+/* 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 {
+ tabDescriptorSpec,
+} = require("resource://devtools/shared/specs/descriptors/tab.js");
+const DESCRIPTOR_TYPES = require("resource://devtools/client/fronts/descriptors/descriptor-types.js");
+
+loader.lazyRequireGetter(
+ this,
+ "gDevTools",
+ "resource://devtools/client/framework/devtools.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "WindowGlobalTargetFront",
+ "resource://devtools/client/fronts/targets/window-global.js",
+ true
+);
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ DescriptorMixin,
+} = require("resource://devtools/client/fronts/descriptors/descriptor-mixin.js");
+
+const POPUP_DEBUG_PREF = "devtools.popups.debug";
+
+/**
+ * DescriptorFront for tab targets.
+ *
+ * @fires remoteness-change
+ * Fired only for target switching, when the debugged tab is a local tab.
+ * TODO: This event could move to the server in order to support
+ * remoteness change for remote debugging.
+ */
+class TabDescriptorFront extends DescriptorMixin(
+ FrontClassWithSpec(tabDescriptorSpec)
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // The tab descriptor can be configured to create either local tab targets
+ // (eg, regular tab toolbox) or browsing context targets (eg tab remote
+ // debugging).
+ this._localTab = null;
+
+ // Flag to prevent the server from trying to spawn targets by the watcher actor.
+ this._disableTargetSwitching = false;
+
+ this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
+ this._handleTabEvent = this._handleTabEvent.bind(this);
+
+ // When the target is created from the server side,
+ // it is not created via TabDescriptor.getTarget.
+ // Instead, it is retrieved by the TargetCommand which
+ // will call TabDescriptor.setTarget from TargetCommand.onTargetAvailable
+ if (this.isServerTargetSwitchingEnabled()) {
+ this._targetFrontPromise = new Promise(
+ r => (this._resolveTargetFrontPromise = r)
+ );
+ }
+ }
+
+ descriptorType = DESCRIPTOR_TYPES.TAB;
+
+ form(json) {
+ this.actorID = json.actor;
+ this._form = json;
+ this.traits = json.traits || {};
+ }
+
+ /**
+ * Destroy the front.
+ *
+ * @param Boolean If true, it means that we destroy the front when receiving the descriptor-destroyed
+ * event from the server.
+ */
+ destroy({ isServerDestroyEvent = false } = {}) {
+ if (this.isDestroyed()) {
+ return;
+ }
+
+ // The descriptor may be destroyed first by the frontend.
+ // When closing the tab, the toolbox document is almost immediately removed from the DOM.
+ // The `unload` event fires and toolbox destroys itself, as well as its related client.
+ //
+ // In such case, we emit the descriptor-destroyed event
+ if (!isServerDestroyEvent) {
+ this.emit("descriptor-destroyed");
+ }
+ if (this.isLocalTab) {
+ this._teardownLocalTabListeners();
+ }
+ super.destroy();
+ }
+
+ getWatcher() {
+ const isPopupDebuggingEnabled = Services.prefs.getBoolPref(
+ POPUP_DEBUG_PREF,
+ false
+ );
+ return super.getWatcher({
+ isServerTargetSwitchingEnabled: this.isServerTargetSwitchingEnabled(),
+ isPopupDebuggingEnabled,
+ });
+ }
+
+ setLocalTab(localTab) {
+ this._localTab = localTab;
+ this._setupLocalTabListeners();
+ }
+
+ get isTabDescriptor() {
+ return true;
+ }
+
+ get isLocalTab() {
+ return !!this._localTab;
+ }
+
+ get localTab() {
+ return this._localTab;
+ }
+
+ _setupLocalTabListeners() {
+ this.localTab.addEventListener("TabClose", this._handleTabEvent);
+ this.localTab.addEventListener("TabRemotenessChange", this._handleTabEvent);
+ }
+
+ _teardownLocalTabListeners() {
+ this.localTab.removeEventListener("TabClose", this._handleTabEvent);
+ this.localTab.removeEventListener(
+ "TabRemotenessChange",
+ this._handleTabEvent
+ );
+ }
+
+ isServerTargetSwitchingEnabled() {
+ return !this._disableTargetSwitching;
+ }
+
+ /**
+ * Called by CommandsFactory, when the WebExtension codebase instantiates
+ * a commands. We have to flag the TabDescriptor for them as they don't support
+ * target switching and gets severely broken when enabling server target which
+ * introduce target switching for all navigations and reloads
+ */
+ setIsForWebExtension() {
+ this.disableTargetSwitching();
+ }
+
+ /**
+ * Method used by the WebExtension which still need to disable server side targets,
+ * and also a few xpcshell tests which are using legacy API and don't support watcher actor.
+ */
+ disableTargetSwitching() {
+ this._disableTargetSwitching = true;
+ // Delete these two attributes which have to be set early from the constructor,
+ // but we don't know yet if target switch should be disabled.
+ delete this._targetFrontPromise;
+ delete this._resolveTargetFrontPromise;
+ }
+
+ get isZombieTab() {
+ return this._form.isZombieTab;
+ }
+
+ get browserId() {
+ return this._form.browserId;
+ }
+
+ get selected() {
+ return this._form.selected;
+ }
+
+ get title() {
+ return this._form.title;
+ }
+
+ get url() {
+ return this._form.url;
+ }
+
+ get favicon() {
+ // Note: the favicon is not part of the default form() payload, it will be
+ // added in `retrieveFavicon`.
+ return this._form.favicon;
+ }
+
+ _createTabTarget(form) {
+ const front = new WindowGlobalTargetFront(this._client, null, this);
+
+ // As these fronts aren't instantiated by protocol.js, we have to set their actor ID
+ // manually like that:
+ front.actorID = form.actor;
+ front.form(form);
+ this.manage(front);
+ return front;
+ }
+
+ _onTargetDestroyed() {
+ // Clear the cached targetFront when the target is destroyed.
+ // Note that we are also checking that _targetFront has a valid actorID
+ // in getTarget, this acts as an additional security to avoid races.
+ this._targetFront = null;
+ }
+
+ /**
+ * Safely retrieves the favicon via getFavicon() and populates this._form.favicon.
+ *
+ * We could let callers explicitly retrieve the favicon instead of inserting it in the
+ * form dynamically.
+ */
+ async retrieveFavicon() {
+ try {
+ this._form.favicon = await this.getFavicon();
+ } catch (e) {
+ // We might request the data for a tab which is going to be destroyed.
+ // In this case the TargetFront will be destroyed. Otherwise log an error.
+ if (!this.isDestroyed()) {
+ console.error("Failed to retrieve the favicon for " + this.url, e);
+ }
+ }
+ }
+
+ /**
+ * Top-level targets created on the server will not be created and managed
+ * by a descriptor front. Instead they are created by the Watcher actor.
+ * On the client side we manually re-establish a link between the descriptor
+ * and the new top-level target.
+ */
+ setTarget(targetFront) {
+ // Completely ignore the previous target.
+ // We might nullify the _targetFront unexpectely due to previous target
+ // being destroyed after the new is created
+ if (this._targetFront) {
+ this._targetFront.off("target-destroyed", this._onTargetDestroyed);
+ }
+ this._targetFront = targetFront;
+
+ targetFront.on("target-destroyed", this._onTargetDestroyed);
+
+ if (this.isServerTargetSwitchingEnabled()) {
+ this._resolveTargetFrontPromise(targetFront);
+
+ // Set a new promise in order to:
+ // 1) Avoid leaking the targetFront we just resolved into the previous promise.
+ // 2) Never return an empty target from `getTarget`
+ //
+ // About the second point:
+ // There is a race condition where we call `onTargetDestroyed` (which clears `this.targetFront`)
+ // a bit before calling `setTarget`. So that `this.targetFront` could be null,
+ // while we now a new target will eventually come when calling `setTarget`.
+ // Setting a new promise will help wait for the next target while `_targetFront` is null.
+ // Note that `getTarget` first look into `_targetFront` before checking for `_targetFrontPromise`.
+ this._targetFrontPromise = new Promise(
+ r => (this._resolveTargetFrontPromise = r)
+ );
+ }
+ }
+ getCachedTarget() {
+ return this._targetFront;
+ }
+ async getTarget() {
+ if (this._targetFront && !this._targetFront.isDestroyed()) {
+ return this._targetFront;
+ }
+
+ if (this._targetFrontPromise) {
+ return this._targetFrontPromise;
+ }
+
+ this._targetFrontPromise = (async () => {
+ let newTargetFront = null;
+ try {
+ const targetForm = await super.getTarget();
+ newTargetFront = this._createTabTarget(targetForm);
+ this.setTarget(newTargetFront);
+ } catch (e) {
+ console.log(
+ `Request to connect to TabDescriptor "${this.id}" failed: ${e}`
+ );
+ }
+
+ this._targetFrontPromise = null;
+ return newTargetFront;
+ })();
+ return this._targetFrontPromise;
+ }
+
+ /**
+ * Handle tabs events.
+ */
+ async _handleTabEvent(event) {
+ switch (event.type) {
+ case "TabClose":
+ // Always destroy the toolbox opened for this local tab descriptor.
+ // When the toolbox is in a Window Host, it won't be removed from the
+ // DOM when the tab is closed.
+ const toolbox = gDevTools.getToolboxForDescriptorFront(this);
+ if (toolbox) {
+ // Toolbox.destroy will call target.destroy eventually.
+ await toolbox.destroy();
+ }
+ break;
+ case "TabRemotenessChange":
+ this._onRemotenessChange();
+ break;
+ }
+ }
+
+ /**
+ * Automatically respawn the toolbox when the tab changes between being
+ * loaded within the parent process and loaded from a content process.
+ * Process change can go in both ways.
+ */
+ async _onRemotenessChange() {
+ // In a near future, this client side code should be replaced by actor code,
+ // notifying about new tab targets.
+ this.emit("remoteness-change", this._targetFront);
+ }
+}
+
+exports.TabDescriptorFront = TabDescriptorFront;
+registerFront(TabDescriptorFront);
diff --git a/devtools/client/fronts/descriptors/webextension.js b/devtools/client/fronts/descriptors/webextension.js
new file mode 100644
index 0000000000..5db86ee5f8
--- /dev/null
+++ b/devtools/client/fronts/descriptors/webextension.js
@@ -0,0 +1,173 @@
+/* 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 {
+ webExtensionDescriptorSpec,
+} = require("resource://devtools/shared/specs/descriptors/webextension.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ DescriptorMixin,
+} = require("resource://devtools/client/fronts/descriptors/descriptor-mixin.js");
+const DESCRIPTOR_TYPES = require("resource://devtools/client/fronts/descriptors/descriptor-types.js");
+loader.lazyRequireGetter(
+ this,
+ "WindowGlobalTargetFront",
+ "resource://devtools/client/fronts/targets/window-global.js",
+ true
+);
+
+class WebExtensionDescriptorFront extends DescriptorMixin(
+ FrontClassWithSpec(webExtensionDescriptorSpec)
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this.traits = {};
+ }
+
+ descriptorType = DESCRIPTOR_TYPES.EXTENSION;
+
+ form(json) {
+ this.actorID = json.actor;
+
+ // Do not use `form` name to avoid colliding with protocol.js's `form` method
+ this._form = json;
+ this.traits = json.traits || {};
+ }
+
+ get backgroundScriptStatus() {
+ return this._form.backgroundScriptStatus;
+ }
+
+ get debuggable() {
+ return this._form.debuggable;
+ }
+
+ get hidden() {
+ return this._form.hidden;
+ }
+
+ get iconDataURL() {
+ return this._form.iconDataURL;
+ }
+
+ get iconURL() {
+ return this._form.iconURL;
+ }
+
+ get id() {
+ return this._form.id;
+ }
+
+ get isSystem() {
+ return this._form.isSystem;
+ }
+
+ get isWebExtensionDescriptor() {
+ return true;
+ }
+
+ get isWebExtension() {
+ return this._form.isWebExtension;
+ }
+
+ get manifestURL() {
+ return this._form.manifestURL;
+ }
+
+ get name() {
+ return this._form.name;
+ }
+
+ get persistentBackgroundScript() {
+ return this._form.persistentBackgroundScript;
+ }
+
+ get temporarilyInstalled() {
+ return this._form.temporarilyInstalled;
+ }
+
+ get url() {
+ return this._form.url;
+ }
+
+ get warnings() {
+ return this._form.warnings;
+ }
+
+ isServerTargetSwitchingEnabled() {
+ // For now, we don't expose any target out of the WatcherActor.
+ // And the top level target is still manually instantiated by the descriptor.
+ // We most likely need to wait for full enabling of EFT before being able to spawn
+ // the extension target from the server side as doing this would most likely break
+ // the iframe dropdown. It would break it as spawning the targets from the server
+ // would probably mean getting rid of the usage of WindowGlobalTargetActor._setWindow
+ // and instead spawn one target per extension document.
+ // That, instead of having a unique target for all the documents.
+ return false;
+ }
+
+ getWatcher() {
+ return super.getWatcher({
+ isServerTargetSwitchingEnabled: this.isServerTargetSwitchingEnabled(),
+ });
+ }
+
+ _createWebExtensionTarget(form) {
+ const front = new WindowGlobalTargetFront(this.conn, null, this);
+ front.form(form);
+ this.manage(front);
+ return front;
+ }
+
+ /**
+ * Retrieve the WindowGlobalTargetFront representing a
+ * WebExtensionTargetActor if this addon is a webextension.
+ *
+ * WebExtensionDescriptors will be created for any type of addon type
+ * (webextension, search plugin, themes). Only webextensions can be targets.
+ * This method will throw for other addon types.
+ *
+ * TODO: We should filter out non-webextension & non-debuggable addons on the
+ * server to avoid the isWebExtension check here. See Bug 1644355.
+ */
+ async getTarget() {
+ if (!this.isWebExtension) {
+ throw new Error(
+ "Tried to create a target for an addon which is not a webextension: " +
+ this.actorID
+ );
+ }
+
+ if (this._targetFront && !this._targetFront.isDestroyed()) {
+ return this._targetFront;
+ }
+
+ if (this._targetFrontPromise) {
+ return this._targetFrontPromise;
+ }
+
+ this._targetFrontPromise = (async () => {
+ let targetFront = null;
+ try {
+ const targetForm = await super.getTarget();
+ targetFront = this._createWebExtensionTarget(targetForm);
+ } catch (e) {
+ console.log(
+ `Request to connect to WebExtensionDescriptor "${this.id}" failed: ${e}`
+ );
+ }
+ this._targetFront = targetFront;
+ this._targetFrontPromise = null;
+ return targetFront;
+ })();
+ return this._targetFrontPromise;
+ }
+}
+
+exports.WebExtensionDescriptorFront = WebExtensionDescriptorFront;
+registerFront(WebExtensionDescriptorFront);
diff --git a/devtools/client/fronts/descriptors/worker.js b/devtools/client/fronts/descriptors/worker.js
new file mode 100644
index 0000000000..47f016f15f
--- /dev/null
+++ b/devtools/client/fronts/descriptors/worker.js
@@ -0,0 +1,146 @@
+/* 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 {
+ workerDescriptorSpec,
+} = require("resource://devtools/shared/specs/descriptors/worker.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ TargetMixin,
+} = require("resource://devtools/client/fronts/targets/target-mixin.js");
+const {
+ DescriptorMixin,
+} = require("resource://devtools/client/fronts/descriptors/descriptor-mixin.js");
+const DESCRIPTOR_TYPES = require("resource://devtools/client/fronts/descriptors/descriptor-types.js");
+
+class WorkerDescriptorFront extends DescriptorMixin(
+ TargetMixin(FrontClassWithSpec(workerDescriptorSpec))
+) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this.traits = {};
+ }
+
+ descriptorType = DESCRIPTOR_TYPES.WORKER;
+
+ form(json) {
+ this.actorID = json.actor;
+ this.id = json.id;
+
+ // 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._url = json.url;
+ this.type = json.type;
+ this.scope = json.scope;
+ this.fetch = json.fetch;
+ this.traits = json.traits;
+ }
+
+ get name() {
+ // this._url is nullified in TargetMixin#destroy.
+ if (!this.url) {
+ return null;
+ }
+
+ return this.url.split("/").pop();
+ }
+
+ get isWorkerDescriptor() {
+ return true;
+ }
+
+ get isDedicatedWorker() {
+ return this.type === Ci.nsIWorkerDebugger.TYPE_DEDICATED;
+ }
+
+ get isSharedWorker() {
+ return this.type === Ci.nsIWorkerDebugger.TYPE_SHARED;
+ }
+
+ get isServiceWorker() {
+ return this.type === Ci.nsIWorkerDebugger.TYPE_SERVICE;
+ }
+
+ // For now, WorkerDescriptor is morphed into a WorkerTarget when calling this method.
+ // Ideally, we would split this into two distinct classes.
+ async morphWorkerDescriptorIntoWorkerTarget() {
+ // temporary, will be moved once we have a target actor
+ return this.getTarget();
+ }
+
+ async getTarget() {
+ if (this._attach) {
+ return this._attach;
+ }
+
+ this._attach = (async () => {
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return this;
+ }
+
+ if (this.isServiceWorker) {
+ this.registration = await this._getRegistrationIfActive();
+ if (this.registration) {
+ await this.registration.preventShutdown();
+ }
+ }
+
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return this;
+ }
+
+ const workerTargetForm = await super.getTarget();
+
+ // Set the console and thread actor IDs on the form so it is accessible by TargetMixin.getFront
+ this.targetForm.consoleActor = workerTargetForm.consoleActor;
+ this.targetForm.threadActor = workerTargetForm.threadActor;
+ this.targetForm.tracerActor = workerTargetForm.tracerActor;
+
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return this;
+ }
+
+ return this;
+ })();
+ return this._attach;
+ }
+
+ async detach() {
+ try {
+ await super.detach();
+
+ if (this.registration) {
+ // Bug 1644772 - Sometimes, the Browser Toolbox fails opening with a connection timeout
+ // with an exception related to this call to allowShutdown and its usage of detachDebugger API.
+ await this.registration.allowShutdown();
+ this.registration = null;
+ }
+ } catch (e) {
+ this.logDetachError(e, "worker");
+ }
+ }
+
+ async _getRegistrationIfActive() {
+ const { registrations } =
+ await this.client.mainRoot.listServiceWorkerRegistrations();
+ return registrations.find(({ activeWorker }) => {
+ return activeWorker && this.id === activeWorker.id;
+ });
+ }
+
+ 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.WorkerDescriptorFront = WorkerDescriptorFront;
+registerFront(exports.WorkerDescriptorFront);
diff --git a/devtools/client/fronts/device.js b/devtools/client/fronts/device.js
new file mode 100644
index 0000000000..47f57c30b8
--- /dev/null
+++ b/devtools/client/fronts/device.js
@@ -0,0 +1,23 @@
+/* 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 { deviceSpec } = require("resource://devtools/shared/specs/device.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+
+class DeviceFront extends FrontClassWithSpec(deviceSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "deviceActor";
+ }
+}
+
+exports.DeviceFront = DeviceFront;
+registerFront(DeviceFront);
diff --git a/devtools/client/fronts/frame.js b/devtools/client/fronts/frame.js
new file mode 100644
index 0000000000..525f0d1170
--- /dev/null
+++ b/devtools/client/fronts/frame.js
@@ -0,0 +1,27 @@
+/* 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 { frameSpec } = require("resource://devtools/shared/specs/frame.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+
+class FrameFront extends FrontClassWithSpec(frameSpec) {
+ form(json) {
+ this.displayName = json.displayName;
+ this.arguments = json.arguments;
+ this.type = json.type;
+ this.where = json.where;
+ this.this = json.this;
+ this.data = json;
+ this.asyncCause = json.asyncCause;
+ this.state = json.state;
+ }
+}
+
+module.exports = FrameFront;
+registerFront(FrameFront);
diff --git a/devtools/client/fronts/highlighters.js b/devtools/client/fronts/highlighters.js
new file mode 100644
index 0000000000..b1e159e5fc
--- /dev/null
+++ b/devtools/client/fronts/highlighters.js
@@ -0,0 +1,53 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ customHighlighterSpec,
+} = require("resource://devtools/shared/specs/highlighters.js");
+const {
+ safeAsyncMethod,
+} = require("resource://devtools/shared/async-utils.js");
+
+class CustomHighlighterFront extends FrontClassWithSpec(customHighlighterSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // show/hide requests can be triggered while DevTools are closing.
+ this.show = safeAsyncMethod(this.show.bind(this), () => this.isDestroyed());
+ this.hide = safeAsyncMethod(this.hide.bind(this), () => this.isDestroyed());
+
+ this._isShown = false;
+ }
+
+ show(...args) {
+ this._isShown = true;
+ return super.show(...args);
+ }
+
+ hide() {
+ this._isShown = false;
+ return super.hide();
+ }
+
+ isShown() {
+ return this._isShown;
+ }
+
+ destroy() {
+ if (this.isDestroyed()) {
+ return;
+ }
+ super.finalize(); // oneway call, doesn't expect a response.
+ super.destroy();
+ }
+}
+
+exports.CustomHighlighterFront = CustomHighlighterFront;
+registerFront(CustomHighlighterFront);
diff --git a/devtools/client/fronts/inspector.js b/devtools/client/fronts/inspector.js
new file mode 100644
index 0000000000..116993078b
--- /dev/null
+++ b/devtools/client/fronts/inspector.js
@@ -0,0 +1,282 @@
+/* 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 Telemetry = require("resource://devtools/client/shared/telemetry.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ inspectorSpec,
+} = require("resource://devtools/shared/specs/inspector.js");
+
+loader.lazyRequireGetter(
+ this,
+ "captureScreenshot",
+ "resource://devtools/client/shared/screenshot.js",
+ true
+);
+
+const TELEMETRY_EYEDROPPER_OPENED = "DEVTOOLS_EYEDROPPER_OPENED_COUNT";
+const TELEMETRY_EYEDROPPER_OPENED_MENU =
+ "DEVTOOLS_MENU_EYEDROPPER_OPENED_COUNT";
+const SHOW_ALL_ANONYMOUS_CONTENT_PREF =
+ "devtools.inspector.showAllAnonymousContent";
+
+const telemetry = new Telemetry();
+
+/**
+ * Client side of the inspector actor, which is used to create
+ * inspector-related actors, including the walker.
+ */
+class InspectorFront extends FrontClassWithSpec(inspectorSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this._client = client;
+ this._highlighters = new Map();
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "inspectorActor";
+
+ // Map of highlighter types to unsettled promises to create a highlighter of that type
+ this._pendingGetHighlighterMap = new Map();
+
+ this.noopStylesheetListener = () => {};
+ }
+
+ // async initialization
+ async initialize() {
+ if (this.initialized) {
+ return this.initialized;
+ }
+
+ // Watch STYLESHEET resources to fill the ResourceCommand cache.
+ // StyleRule front's `get parentStyleSheet()` will query the cache to
+ // retrieve the resource corresponding to the parent stylesheet of a rule.
+ const { resourceCommand } = this.targetFront.commands;
+ // Backup resourceCommand, targetFront.commands might be null in `destroy`.
+ this.resourceCommand = resourceCommand;
+ await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
+ onAvailable: this.noopStylesheetListener,
+ });
+
+ // Bail out if the inspector is closed while watchResources was pending
+ if (this.isDestroyed()) {
+ return null;
+ }
+
+ this.initialized = await Promise.all([
+ this._getWalker(),
+ this._getPageStyle(),
+ ]);
+
+ return this.initialized;
+ }
+
+ async _getWalker() {
+ const showAllAnonymousContent = Services.prefs.getBoolPref(
+ SHOW_ALL_ANONYMOUS_CONTENT_PREF
+ );
+ this.walker = await this.getWalker({
+ showAllAnonymousContent,
+ });
+
+ // We need to reparent the RootNode of remote iframe Walkers
+ // so that their parent is the NodeFront of the <iframe>
+ // element, coming from another process/target/WalkerFront.
+ await this.walker.reparentRemoteFrame();
+ }
+
+ hasHighlighter(type) {
+ return this._highlighters.has(type);
+ }
+
+ async _getPageStyle() {
+ this.pageStyle = await super.getPageStyle();
+ }
+
+ async getCompatibilityFront() {
+ if (!this._compatibility) {
+ this._compatibility = await super.getCompatibility();
+ }
+
+ return this._compatibility;
+ }
+
+ destroy() {
+ if (this.isDestroyed()) {
+ return;
+ }
+ this._compatibility = null;
+
+ const { resourceCommand } = this;
+ resourceCommand.unwatchResources([resourceCommand.TYPES.STYLESHEET], {
+ onAvailable: this.noopStylesheetListener,
+ });
+ this.resourceCommand = null;
+
+ this.walker = null;
+
+ // CustomHighlighter fronts are managed by InspectorFront and so will be
+ // automatically destroyed. But we have to clear the `_highlighters`
+ // Map as well as explicitly call `finalize` request on all of them.
+ this.destroyHighlighters();
+ super.destroy();
+ }
+
+ destroyHighlighters() {
+ for (const type of this._highlighters.keys()) {
+ if (this._highlighters.has(type)) {
+ const highlighter = this._highlighters.get(type);
+ if (!highlighter.isDestroyed()) {
+ highlighter.finalize();
+ }
+ this._highlighters.delete(type);
+ }
+ }
+ }
+
+ async getHighlighterByType(typeName) {
+ let highlighter = null;
+ try {
+ highlighter = await super.getHighlighterByType(typeName);
+ } catch (_) {
+ throw new Error(
+ "The target doesn't support " +
+ `creating highlighters by types or ${typeName} is unknown`
+ );
+ }
+ return highlighter;
+ }
+
+ getKnownHighlighter(type) {
+ return this._highlighters.get(type);
+ }
+
+ /**
+ * Return a highlighter instance of the given type.
+ * If an instance was previously created, return it. Else, create and return a new one.
+ *
+ * Store a promise for the request to create a new highlighter. If another request
+ * comes in before that promise is resolved, wait for it to resolve and return the
+ * highlighter instance it resolved with instead of creating a new request.
+ *
+ * @param {String} type
+ * Highlighter type
+ * @return {Promise}
+ * Promise which resolves with a highlighter instance of the given type
+ */
+ async getOrCreateHighlighterByType(type) {
+ let front = this._highlighters.get(type);
+ let pendingGetHighlighter = this._pendingGetHighlighterMap.get(type);
+
+ if (!front && !pendingGetHighlighter) {
+ pendingGetHighlighter = (async () => {
+ const highlighter = await this.getHighlighterByType(type);
+ this._highlighters.set(type, highlighter);
+ this._pendingGetHighlighterMap.delete(type);
+ return highlighter;
+ })();
+
+ this._pendingGetHighlighterMap.set(type, pendingGetHighlighter);
+ }
+
+ if (pendingGetHighlighter) {
+ front = await pendingGetHighlighter;
+ }
+
+ return front;
+ }
+
+ async pickColorFromPage(options) {
+ let screenshot = null;
+
+ // @backward-compat { version 87 } ScreenshotContentActor was only added in 87.
+ // When connecting to older server, the eyedropper will use drawWindow
+ // to retrieve the screenshot of the page (that's a decent fallback,
+ // even if it doesn't handle remote frames).
+ if (this.targetFront.hasActor("screenshotContent")) {
+ try {
+ // We use the screenshot actors as it can retrieve an image of the current viewport,
+ // handling remote frame if need be.
+ const { data } = await captureScreenshot(this.targetFront, {
+ browsingContextID: this.targetFront.browsingContextID,
+ disableFlash: true,
+ ignoreDprForFileScale: true,
+ });
+ screenshot = data;
+ } catch (e) {
+ // We simply log the error and still call pickColorFromPage as it will default to
+ // use drawWindow in order to get the screenshot of the page (that's a decent
+ // fallback, even if it doesn't handle remote frames).
+ console.error(
+ "Error occured when taking a screenshot for the eyedropper",
+ e
+ );
+ }
+ }
+
+ await super.pickColorFromPage({
+ ...options,
+ screenshot,
+ });
+
+ if (options?.fromMenu) {
+ telemetry.getHistogramById(TELEMETRY_EYEDROPPER_OPENED_MENU).add(true);
+ } else {
+ telemetry.getHistogramById(TELEMETRY_EYEDROPPER_OPENED).add(true);
+ }
+ }
+
+ /**
+ * Given a node grip, return a NodeFront on the right context.
+ *
+ * @param {Object} grip: The node grip.
+ * @returns {Promise<NodeFront|null>} A promise that resolves with a NodeFront or null
+ * if the NodeFront couldn't be created/retrieved.
+ */
+ async getNodeFrontFromNodeGrip(grip) {
+ return this.getNodeActorFromContentDomReference(grip.contentDomReference);
+ }
+
+ async getNodeActorFromContentDomReference(contentDomReference) {
+ const { browsingContextId } = contentDomReference;
+ // If the contentDomReference lives in the same browsing context id than the
+ // current one, we can directly use the current walker.
+ if (this.targetFront.browsingContextID === browsingContextId) {
+ return this.walker.getNodeActorFromContentDomReference(
+ contentDomReference
+ );
+ }
+
+ // If the contentDomReference has a different browsing context than the current one,
+ // we are either in Fission or in the Multiprocess Browser Toolbox, so we need to
+ // retrieve the walker of the WindowGlobalTarget.
+ // Get the target for this remote frame element
+
+ // Tab and Process Descriptors expose a Watcher, which should be used to
+ // fetch the node's target.
+ let target;
+ const { watcherFront } = this.targetFront.commands;
+ if (watcherFront) {
+ target = await watcherFront.getWindowGlobalTarget(browsingContextId);
+ } else {
+ // 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 getNodeActorFromContentDomReference for ${this.targetFront.actorID}`
+ );
+ }
+ const { walker } = await target.getFront("inspector");
+ return walker.getNodeActorFromContentDomReference(contentDomReference);
+ }
+}
+
+exports.InspectorFront = InspectorFront;
+registerFront(InspectorFront);
diff --git a/devtools/client/fronts/inspector/moz.build b/devtools/client/fronts/inspector/moz.build
new file mode 100644
index 0000000000..de635f5947
--- /dev/null
+++ b/devtools/client/fronts/inspector/moz.build
@@ -0,0 +1,9 @@
+# -*- 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(
+ "rule-rewriter.js",
+)
diff --git a/devtools/client/fronts/inspector/rule-rewriter.js b/devtools/client/fronts/inspector/rule-rewriter.js
new file mode 100644
index 0000000000..30d1cf88d2
--- /dev/null
+++ b/devtools/client/fronts/inspector/rule-rewriter.js
@@ -0,0 +1,745 @@
+/* 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 holds various CSS parsing and rewriting utilities.
+// Some entry points of note are:
+// parseDeclarations - parse a CSS rule into declarations
+// RuleRewriter - rewrite CSS rule text
+// parsePseudoClassesAndAttributes - parse selector and extract
+// pseudo-classes
+// parseSingleValue - parse a single CSS property value
+
+"use strict";
+
+const { getCSSLexer } = require("resource://devtools/shared/css/lexer.js");
+const {
+ COMMENT_PARSING_HEURISTIC_BYPASS_CHAR,
+ escapeCSSComment,
+ parseNamedDeclarations,
+ unescapeCSSComment,
+} = require("resource://devtools/shared/css/parsing-utils.js");
+
+loader.lazyRequireGetter(
+ this,
+ ["getIndentationFromPrefs", "getIndentationFromString"],
+ "resource://devtools/shared/indentation.js",
+ true
+);
+
+// Used to test whether a newline appears anywhere in some text.
+const NEWLINE_RX = /[\r\n]/;
+// Used to test whether a bit of text starts an empty comment, either
+// an "ordinary" /* ... */ comment, or a "heuristic bypass" comment
+// like /*! ... */.
+const EMPTY_COMMENT_START_RX = /^\/\*!?[ \r\n\t\f]*$/;
+// Used to test whether a bit of text ends an empty comment.
+const EMPTY_COMMENT_END_RX = /^[ \r\n\t\f]*\*\//;
+// Used to test whether a string starts with a blank line.
+const BLANK_LINE_RX = /^[ \t]*(?:\r\n|\n|\r|\f|$)/;
+
+/**
+ * Return an object that can be used to rewrite declarations in some
+ * source text. The source text and parsing are handled in the same
+ * way as @see parseNamedDeclarations, with |parseComments| being true.
+ * Rewriting is done by calling one of the modification functions like
+ * setPropertyEnabled. The returned object has the same interface
+ * as @see RuleModificationList.
+ *
+ * An example showing how to disable the 3rd property in a rule:
+ *
+ * let rewriter = new RuleRewriter(isCssPropertyKnown, ruleActor,
+ * ruleActor.authoredText);
+ * rewriter.setPropertyEnabled(3, "color", false);
+ * rewriter.apply().then(() => { ... the change is made ... });
+ *
+ * The exported rewriting methods are |renameProperty|, |setPropertyEnabled|,
+ * |createProperty|, |setProperty|, and |removeProperty|. The |apply|
+ * method can be used to send the edited text to the StyleRuleActor;
+ * |getDefaultIndentation| is useful for the methods requiring a
+ * default indentation value; and |getResult| is useful for testing.
+ *
+ * Additionally, editing will set the |changedDeclarations| property
+ * on this object. This property has the same form as the |changed|
+ * property of the object returned by |getResult|.
+ *
+ * @param {Function} isCssPropertyKnown
+ * A function to check if the CSS property is known. This is either an
+ * internal server function or from the CssPropertiesFront.
+ * that are supported by the server. Note that if Bug 1222047
+ * is completed then isCssPropertyKnown will not need to be passed in.
+ * The CssProperty front will be able to obtained directly from the
+ * RuleRewriter.
+ * @param {StyleRuleFront} rule The style rule to use. Note that this
+ * is only needed by the |apply| and |getDefaultIndentation| methods;
+ * and in particular for testing it can be |null|.
+ * @param {String} inputString The CSS source text to parse and modify.
+ * @return {Object} an object that can be used to rewrite the input text.
+ */
+function RuleRewriter(isCssPropertyKnown, rule, inputString) {
+ this.rule = rule;
+ this.isCssPropertyKnown = isCssPropertyKnown;
+ // The RuleRewriter sends CSS rules as text to the server, but with this modifications
+ // array, it also sends the list of changes so the server doesn't have to re-parse the
+ // rule if it needs to track what changed.
+ this.modifications = [];
+
+ // Keep track of which any declarations we had to rewrite while
+ // performing the requested action.
+ this.changedDeclarations = {};
+
+ // If not null, a promise that must be wait upon before |apply| can
+ // do its work.
+ this.editPromise = null;
+
+ // If the |defaultIndentation| property is set, then it is used;
+ // otherwise the RuleRewriter will try to compute the default
+ // indentation based on the style sheet's text. This override
+ // facility is for testing.
+ this.defaultIndentation = null;
+
+ this.startInitialization(inputString);
+}
+
+RuleRewriter.prototype = {
+ /**
+ * An internal function to initialize the rewriter with a given
+ * input string.
+ *
+ * @param {String} inputString the input to use
+ */
+ startInitialization(inputString) {
+ this.inputString = inputString;
+ // Whether there are any newlines in the input text.
+ this.hasNewLine = /[\r\n]/.test(this.inputString);
+ // The declarations.
+ this.declarations = parseNamedDeclarations(
+ this.isCssPropertyKnown,
+ this.inputString,
+ true
+ );
+ this.decl = null;
+ this.result = null;
+ },
+
+ /**
+ * An internal function to complete initialization and set some
+ * properties for further processing.
+ *
+ * @param {Number} index The index of the property to modify
+ */
+ completeInitialization(index) {
+ if (index < 0) {
+ throw new Error("Invalid index " + index + ". Expected positive integer");
+ }
+ // |decl| is the declaration to be rewritten, or null if there is no
+ // declaration corresponding to |index|.
+ // |result| is used to accumulate the result text.
+ if (index < this.declarations.length) {
+ this.decl = this.declarations[index];
+ this.result = this.inputString.substring(0, this.decl.offsets[0]);
+ } else {
+ this.decl = null;
+ this.result = this.inputString;
+ }
+ },
+
+ /**
+ * A helper function to compute the indentation of some text. This
+ * examines the rule's existing text to guess the indentation to use;
+ * unlike |getDefaultIndentation|, which examines the entire style
+ * sheet.
+ *
+ * @param {String} string the input text
+ * @param {Number} offset the offset at which to compute the indentation
+ * @return {String} the indentation at the indicated position
+ */
+ getIndentation(string, offset) {
+ let originalOffset = offset;
+ for (--offset; offset >= 0; --offset) {
+ const c = string[offset];
+ if (c === "\r" || c === "\n" || c === "\f") {
+ return string.substring(offset + 1, originalOffset);
+ }
+ if (c !== " " && c !== "\t") {
+ // Found some non-whitespace character before we found a newline
+ // -- let's reset the starting point and keep going, as we saw
+ // something on the line before the declaration.
+ originalOffset = offset;
+ }
+ }
+ // Ran off the end.
+ return "";
+ },
+
+ /**
+ * Modify a property value to ensure it is "lexically safe" for
+ * insertion into a style sheet. This function doesn't attempt to
+ * ensure that the resulting text is a valid value for the given
+ * property; but rather just that inserting the text into the style
+ * sheet will not cause unwanted changes to other rules or
+ * declarations.
+ *
+ * @param {String} text The input text. This should include the trailing ";".
+ * @return {Array} An array of the form [anySanitized, text], where
+ * |anySanitized| is a boolean that indicates
+ * whether anything substantive has changed; and
+ * where |text| is the text that has been rewritten
+ * to be "lexically safe".
+ */
+ sanitizePropertyValue(text) {
+ // Start by stripping any trailing ";". This is done here to
+ // avoid the case where the user types "url(" (which is turned
+ // into "url(;" by the rule view before coming here), being turned
+ // into "url(;)" by this code -- due to the way "url(...)" is
+ // parsed as a single token.
+ text = text.replace(/;$/, "");
+ const lexer = getCSSLexer(text);
+
+ let result = "";
+ let previousOffset = 0;
+ const parenStack = [];
+ let anySanitized = false;
+
+ // Push a closing paren on the stack.
+ const pushParen = (token, closer) => {
+ result =
+ result +
+ text.substring(previousOffset, token.startOffset) +
+ text.substring(token.startOffset, token.endOffset);
+ // We set the location of the paren in a funny way, to handle
+ // the case where we've seen a function token, where the paren
+ // appears at the end.
+ parenStack.push({ closer, offset: result.length - 1 });
+ previousOffset = token.endOffset;
+ };
+
+ // Pop a closing paren from the stack.
+ const popSomeParens = closer => {
+ while (parenStack.length) {
+ const paren = parenStack.pop();
+
+ if (paren.closer === closer) {
+ return true;
+ }
+
+ // Found a non-matching closing paren, so quote it. Note that
+ // these are processed in reverse order.
+ result =
+ result.substring(0, paren.offset) +
+ "\\" +
+ result.substring(paren.offset);
+ anySanitized = true;
+ }
+ return false;
+ };
+
+ while (true) {
+ const token = lexer.nextToken();
+ if (!token) {
+ break;
+ }
+
+ if (token.tokenType === "symbol") {
+ switch (token.text) {
+ case ";":
+ // We simply drop the ";" here. This lets us cope with
+ // declarations that don't have a ";" and also other
+ // termination. The caller handles adding the ";" again.
+ result += text.substring(previousOffset, token.startOffset);
+ previousOffset = token.endOffset;
+ break;
+
+ case "{":
+ pushParen(token, "}");
+ break;
+
+ case "(":
+ pushParen(token, ")");
+ break;
+
+ case "[":
+ pushParen(token, "]");
+ break;
+
+ case "}":
+ case ")":
+ case "]":
+ // Did we find an unmatched close bracket?
+ if (!popSomeParens(token.text)) {
+ // Copy out text from |previousOffset|.
+ result += text.substring(previousOffset, token.startOffset);
+ // Quote the offending symbol.
+ result += "\\" + token.text;
+ previousOffset = token.endOffset;
+ anySanitized = true;
+ }
+ break;
+ }
+ } else if (token.tokenType === "function") {
+ pushParen(token, ")");
+ }
+ }
+
+ // Fix up any unmatched parens.
+ popSomeParens(null);
+
+ // Copy out any remaining text, then any needed terminators.
+ result += text.substring(previousOffset, text.length);
+ const eofFixup = lexer.performEOFFixup("", true);
+ if (eofFixup) {
+ anySanitized = true;
+ result += eofFixup;
+ }
+ return [anySanitized, result];
+ },
+
+ /**
+ * Start at |index| and skip whitespace
+ * backward in |string|. Return the index of the first
+ * non-whitespace character, or -1 if the entire string was
+ * whitespace.
+ * @param {String} string the input string
+ * @param {Number} index the index at which to start
+ * @return {Number} index of the first non-whitespace character, or -1
+ */
+ skipWhitespaceBackward(string, index) {
+ for (
+ --index;
+ index >= 0 && (string[index] === " " || string[index] === "\t");
+ --index
+ ) {
+ // Nothing.
+ }
+ return index;
+ },
+
+ /**
+ * Terminate a given declaration, if needed.
+ *
+ * @param {Number} index The index of the rule to possibly
+ * terminate. It might be invalid, so this
+ * function must check for that.
+ */
+ maybeTerminateDecl(index) {
+ if (
+ index < 0 ||
+ index >= this.declarations.length ||
+ // No need to rewrite declarations in comments.
+ "commentOffsets" in this.declarations[index]
+ ) {
+ return;
+ }
+
+ const termDecl = this.declarations[index];
+ let endIndex = termDecl.offsets[1];
+ // Due to an oddity of the lexer, we might have gotten a bit of
+ // extra whitespace in a trailing bad_url token -- so be sure to
+ // skip that as well.
+ endIndex = this.skipWhitespaceBackward(this.result, endIndex) + 1;
+
+ const trailingText = this.result.substring(endIndex);
+ if (termDecl.terminator) {
+ // Insert the terminator just at the end of the declaration,
+ // before any trailing whitespace.
+ this.result =
+ this.result.substring(0, endIndex) + termDecl.terminator + trailingText;
+ // In a couple of cases, we may have had to add something to
+ // terminate the declaration, but the termination did not
+ // actually affect the property's value -- and at this spot, we
+ // only care about reporting value changes. In particular, we
+ // might have added a plain ";", or we might have terminated a
+ // comment with "*/;". Neither of these affect the value.
+ if (termDecl.terminator !== ";" && termDecl.terminator !== "*/;") {
+ this.changedDeclarations[index] =
+ termDecl.value + termDecl.terminator.slice(0, -1);
+ }
+ }
+ // If the rule generally has newlines, but this particular
+ // declaration doesn't have a trailing newline, insert one now.
+ // Maybe this style is too weird to bother with.
+ if (this.hasNewLine && !NEWLINE_RX.test(trailingText)) {
+ this.result += "\n";
+ }
+ },
+
+ /**
+ * Sanitize the given property value and return the sanitized form.
+ * If the property is rewritten during sanitization, make a note in
+ * |changedDeclarations|.
+ *
+ * @param {String} text The property text.
+ * @param {Number} index The index of the property.
+ * @return {String} The sanitized text.
+ */
+ sanitizeText(text, index) {
+ const [anySanitized, sanitizedText] = this.sanitizePropertyValue(text);
+ if (anySanitized) {
+ this.changedDeclarations[index] = sanitizedText;
+ }
+ return sanitizedText;
+ },
+
+ /**
+ * Rename a declaration.
+ *
+ * @param {Number} index index of the property in the rule.
+ * @param {String} name current name of the property
+ * @param {String} newName new name of the property
+ */
+ renameProperty(index, name, newName) {
+ this.completeInitialization(index);
+ this.result += CSS.escape(newName);
+ // We could conceivably compute the name offsets instead so we
+ // could preserve white space and comments on the LHS of the ":".
+ this.completeCopying(this.decl.colonOffsets[0]);
+ this.modifications.push({ type: "set", index, name, newName });
+ },
+
+ /**
+ * Enable or disable a declaration
+ *
+ * @param {Number} index index of the property in the rule.
+ * @param {String} name current name of the property
+ * @param {Boolean} isEnabled true if the property should be enabled;
+ * false if it should be disabled
+ */
+ setPropertyEnabled(index, name, isEnabled) {
+ this.completeInitialization(index);
+ const decl = this.decl;
+ const priority = decl.priority;
+ let copyOffset = decl.offsets[1];
+ if (isEnabled) {
+ // Enable it. First see if the comment start can be deleted.
+ const commentStart = decl.commentOffsets[0];
+ if (EMPTY_COMMENT_START_RX.test(this.result.substring(commentStart))) {
+ this.result = this.result.substring(0, commentStart);
+ } else {
+ this.result += "*/ ";
+ }
+
+ // Insert the name and value separately, so we can report
+ // sanitization changes properly.
+ const commentNamePart = this.inputString.substring(
+ decl.offsets[0],
+ decl.colonOffsets[1]
+ );
+ this.result += unescapeCSSComment(commentNamePart);
+
+ // When uncommenting, we must be sure to sanitize the text, to
+ // avoid things like /* decl: }; */, which will be accepted as
+ // a property but which would break the entire style sheet.
+ let newText = this.inputString.substring(
+ decl.colonOffsets[1],
+ decl.offsets[1]
+ );
+ newText = cssTrimRight(unescapeCSSComment(newText));
+ this.result += this.sanitizeText(newText, index) + ";";
+
+ // See if the comment end can be deleted.
+ const trailingText = this.inputString.substring(decl.offsets[1]);
+ if (EMPTY_COMMENT_END_RX.test(trailingText)) {
+ copyOffset = decl.commentOffsets[1];
+ } else {
+ this.result += " /*";
+ }
+ } else {
+ // Disable it. Note that we use our special comment syntax
+ // here.
+ const declText = this.inputString.substring(
+ decl.offsets[0],
+ decl.offsets[1]
+ );
+ this.result +=
+ "/*" +
+ COMMENT_PARSING_HEURISTIC_BYPASS_CHAR +
+ " " +
+ escapeCSSComment(declText) +
+ " */";
+ }
+ this.completeCopying(copyOffset);
+
+ if (isEnabled) {
+ this.modifications.push({
+ type: "set",
+ index,
+ name,
+ value: decl.value,
+ priority,
+ });
+ } else {
+ this.modifications.push({ type: "disable", index, name });
+ }
+ },
+
+ /**
+ * Return a promise that will be resolved to the default indentation
+ * of the rule. This is a helper for internalCreateProperty.
+ *
+ * @return {Promise} a promise that will be resolved to a string
+ * that holds the default indentation that should be used
+ * for edits to the rule.
+ */
+ async getDefaultIndentation() {
+ if (!this.rule.parentStyleSheet) {
+ return null;
+ }
+
+ const prefIndent = getIndentationFromPrefs();
+ if (prefIndent) {
+ const { indentUnit, indentWithTabs } = prefIndent;
+ return indentWithTabs ? "\t" : " ".repeat(indentUnit);
+ }
+
+ const styleSheetsFront = await this.rule.targetFront.getFront(
+ "stylesheets"
+ );
+ const { str: source } = await styleSheetsFront.getText(
+ this.rule.parentStyleSheet.resourceId
+ );
+ const { indentUnit, indentWithTabs } = getIndentationFromString(source);
+ return indentWithTabs ? "\t" : " ".repeat(indentUnit);
+ },
+
+ /**
+ * An internal function to create a new declaration. This does all
+ * the work of |createProperty|.
+ *
+ * @param {Number} index index of the property in the rule.
+ * @param {String} name name of the new property
+ * @param {String} value value of the new property
+ * @param {String} priority priority of the new property; either
+ * the empty string or "important"
+ * @param {Boolean} enabled True if the new property should be
+ * enabled, false if disabled
+ * @return {Promise} a promise that is resolved when the edit has
+ * completed
+ */
+ async internalCreateProperty(index, name, value, priority, enabled) {
+ this.completeInitialization(index);
+ let newIndentation = "";
+ if (this.hasNewLine) {
+ if (this.declarations.length) {
+ newIndentation = this.getIndentation(
+ this.inputString,
+ this.declarations[0].offsets[0]
+ );
+ } else if (this.defaultIndentation) {
+ newIndentation = this.defaultIndentation;
+ } else {
+ newIndentation = await this.getDefaultIndentation();
+ }
+ }
+
+ this.maybeTerminateDecl(index - 1);
+
+ // If we generally have newlines, and if skipping whitespace
+ // backward stops at a newline, then insert our text before that
+ // whitespace. This ensures the indentation we computed is what
+ // is actually used.
+ let savedWhitespace = "";
+ if (this.hasNewLine) {
+ const wsOffset = this.skipWhitespaceBackward(
+ this.result,
+ this.result.length
+ );
+ if (this.result[wsOffset] === "\r" || this.result[wsOffset] === "\n") {
+ savedWhitespace = this.result.substring(wsOffset + 1);
+ this.result = this.result.substring(0, wsOffset + 1);
+ }
+ }
+
+ let newText = CSS.escape(name) + ": " + this.sanitizeText(value, index);
+ if (priority === "important") {
+ newText += " !important";
+ }
+ newText += ";";
+
+ if (!enabled) {
+ newText =
+ "/*" +
+ COMMENT_PARSING_HEURISTIC_BYPASS_CHAR +
+ " " +
+ escapeCSSComment(newText) +
+ " */";
+ }
+
+ this.result += newIndentation + newText;
+ if (this.hasNewLine) {
+ this.result += "\n";
+ }
+ this.result += savedWhitespace;
+
+ if (this.decl) {
+ // Still want to copy in the declaration previously at this
+ // index.
+ this.completeCopying(this.decl.offsets[0]);
+ }
+ },
+
+ /**
+ * Create a new declaration.
+ *
+ * @param {Number} index index of the property in the rule.
+ * @param {String} name name of the new property
+ * @param {String} value value of the new property
+ * @param {String} priority priority of the new property; either
+ * the empty string or "important"
+ * @param {Boolean} enabled True if the new property should be
+ * enabled, false if disabled
+ */
+ createProperty(index, name, value, priority, enabled) {
+ this.editPromise = this.internalCreateProperty(
+ index,
+ name,
+ value,
+ priority,
+ enabled
+ );
+ // Log the modification only if the created property is enabled.
+ if (enabled) {
+ this.modifications.push({ type: "set", index, name, value, priority });
+ }
+ },
+
+ /**
+ * Set a declaration's value.
+ *
+ * @param {Number} index index of the property in the rule.
+ * This can be -1 in the case where
+ * the rule does not support setRuleText;
+ * generally for setting properties
+ * on an element's style.
+ * @param {String} name the property's name
+ * @param {String} value the property's value
+ * @param {String} priority the property's priority, either the empty
+ * string or "important"
+ */
+ setProperty(index, name, value, priority) {
+ this.completeInitialization(index);
+ // We might see a "set" on a previously non-existent property; in
+ // that case, act like "create".
+ if (!this.decl) {
+ this.createProperty(index, name, value, priority, true);
+ return;
+ }
+
+ // Note that this assumes that "set" never operates on disabled
+ // properties.
+ this.result +=
+ this.inputString.substring(
+ this.decl.offsets[0],
+ this.decl.colonOffsets[1]
+ ) + this.sanitizeText(value, index);
+
+ if (priority === "important") {
+ this.result += " !important";
+ }
+ this.result += ";";
+ this.completeCopying(this.decl.offsets[1]);
+ this.modifications.push({ type: "set", index, name, value, priority });
+ },
+
+ /**
+ * Remove a declaration.
+ *
+ * @param {Number} index index of the property in the rule.
+ * @param {String} name the name of the property to remove
+ */
+ removeProperty(index, name) {
+ this.completeInitialization(index);
+
+ // If asked to remove a property that does not exist, bail out.
+ if (!this.decl) {
+ return;
+ }
+
+ // If the property is disabled, then first enable it, and then
+ // delete it. We take this approach because we want to remove the
+ // entire comment if possible; but the logic for dealing with
+ // comments is hairy and already implemented in
+ // setPropertyEnabled.
+ if (this.decl.commentOffsets) {
+ this.setPropertyEnabled(index, name, true);
+ this.startInitialization(this.result);
+ this.completeInitialization(index);
+ }
+
+ let copyOffset = this.decl.offsets[1];
+ // Maybe removing this rule left us with a completely blank
+ // line. In this case, we'll delete the whole thing. We only
+ // bother with this if we're looking at sources that already
+ // have a newline somewhere.
+ if (this.hasNewLine) {
+ const nlOffset = this.skipWhitespaceBackward(
+ this.result,
+ this.decl.offsets[0]
+ );
+ if (
+ nlOffset < 0 ||
+ this.result[nlOffset] === "\r" ||
+ this.result[nlOffset] === "\n"
+ ) {
+ const trailingText = this.inputString.substring(copyOffset);
+ const match = BLANK_LINE_RX.exec(trailingText);
+ if (match) {
+ this.result = this.result.substring(0, nlOffset + 1);
+ copyOffset += match[0].length;
+ }
+ }
+ }
+ this.completeCopying(copyOffset);
+ this.modifications.push({ type: "remove", index, name });
+ },
+
+ /**
+ * An internal function to copy any trailing text to the output
+ * string.
+ *
+ * @param {Number} copyOffset Offset into |inputString| of the
+ * final text to copy to the output string.
+ */
+ completeCopying(copyOffset) {
+ // Add the trailing text.
+ this.result += this.inputString.substring(copyOffset);
+ },
+
+ /**
+ * Apply the modifications in this object to the associated rule.
+ *
+ * @return {Promise} A promise which will be resolved when the modifications
+ * are complete.
+ */
+ apply() {
+ return Promise.resolve(this.editPromise).then(() => {
+ return this.rule.setRuleText(this.result, this.modifications);
+ });
+ },
+
+ /**
+ * Get the result of the rewriting. This is used for testing.
+ *
+ * @return {object} an object of the form {changed: object, text: string}
+ * |changed| is an object where each key is
+ * the index of a property whose value had to be
+ * rewritten during the sanitization process, and
+ * whose value is the new text of the property.
+ * |text| is the rewritten text of the rule.
+ */
+ getResult() {
+ return { changed: this.changedDeclarations, text: this.result };
+ },
+};
+
+/**
+ * Like trimRight, but only trims CSS-allowed whitespace.
+ */
+function cssTrimRight(str) {
+ const match = /^(.*?)[ \t\r\n\f]*$/.exec(str);
+ if (match) {
+ return match[1];
+ }
+ return str;
+}
+
+module.exports = RuleRewriter;
diff --git a/devtools/client/fronts/layout.js b/devtools/client/fronts/layout.js
new file mode 100644
index 0000000000..6f84032ba6
--- /dev/null
+++ b/devtools/client/fronts/layout.js
@@ -0,0 +1,184 @@
+/* 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 {
+ safeAsyncMethod,
+} = require("resource://devtools/shared/async-utils.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ flexboxSpec,
+ flexItemSpec,
+ gridSpec,
+ layoutSpec,
+} = require("resource://devtools/shared/specs/layout.js");
+
+class FlexboxFront extends FrontClassWithSpec(flexboxSpec) {
+ form(form) {
+ this._form = form;
+ }
+
+ /**
+ * In some cases, the FlexboxActor already knows the NodeActor ID of the node where the
+ * flexbox is located. In such cases, this getter returns the NodeFront for it.
+ */
+ get containerNodeFront() {
+ if (!this._form.containerNodeActorID) {
+ return null;
+ }
+
+ return this.conn.getFrontByID(this._form.containerNodeActorID);
+ }
+
+ /**
+ * Get the WalkerFront instance that owns this FlexboxFront.
+ */
+ get walkerFront() {
+ return this.parentFront.walkerFront;
+ }
+
+ /**
+ * Get the computed style properties for the flex container.
+ */
+ get properties() {
+ return this._form.properties;
+ }
+}
+
+class FlexItemFront extends FrontClassWithSpec(flexItemSpec) {
+ form(form) {
+ this._form = form;
+ }
+
+ /**
+ * Get the flex item sizing data.
+ */
+ get flexItemSizing() {
+ return this._form.flexItemSizing;
+ }
+
+ /**
+ * In some cases, the FlexItemActor already knows the NodeActor ID of the node where the
+ * flex item is located. In such cases, this getter returns the NodeFront for it.
+ */
+ get nodeFront() {
+ if (!this._form.nodeActorID) {
+ return null;
+ }
+
+ return this.conn.getFrontByID(this._form.nodeActorID);
+ }
+
+ /**
+ * Get the WalkerFront instance that owns this FlexItemFront.
+ */
+ get walkerFront() {
+ return this.parentFront.walkerFront;
+ }
+
+ /**
+ * Get the computed style properties for the flex item.
+ */
+ get computedStyle() {
+ return this._form.computedStyle;
+ }
+
+ /**
+ * Get the style properties for the flex item.
+ */
+ get properties() {
+ return this._form.properties;
+ }
+}
+
+class GridFront extends FrontClassWithSpec(gridSpec) {
+ form(form) {
+ this._form = form;
+ }
+
+ /**
+ * In some cases, the GridActor already knows the NodeActor ID of the node where the
+ * grid is located. In such cases, this getter returns the NodeFront for it.
+ */
+ get containerNodeFront() {
+ if (!this._form.containerNodeActorID) {
+ return null;
+ }
+
+ return this.conn.getFrontByID(this._form.containerNodeActorID);
+ }
+
+ /**
+ * Get the WalkerFront instance that owns this GridFront.
+ */
+ get walkerFront() {
+ return this.parentFront.walkerFront;
+ }
+
+ /**
+ * Get the text direction of the grid container.
+ */
+ get direction() {
+ return this._form.direction;
+ }
+
+ /**
+ * Getter for the grid fragments data.
+ */
+ get gridFragments() {
+ return this._form.gridFragments;
+ }
+
+ /**
+ * Get whether or not the grid is a subgrid.
+ */
+ get isSubgrid() {
+ return !!this._form.isSubgrid;
+ }
+
+ /**
+ * Get the writing mode of the grid container.
+ */
+ get writingMode() {
+ return this._form.writingMode;
+ }
+}
+
+class LayoutFront extends FrontClassWithSpec(layoutSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this.getAllGrids = safeAsyncMethod(
+ this.getAllGrids.bind(this),
+ () => this.isDestroyed(),
+ []
+ );
+ }
+ /**
+ * Get the WalkerFront instance that owns this LayoutFront.
+ */
+ get walkerFront() {
+ return this.parentFront;
+ }
+
+ getAllGrids() {
+ if (!this.walkerFront.rootNode) {
+ return [];
+ }
+ return this.getGrids(this.walkerFront.rootNode);
+ }
+}
+
+exports.FlexboxFront = FlexboxFront;
+registerFront(FlexboxFront);
+exports.FlexItemFront = FlexItemFront;
+registerFront(FlexItemFront);
+exports.GridFront = GridFront;
+registerFront(GridFront);
+exports.LayoutFront = LayoutFront;
+registerFront(LayoutFront);
diff --git a/devtools/client/fronts/manifest.js b/devtools/client/fronts/manifest.js
new file mode 100644
index 0000000000..0cd720d278
--- /dev/null
+++ b/devtools/client/fronts/manifest.js
@@ -0,0 +1,25 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ manifestSpec,
+} = require("resource://devtools/shared/specs/manifest.js");
+
+class ManifestFront extends FrontClassWithSpec(manifestSpec) {
+ constructor(client) {
+ super(client);
+
+ // attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "manifestActor";
+ }
+}
+
+exports.ManifestFront = ManifestFront;
+registerFront(ManifestFront);
diff --git a/devtools/client/fronts/memory.js b/devtools/client/fronts/memory.js
new file mode 100644
index 0000000000..80ba417b67
--- /dev/null
+++ b/devtools/client/fronts/memory.js
@@ -0,0 +1,116 @@
+/* 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 { memorySpec } = require("resource://devtools/shared/specs/memory.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+});
+loader.lazyRequireGetter(
+ this,
+ "HeapSnapshotFileUtils",
+ "resource://devtools/shared/heapsnapshot/HeapSnapshotFileUtils.js"
+);
+
+class MemoryFront extends FrontClassWithSpec(memorySpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this._client = client;
+ this.heapSnapshotFileActorID = null;
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "memoryActor";
+ }
+
+ /**
+ * Save a heap snapshot, transfer it from the server to the client if the
+ * server and client do not share a file system, and return the local file
+ * path to the heap snapshot.
+ *
+ * Note that this is safe to call for actors inside sandoxed child processes,
+ * as we jump through the correct IPDL hoops.
+ *
+ * @params Boolean options.forceCopy
+ * Always force a bulk data copy of the saved heap snapshot, even when
+ * the server and client share a file system.
+ *
+ * @params {Object|undefined} options.boundaries
+ * The boundaries for the heap snapshot. See
+ * ChromeUtils.webidl for more details.
+ *
+ * @returns Promise<String>
+ */
+ async saveHeapSnapshot(options = {}) {
+ const snapshotId = await super.saveHeapSnapshot(options.boundaries);
+
+ if (
+ !options.forceCopy &&
+ (await HeapSnapshotFileUtils.haveHeapSnapshotTempFile(snapshotId))
+ ) {
+ return HeapSnapshotFileUtils.getHeapSnapshotTempFilePath(snapshotId);
+ }
+
+ return this.transferHeapSnapshot(snapshotId);
+ }
+
+ /**
+ * Given that we have taken a heap snapshot with the given id, transfer the
+ * heap snapshot file to the client. The path to the client's local file is
+ * returned.
+ *
+ * @param {String} snapshotId
+ *
+ * @returns Promise<String>
+ */
+ async transferHeapSnapshot(snapshotId) {
+ if (!this.heapSnapshotFileActorID) {
+ const form = await this._client.mainRoot.rootForm;
+ this.heapSnapshotFileActorID = form.heapSnapshotFileActor;
+ }
+
+ try {
+ const request = this._client.request({
+ to: this.heapSnapshotFileActorID,
+ type: "transferHeapSnapshot",
+ snapshotId,
+ });
+
+ const outFilePath =
+ HeapSnapshotFileUtils.getNewUniqueHeapSnapshotTempFilePath();
+ const outFile = new lazy.FileUtils.File(outFilePath);
+ const outFileStream = lazy.FileUtils.openSafeFileOutputStream(outFile);
+
+ // This request is a bulk request. That's why the result of the request is
+ // an object with the `copyTo` function that can transfer the data to
+ // another stream.
+ // See devtools/shared/transport/transport.js to know more about this mode.
+ const { copyTo } = await request;
+ await copyTo(outFileStream);
+
+ lazy.FileUtils.closeSafeFileOutputStream(outFileStream);
+ return outFilePath;
+ } catch (e) {
+ if (e.error) {
+ // This isn't a real error, rather this is a message coming from the
+ // server. So let's throw a real error instead.
+ throw new Error(
+ `The server's actor threw an error: (${e.error}) ${e.message}`
+ );
+ }
+
+ // Otherwise, rethrow the error
+ throw e;
+ }
+ }
+}
+
+exports.MemoryFront = MemoryFront;
+registerFront(MemoryFront);
diff --git a/devtools/client/fronts/moz.build b/devtools/client/fronts/moz.build
new file mode 100644
index 0000000000..8b56870104
--- /dev/null
+++ b/devtools/client/fronts/moz.build
@@ -0,0 +1,58 @@
+# -*- 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/.
+
+DIRS += [
+ "addon",
+ "descriptors",
+ "inspector",
+ "targets",
+ "worker",
+]
+
+DevToolsModules(
+ "accessibility.js",
+ "animation.js",
+ "array-buffer.js",
+ "blackboxing.js",
+ "breakpoint-list.js",
+ "changes.js",
+ "compatibility.js",
+ "css-properties.js",
+ "device.js",
+ "frame.js",
+ "highlighters.js",
+ "inspector.js",
+ "layout.js",
+ "manifest.js",
+ "memory.js",
+ "network-content.js",
+ "network-parent.js",
+ "node.js",
+ "object.js",
+ "page-style.js",
+ "perf.js",
+ "preference.js",
+ "private-properties-iterator.js",
+ "property-iterator.js",
+ "reflow.js",
+ "responsive.js",
+ "root.js",
+ "screenshot-content.js",
+ "screenshot.js",
+ "source.js",
+ "storage.js",
+ "string.js",
+ "style-rule.js",
+ "style-sheets.js",
+ "symbol-iterator.js",
+ "target-configuration.js",
+ "thread-configuration.js",
+ "thread.js",
+ "tracer.js",
+ "walker.js",
+ "watcher.js",
+ "webconsole.js",
+)
diff --git a/devtools/client/fronts/network-content.js b/devtools/client/fronts/network-content.js
new file mode 100644
index 0000000000..43b4154c56
--- /dev/null
+++ b/devtools/client/fronts/network-content.js
@@ -0,0 +1,24 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ networkContentSpec,
+} = require("resource://devtools/shared/specs/network-content.js");
+
+class NetworkContentFront extends FrontClassWithSpec(networkContentSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "networkContentActor";
+ }
+}
+
+exports.NetworkContentFront = NetworkContentFront;
+registerFront(NetworkContentFront);
diff --git a/devtools/client/fronts/network-parent.js b/devtools/client/fronts/network-parent.js
new file mode 100644
index 0000000000..c2b68449b5
--- /dev/null
+++ b/devtools/client/fronts/network-parent.js
@@ -0,0 +1,27 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ networkParentSpec,
+} = require("resource://devtools/shared/specs/network-parent.js");
+
+/**
+ * Client side for the network actor, used for managing network requests.
+ */
+
+class NetworkParentFront extends FrontClassWithSpec(networkParentSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "networkParentActor";
+ }
+}
+
+registerFront(NetworkParentFront);
diff --git a/devtools/client/fronts/node.js b/devtools/client/fronts/node.js
new file mode 100644
index 0000000000..ef3497a92b
--- /dev/null
+++ b/devtools/client/fronts/node.js
@@ -0,0 +1,629 @@
+/* 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 {
+ FrontClassWithSpec,
+ types,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ nodeSpec,
+ nodeListSpec,
+} = require("resource://devtools/shared/specs/node.js");
+const {
+ SimpleStringFront,
+} = require("resource://devtools/client/fronts/string.js");
+
+loader.lazyRequireGetter(
+ this,
+ "nodeConstants",
+ "resource://devtools/shared/dom-node-constants.js"
+);
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "browserToolboxScope",
+ "devtools.browsertoolbox.scope"
+);
+
+const BROWSER_TOOLBOX_SCOPE_EVERYTHING = "everything";
+
+const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
+
+/**
+ * Client side of a node list as returned by querySelectorAll()
+ */
+class NodeListFront extends FrontClassWithSpec(nodeListSpec) {
+ marshallPool() {
+ return this.getParent();
+ }
+
+ // Update the object given a form representation off the wire.
+ form(json) {
+ this.length = json.length;
+ }
+
+ item(index) {
+ return super.item(index).then(response => {
+ return response.node;
+ });
+ }
+
+ items(start, end) {
+ return super.items(start, end).then(response => {
+ return response.nodes;
+ });
+ }
+}
+
+exports.NodeListFront = NodeListFront;
+registerFront(NodeListFront);
+
+/**
+ * Convenience API for building a list of attribute modifications
+ * for the `modifyAttributes` request.
+ */
+class AttributeModificationList {
+ constructor(node) {
+ this.node = node;
+ this.modifications = [];
+ }
+
+ apply() {
+ const ret = this.node.modifyAttributes(this.modifications);
+ return ret;
+ }
+
+ destroy() {
+ this.node = null;
+ this.modification = null;
+ }
+
+ setAttributeNS(ns, name, value) {
+ this.modifications.push({
+ attributeNamespace: ns,
+ attributeName: name,
+ newValue: value,
+ });
+ }
+
+ setAttribute(name, value) {
+ this.setAttributeNS(undefined, name, value);
+ }
+
+ removeAttributeNS(ns, name) {
+ this.setAttributeNS(ns, name, undefined);
+ }
+
+ removeAttribute(name) {
+ this.setAttributeNS(undefined, name, undefined);
+ }
+}
+
+/**
+ * Client side of the node actor.
+ *
+ * Node fronts are strored in a tree that mirrors the DOM tree on the
+ * server, but with a few key differences:
+ * - Not all children will be necessary loaded for each node.
+ * - The order of children isn't guaranteed to be the same as the DOM.
+ * Children are stored in a doubly-linked list, to make addition/removal
+ * and traversal quick.
+ *
+ * Due to the order/incompleteness of the child list, it is safe to use
+ * the parent node from clients, but the `children` request should be used
+ * to traverse children.
+ */
+class NodeFront extends FrontClassWithSpec(nodeSpec) {
+ constructor(conn, targetFront, parentFront) {
+ super(conn, targetFront, parentFront);
+ // The parent node
+ this._parent = null;
+ // The first child of this node.
+ this._child = null;
+ // The next sibling of this node.
+ this._next = null;
+ // The previous sibling of this node.
+ this._prev = null;
+ // Store the flag to use it after destroy, where targetFront is set to null.
+ this._hasParentProcessTarget = targetFront.isParentProcess;
+ }
+
+ /**
+ * Destroy a node front. The node must have been removed from the
+ * ownership tree before this is called, unless the whole walker front
+ * is being destroyed.
+ */
+ destroy() {
+ super.destroy();
+ }
+
+ // Update the object given a form representation off the wire.
+ form(form, ctx) {
+ // backward-compatibility: shortValue indicates we are connected to old server
+ if (form.shortValue) {
+ // If the value is not complete, set nodeValue to null, it will be fetched
+ // when calling getNodeValue()
+ form.nodeValue = form.incompleteValue ? null : form.shortValue;
+ }
+
+ this.traits = form.traits || {};
+
+ // Shallow copy of the form. We could just store a reference, but
+ // eventually we'll want to update some of the data.
+ this._form = Object.assign({}, form);
+ this._form.attrs = this._form.attrs ? this._form.attrs.slice() : [];
+
+ if (form.parent) {
+ // Get the owner actor for this actor (the walker), and find the
+ // parent node of this actor from it, creating a standin node if
+ // necessary.
+ const owner = ctx.marshallPool();
+ if (typeof owner.ensureDOMNodeFront === "function") {
+ const parentNodeFront = owner.ensureDOMNodeFront(form.parent);
+ this.reparent(parentNodeFront);
+ }
+ }
+
+ if (form.host) {
+ const owner = ctx.marshallPool();
+ if (typeof owner.ensureDOMNodeFront === "function") {
+ this.host = owner.ensureDOMNodeFront(form.host);
+ }
+ }
+
+ if (form.inlineTextChild) {
+ this.inlineTextChild = types
+ .getType("domnode")
+ .read(form.inlineTextChild, ctx);
+ } else {
+ this.inlineTextChild = undefined;
+ }
+ }
+
+ /**
+ * Returns the parent NodeFront for this NodeFront.
+ */
+ parentNode() {
+ return this._parent;
+ }
+
+ /**
+ * Returns the NodeFront corresponding to the parentNode of this NodeFront, or the
+ * NodeFront corresponding to the host element for shadowRoot elements.
+ */
+ parentOrHost() {
+ return this.isShadowRoot ? this.host : this._parent;
+ }
+
+ /**
+ * Returns the owner DocumentElement|ShadowRootElement NodeFront for this NodeFront,
+ * or null if such element can't be found.
+ *
+ * @returns {NodeFront|null}
+ */
+ getOwnerRootNodeFront() {
+ let currentNode = this;
+ while (currentNode) {
+ if (
+ currentNode.isShadowRoot ||
+ currentNode.nodeType === Node.DOCUMENT_NODE
+ ) {
+ return currentNode;
+ }
+
+ currentNode = currentNode.parentNode();
+ }
+
+ return null;
+ }
+
+ /**
+ * Process a mutation entry as returned from the walker's `getMutations`
+ * request. Only tries to handle changes of the node's contents
+ * themselves (character data and attribute changes), the walker itself
+ * will keep the ownership tree up to date.
+ */
+ updateMutation(change) {
+ if (change.type === "attributes") {
+ // We'll need to lazily reparse the attributes after this change.
+ this._attrMap = undefined;
+
+ // Update any already-existing attributes.
+ let found = false;
+ for (let i = 0; i < this.attributes.length; i++) {
+ const attr = this.attributes[i];
+ if (
+ attr.name == change.attributeName &&
+ attr.namespace == change.attributeNamespace
+ ) {
+ if (change.newValue !== null) {
+ attr.value = change.newValue;
+ } else {
+ this.attributes.splice(i, 1);
+ }
+ found = true;
+ break;
+ }
+ }
+ // This is a new attribute. The null check is because of Bug 1192270,
+ // in the case of a newly added then removed attribute
+ if (!found && change.newValue !== null) {
+ this.attributes.push({
+ name: change.attributeName,
+ namespace: change.attributeNamespace,
+ value: change.newValue,
+ });
+ }
+ } else if (change.type === "characterData") {
+ this._form.nodeValue = change.newValue;
+ } else if (change.type === "pseudoClassLock") {
+ this._form.pseudoClassLocks = change.pseudoClassLocks;
+ } else if (change.type === "events") {
+ this._form.hasEventListeners = change.hasEventListeners;
+ } else if (change.type === "mutationBreakpoint") {
+ this._form.mutationBreakpoints = change.mutationBreakpoints;
+ }
+ }
+
+ // Some accessors to make NodeFront feel more like a Node
+
+ get id() {
+ return this.getAttribute("id");
+ }
+
+ get nodeType() {
+ return this._form.nodeType;
+ }
+ get namespaceURI() {
+ return this._form.namespaceURI;
+ }
+ get nodeName() {
+ return this._form.nodeName;
+ }
+ get displayName() {
+ const { displayName, nodeName } = this._form;
+
+ // Keep `nodeName.toLowerCase()` for backward compatibility
+ return displayName || nodeName.toLowerCase();
+ }
+ get doctypeString() {
+ return (
+ "<!DOCTYPE " +
+ this._form.name +
+ (this._form.publicId ? ' PUBLIC "' + this._form.publicId + '"' : "") +
+ (this._form.systemId ? ' "' + this._form.systemId + '"' : "") +
+ ">"
+ );
+ }
+
+ get baseURI() {
+ return this._form.baseURI;
+ }
+
+ get browsingContextID() {
+ return this._form.browsingContextID;
+ }
+
+ get className() {
+ return this.getAttribute("class") || "";
+ }
+
+ // Check if the node has children but the current DevTools session is unable
+ // to retrieve them.
+ // Typically: a <frame> or <browser> element which loads a document in another
+ // process, but the toolbox' configuration prevents to inspect it (eg the
+ // parent-process only Browser Toolbox).
+ get childrenUnavailable() {
+ return (
+ // If form.useChildTargetToFetchChildren is true, it means the node HAS
+ // children in another target.
+ // Note: useChildTargetToFetchChildren might be undefined, force
+ // conversion to boolean. See Bug 1783613 to try and improve this.
+ !!this._form.useChildTargetToFetchChildren &&
+ // But if useChildTargetToFetchChildren is false, it means the client
+ // configuration prevents from displaying such children.
+ // This is the only case where children are considered as unavailable:
+ // they exist, but can't be retrieved by configuration.
+ !this.useChildTargetToFetchChildren
+ );
+ }
+ get hasChildren() {
+ return this.numChildren > 0;
+ }
+ get numChildren() {
+ if (this.childrenUnavailable) {
+ return 0;
+ }
+
+ return this._form.numChildren;
+ }
+ get useChildTargetToFetchChildren() {
+ if (
+ this._hasParentProcessTarget &&
+ browserToolboxScope != BROWSER_TOOLBOX_SCOPE_EVERYTHING
+ ) {
+ return false;
+ }
+
+ return !!this._form.useChildTargetToFetchChildren;
+ }
+ get hasEventListeners() {
+ return this._form.hasEventListeners;
+ }
+
+ get isMarkerPseudoElement() {
+ return this._form.isMarkerPseudoElement;
+ }
+ get isBeforePseudoElement() {
+ return this._form.isBeforePseudoElement;
+ }
+ get isAfterPseudoElement() {
+ return this._form.isAfterPseudoElement;
+ }
+ get isPseudoElement() {
+ return (
+ this.isBeforePseudoElement ||
+ this.isAfterPseudoElement ||
+ this.isMarkerPseudoElement
+ );
+ }
+ get isAnonymous() {
+ return this._form.isAnonymous;
+ }
+ get isInHTMLDocument() {
+ return this._form.isInHTMLDocument;
+ }
+ get tagName() {
+ return this.nodeType === nodeConstants.ELEMENT_NODE ? this.nodeName : null;
+ }
+
+ get isDocumentElement() {
+ return !!this._form.isDocumentElement;
+ }
+
+ get isTopLevelDocument() {
+ return this._form.isTopLevelDocument;
+ }
+
+ get isShadowRoot() {
+ return this._form.isShadowRoot;
+ }
+
+ get shadowRootMode() {
+ return this._form.shadowRootMode;
+ }
+
+ get isShadowHost() {
+ return this._form.isShadowHost;
+ }
+
+ get customElementLocation() {
+ return this._form.customElementLocation;
+ }
+
+ get isDirectShadowHostChild() {
+ return this._form.isDirectShadowHostChild;
+ }
+
+ // doctype properties
+ get name() {
+ return this._form.name;
+ }
+ get publicId() {
+ return this._form.publicId;
+ }
+ get systemId() {
+ return this._form.systemId;
+ }
+
+ getAttribute(name) {
+ const attr = this._getAttribute(name);
+ return attr ? attr.value : null;
+ }
+ hasAttribute(name) {
+ this._cacheAttributes();
+ return name in this._attrMap;
+ }
+
+ get hidden() {
+ const cls = this.getAttribute("class");
+ return cls && cls.indexOf(HIDDEN_CLASS) > -1;
+ }
+
+ get attributes() {
+ return this._form.attrs;
+ }
+
+ get mutationBreakpoints() {
+ return this._form.mutationBreakpoints;
+ }
+
+ get pseudoClassLocks() {
+ return this._form.pseudoClassLocks || [];
+ }
+ hasPseudoClassLock(pseudo) {
+ return this.pseudoClassLocks.some(locked => locked === pseudo);
+ }
+
+ get displayType() {
+ return this._form.displayType;
+ }
+
+ get isDisplayed() {
+ return this._form.isDisplayed;
+ }
+
+ get isScrollable() {
+ return this._form.isScrollable;
+ }
+
+ get causesOverflow() {
+ return this._form.causesOverflow;
+ }
+
+ get isTreeDisplayed() {
+ let parent = this;
+ while (parent) {
+ if (!parent.isDisplayed) {
+ return false;
+ }
+ parent = parent.parentNode();
+ }
+ return true;
+ }
+
+ get inspectorFront() {
+ return this.parentFront.parentFront;
+ }
+
+ get walkerFront() {
+ return this.parentFront;
+ }
+
+ getNodeValue() {
+ // backward-compatibility: if nodevalue is null and shortValue is defined, the actual
+ // value of the node needs to be fetched on the server.
+ if (this._form.nodeValue === null && this._form.shortValue) {
+ return super.getNodeValue();
+ }
+
+ const str = this._form.nodeValue || "";
+ return Promise.resolve(new SimpleStringFront(str));
+ }
+
+ /**
+ * Return a new AttributeModificationList for this node.
+ */
+ startModifyingAttributes() {
+ return new AttributeModificationList(this);
+ }
+
+ _cacheAttributes() {
+ if (typeof this._attrMap != "undefined") {
+ return;
+ }
+ this._attrMap = {};
+ for (const attr of this.attributes) {
+ this._attrMap[attr.name] = attr;
+ }
+ }
+
+ _getAttribute(name) {
+ this._cacheAttributes();
+ return this._attrMap[name] || undefined;
+ }
+
+ /**
+ * Set this node's parent. Note that the children saved in
+ * this tree are unordered and incomplete, so shouldn't be used
+ * instead of a `children` request.
+ */
+ reparent(parent) {
+ if (this._parent === parent) {
+ return;
+ }
+
+ if (this._parent && this._parent._child === this) {
+ this._parent._child = this._next;
+ }
+ if (this._prev) {
+ this._prev._next = this._next;
+ }
+ if (this._next) {
+ this._next._prev = this._prev;
+ }
+ this._next = null;
+ this._prev = null;
+ this._parent = parent;
+ if (!parent) {
+ // Subtree is disconnected, we're done
+ return;
+ }
+ this._next = parent._child;
+ if (this._next) {
+ this._next._prev = this;
+ }
+ parent._child = this;
+ }
+
+ /**
+ * Return all the known children of this node.
+ */
+ treeChildren() {
+ const ret = [];
+ for (let child = this._child; child != null; child = child._next) {
+ ret.push(child);
+ }
+ return ret;
+ }
+
+ /**
+ * Do we use a local target?
+ * Useful to know if a rawNode is available or not.
+ *
+ * This will, one day, be removed. External code should
+ * not need to know if the target is remote or not.
+ */
+ isLocalToBeDeprecated() {
+ return !!this.conn._transport._serverConnection;
+ }
+
+ /**
+ * Get a Node for the given node front. This only works locally,
+ * and is only intended as a stopgap during the transition to the remote
+ * protocol. If you depend on this you're likely to break soon.
+ */
+ rawNode(rawNode) {
+ if (!this.isLocalToBeDeprecated()) {
+ console.warn("Tried to use rawNode on a remote connection.");
+ return null;
+ }
+ const {
+ DevToolsServer,
+ } = require("resource://devtools/server/devtools-server.js");
+ const actor = DevToolsServer.searchAllConnectionsForActor(this.actorID);
+ if (!actor) {
+ // Can happen if we try to get the raw node for an already-expired
+ // actor.
+ return null;
+ }
+ return actor.rawNode;
+ }
+
+ async connectToFrame() {
+ if (!this.useChildTargetToFetchChildren) {
+ console.warn("Tried to open connection to an invalid frame.");
+ return null;
+ }
+ if (
+ this._childBrowsingContextTarget &&
+ !this._childBrowsingContextTarget.isDestroyed()
+ ) {
+ return this._childBrowsingContextTarget;
+ }
+
+ // Get the target for this frame element
+ this._childBrowsingContextTarget =
+ await this.targetFront.getWindowGlobalTarget(
+ this._form.browsingContextID
+ );
+
+ // Bug 1776250: When the target is destroyed, we need to easily find the
+ // parent node front so that we can update its frontend container in the
+ // markup-view.
+ this._childBrowsingContextTarget.setParentNodeFront(this);
+
+ return this._childBrowsingContextTarget;
+ }
+}
+
+exports.NodeFront = NodeFront;
+registerFront(NodeFront);
diff --git a/devtools/client/fronts/object.js b/devtools/client/fronts/object.js
new file mode 100644
index 0000000000..fa1f1d8ba1
--- /dev/null
+++ b/devtools/client/fronts/object.js
@@ -0,0 +1,465 @@
+/* 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 { objectSpec } = require("resource://devtools/shared/specs/object.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ LongStringFront,
+} = require("resource://devtools/client/fronts/string.js");
+
+const SUPPORT_ENUM_ENTRIES_SET = new Set([
+ "Headers",
+ "Map",
+ "WeakMap",
+ "Set",
+ "WeakSet",
+ "Storage",
+ "URLSearchParams",
+ "FormData",
+ "MIDIInputMap",
+ "MIDIOutputMap",
+]);
+
+/**
+ * A ObjectFront is used as a front end for the ObjectActor that is
+ * created on the server, hiding implementation details.
+ */
+class ObjectFront extends FrontClassWithSpec(objectSpec) {
+ constructor(conn = null, targetFront = null, parentFront = null, data) {
+ if (!parentFront) {
+ throw new Error("ObjectFront require a parent front");
+ }
+
+ super(conn, targetFront, parentFront);
+
+ this._grip = data;
+ this.actorID = this._grip.actor;
+ this.valid = true;
+
+ parentFront.manage(this);
+ }
+
+ skipDestroy() {
+ // Object fronts are simple fronts, they don't need to be cleaned up on
+ // toolbox destroy. `conn` is a DebuggerClient instance, check the
+ // `isToolboxDestroy` flag to skip the destroy.
+ return this.conn && this.conn.isToolboxDestroy;
+ }
+
+ getGrip() {
+ return this._grip;
+ }
+
+ get isFrozen() {
+ return this._grip.frozen;
+ }
+
+ get isSealed() {
+ return this._grip.sealed;
+ }
+
+ get isExtensible() {
+ return this._grip.extensible;
+ }
+
+ /**
+ * Request the prototype and own properties of the object.
+ */
+ async getPrototypeAndProperties() {
+ const result = await super.prototypeAndProperties();
+
+ if (result.prototype) {
+ result.prototype = getAdHocFrontOrPrimitiveGrip(result.prototype, this);
+ }
+
+ // The result packet can have multiple properties that hold grips which we may need
+ // to turn into fronts.
+ const gripKeys = ["value", "getterValue", "get", "set"];
+
+ if (result.ownProperties) {
+ Object.entries(result.ownProperties).forEach(([key, descriptor]) => {
+ if (descriptor) {
+ for (const gripKey of gripKeys) {
+ if (descriptor.hasOwnProperty(gripKey)) {
+ result.ownProperties[key][gripKey] = getAdHocFrontOrPrimitiveGrip(
+ descriptor[gripKey],
+ this
+ );
+ }
+ }
+ }
+ });
+ }
+
+ if (result.safeGetterValues) {
+ Object.entries(result.safeGetterValues).forEach(([key, descriptor]) => {
+ if (descriptor) {
+ for (const gripKey of gripKeys) {
+ if (descriptor.hasOwnProperty(gripKey)) {
+ result.safeGetterValues[key][gripKey] =
+ getAdHocFrontOrPrimitiveGrip(descriptor[gripKey], this);
+ }
+ }
+ }
+ });
+ }
+
+ if (result.ownSymbols) {
+ result.ownSymbols.forEach((descriptor, i, arr) => {
+ if (descriptor) {
+ for (const gripKey of gripKeys) {
+ if (descriptor.hasOwnProperty(gripKey)) {
+ arr[i][gripKey] = getAdHocFrontOrPrimitiveGrip(
+ descriptor[gripKey],
+ this
+ );
+ }
+ }
+ }
+ });
+ }
+
+ return result;
+ }
+
+ /**
+ * Request a PropertyIteratorFront instance to ease listing
+ * properties for this object.
+ *
+ * @param options Object
+ * A dictionary object with various boolean attributes:
+ * - ignoreIndexedProperties Boolean
+ * If true, filters out Array items.
+ * e.g. properties names between `0` and `object.length`.
+ * - ignoreNonIndexedProperties Boolean
+ * If true, filters out items that aren't array items
+ * e.g. properties names that are not a number between `0`
+ * and `object.length`.
+ * - sort Boolean
+ * If true, the iterator will sort the properties by name
+ * before dispatching them.
+ */
+ enumProperties(options) {
+ return super.enumProperties(options);
+ }
+
+ /**
+ * Request a PropertyIteratorFront instance to enumerate entries in a
+ * Map/Set-like object.
+ */
+ enumEntries() {
+ if (!SUPPORT_ENUM_ENTRIES_SET.has(this._grip.class)) {
+ console.error(
+ `enumEntries can't be called for "${
+ this._grip.class
+ }" grips. Supported grips are: ${[...SUPPORT_ENUM_ENTRIES_SET].join(
+ ", "
+ )}.`
+ );
+ return null;
+ }
+ return super.enumEntries();
+ }
+
+ /**
+ * Request a SymbolIteratorFront instance to enumerate symbols in an object.
+ */
+ enumSymbols() {
+ if (this._grip.type !== "object") {
+ console.error("enumSymbols is only valid for objects grips.");
+ return null;
+ }
+ return super.enumSymbols();
+ }
+
+ /**
+ * Request the property descriptor of the object's specified property.
+ *
+ * @param name string The name of the requested property.
+ */
+ getProperty(name) {
+ return super.property(name);
+ }
+
+ /**
+ * Request the value of the object's specified property.
+ *
+ * @param name string The name of the requested property.
+ * @param receiverId string|null The actorId of the receiver to be used for getters.
+ */
+ async getPropertyValue(name, receiverId) {
+ const response = await super.propertyValue(name, receiverId);
+
+ if (response.value) {
+ const { value } = response;
+ if (value.return) {
+ response.value.return = getAdHocFrontOrPrimitiveGrip(
+ value.return,
+ this
+ );
+ }
+
+ if (value.throw) {
+ response.value.throw = getAdHocFrontOrPrimitiveGrip(value.throw, this);
+ }
+ }
+ return response;
+ }
+
+ /**
+ * Get the body of a custom formatted object.
+ */
+ async customFormatterBody() {
+ const result = await super.customFormatterBody();
+
+ if (!result?.customFormatterBody) {
+ return result;
+ }
+
+ const createFrontsInJsonMl = item => {
+ if (Array.isArray(item)) {
+ return item.map(i => createFrontsInJsonMl(i));
+ }
+ return getAdHocFrontOrPrimitiveGrip(item, this);
+ };
+
+ result.customFormatterBody = createFrontsInJsonMl(
+ result.customFormatterBody
+ );
+
+ return result;
+ }
+
+ /**
+ * Request the prototype of the object.
+ */
+ async getPrototype() {
+ const result = await super.prototype();
+
+ if (!result.prototype) {
+ return result;
+ }
+
+ result.prototype = getAdHocFrontOrPrimitiveGrip(result.prototype, this);
+
+ return result;
+ }
+
+ /**
+ * Request the state of a promise.
+ */
+ async getPromiseState() {
+ if (this._grip.class !== "Promise") {
+ console.error("getPromiseState is only valid for promise grips.");
+ return null;
+ }
+
+ let response, promiseState;
+ try {
+ response = await super.promiseState();
+ promiseState = response.promiseState;
+ } catch (error) {
+ // @backward-compat { version 85 } On older server, the promiseState request didn't
+ // didn't exist (bug 1552648). The promise state was directly included in the grip.
+ if (error.message.includes("unrecognizedPacketType")) {
+ promiseState = this._grip.promiseState;
+ response = { promiseState };
+ } else {
+ throw error;
+ }
+ }
+
+ const { value, reason } = promiseState;
+
+ if (value) {
+ promiseState.value = getAdHocFrontOrPrimitiveGrip(value, this);
+ }
+
+ if (reason) {
+ promiseState.reason = getAdHocFrontOrPrimitiveGrip(reason, this);
+ }
+
+ return response;
+ }
+
+ /**
+ * Request the target and handler internal slots of a proxy.
+ */
+ async getProxySlots() {
+ if (this._grip.class !== "Proxy") {
+ console.error("getProxySlots is only valid for proxy grips.");
+ return null;
+ }
+
+ const response = await super.proxySlots();
+ const { proxyHandler, proxyTarget } = response;
+
+ if (proxyHandler) {
+ response.proxyHandler = getAdHocFrontOrPrimitiveGrip(proxyHandler, this);
+ }
+
+ if (proxyTarget) {
+ response.proxyTarget = getAdHocFrontOrPrimitiveGrip(proxyTarget, this);
+ }
+
+ return response;
+ }
+
+ get isSyntaxError() {
+ return this._grip.preview && this._grip.preview.name == "SyntaxError";
+ }
+}
+
+/**
+ * When we are asking the server for the value of a given variable, we might get different
+ * type of objects:
+ * - a primitive (string, number, null, false, boolean)
+ * - a long string
+ * - an "object" (i.e. not primitive nor long string)
+ *
+ * Each of those type need a different front, or none:
+ * - a primitive does not allow further interaction with the server, so we don't need
+ * to have a dedicated front.
+ * - a long string needs a longStringFront to be able to retrieve the full string.
+ * - an object need an objectFront to retrieve properties, symbols and prototype.
+ *
+ * In the case an ObjectFront is created, we also check if the object has properties
+ * that should be turned into fronts as well.
+ *
+ * @param {String|Number|Object} options: The packet returned by the server.
+ * @param {Front} parentFront
+ *
+ * @returns {Number|String|Object|LongStringFront|ObjectFront}
+ */
+function getAdHocFrontOrPrimitiveGrip(packet, parentFront) {
+ // We only want to try to create a front when it makes sense, i.e when it has an
+ // actorID, unless:
+ // - it's a Symbol (See Bug 1600299)
+ // - it's a mapEntry (the preview.key and preview.value properties can hold actors)
+ // - or it is already a front (happens when we are using the legacy listeners in the ResourceCommand)
+ const isPacketAnObject = packet && typeof packet === "object";
+ const isFront = !!packet.typeName;
+ if (
+ !isPacketAnObject ||
+ packet.type == "symbol" ||
+ (packet.type !== "mapEntry" && !packet.actor) ||
+ isFront
+ ) {
+ return packet;
+ }
+
+ const { conn } = parentFront;
+ // If the parent front is a target, consider it as the target to use for all objects
+ const targetFront = parentFront.isTargetFront
+ ? parentFront
+ : parentFront.targetFront;
+
+ // We may have already created a front for this object actor since some actor (e.g. the
+ // thread actor) cache the object actors they create.
+ const existingFront = conn.getFrontByID(packet.actor);
+ if (existingFront) {
+ return existingFront;
+ }
+
+ const { type } = packet;
+
+ if (type === "longString") {
+ const longStringFront = new LongStringFront(conn, targetFront, parentFront);
+ longStringFront.form(packet);
+ parentFront.manage(longStringFront);
+ return longStringFront;
+ }
+
+ if (type === "mapEntry" && packet.preview) {
+ const { key, value } = packet.preview;
+ packet.preview.key = getAdHocFrontOrPrimitiveGrip(
+ key,
+ parentFront,
+ targetFront
+ );
+ packet.preview.value = getAdHocFrontOrPrimitiveGrip(
+ value,
+ parentFront,
+ targetFront
+ );
+ return packet;
+ }
+
+ const objectFront = new ObjectFront(conn, targetFront, parentFront, packet);
+ createChildFronts(objectFront, packet);
+ return objectFront;
+}
+
+/**
+ * Create child fronts of the passed object front given a packet. Those child fronts are
+ * usually mapping actors of the packet sub-properties (preview items, promise fullfilled
+ * values, …).
+ *
+ * @param {ObjectFront} objectFront
+ * @param {String|Number|Object} packet: The packet returned by the server
+ */
+function createChildFronts(objectFront, packet) {
+ if (packet.preview) {
+ const { message, entries } = packet.preview;
+
+ // The message could be a longString.
+ if (packet.preview.message) {
+ packet.preview.message = getAdHocFrontOrPrimitiveGrip(
+ message,
+ objectFront
+ );
+ }
+
+ // Handle Map/WeakMap preview entries (the preview might be directly used if has all the
+ // items needed, i.e. if the Map has less than 10 items).
+ if (entries && Array.isArray(entries)) {
+ packet.preview.entries = entries.map(([key, value]) => [
+ getAdHocFrontOrPrimitiveGrip(key, objectFront),
+ getAdHocFrontOrPrimitiveGrip(value, objectFront),
+ ]);
+ }
+ }
+
+ if (packet && typeof packet.ownProperties === "object") {
+ for (const [name, descriptor] of Object.entries(packet.ownProperties)) {
+ // The descriptor can have multiple properties that hold grips which we may need
+ // to turn into fronts.
+ const gripKeys = ["value", "getterValue", "get", "set"];
+ for (const key of gripKeys) {
+ if (
+ descriptor &&
+ typeof descriptor === "object" &&
+ descriptor.hasOwnProperty(key)
+ ) {
+ packet.ownProperties[name][key] = getAdHocFrontOrPrimitiveGrip(
+ descriptor[key],
+ objectFront
+ );
+ }
+ }
+ }
+ }
+
+ // Handle custom formatters
+ if (packet && packet.useCustomFormatter && Array.isArray(packet.header)) {
+ const createFrontsInJsonMl = item => {
+ if (Array.isArray(item)) {
+ return item.map(i => createFrontsInJsonMl(i));
+ }
+ return getAdHocFrontOrPrimitiveGrip(item, objectFront);
+ };
+
+ packet.header = createFrontsInJsonMl(packet.header);
+ }
+}
+
+registerFront(ObjectFront);
+
+exports.ObjectFront = ObjectFront;
+exports.getAdHocFrontOrPrimitiveGrip = getAdHocFrontOrPrimitiveGrip;
diff --git a/devtools/client/fronts/page-style.js b/devtools/client/fronts/page-style.js
new file mode 100644
index 0000000000..dfe9f593e4
--- /dev/null
+++ b/devtools/client/fronts/page-style.js
@@ -0,0 +1,123 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ pageStyleSpec,
+} = require("resource://devtools/shared/specs/page-style.js");
+
+/**
+ * PageStyleFront, the front object for the PageStyleActor
+ */
+class PageStyleFront extends FrontClassWithSpec(pageStyleSpec) {
+ _attributesCache = new Map();
+
+ constructor(conn, targetFront, parentFront) {
+ super(conn, targetFront, parentFront);
+ this.inspector = this.getParent();
+
+ this._clearAttributesCache = this._clearAttributesCache.bind(this);
+ this.on("stylesheet-updated", this._clearAttributesCache);
+ this.walker.on("new-mutations", this._clearAttributesCache);
+ }
+
+ form(form) {
+ this._form = form;
+ }
+
+ get walker() {
+ return this.inspector.walker;
+ }
+
+ get supportsFontStretchLevel4() {
+ return this._form.traits && this._form.traits.fontStretchLevel4;
+ }
+
+ get supportsFontStyleLevel4() {
+ return this._form.traits && this._form.traits.fontStyleLevel4;
+ }
+
+ get supportsFontVariations() {
+ return this._form.traits && this._form.traits.fontVariations;
+ }
+
+ get supportsFontWeightLevel4() {
+ return this._form.traits && this._form.traits.fontWeightLevel4;
+ }
+
+ getMatchedSelectors(node, property, options) {
+ return super.getMatchedSelectors(node, property, options).then(ret => {
+ return ret.matched;
+ });
+ }
+
+ async getApplied(node, options = {}) {
+ const ret = await super.getApplied(node, options);
+ return ret.entries;
+ }
+
+ addNewRule(node, pseudoClasses) {
+ return super.addNewRule(node, pseudoClasses).then(ret => {
+ return ret.entries[0];
+ });
+ }
+
+ /**
+ * Get an array of existing attribute values in a node document, given an attribute type.
+ *
+ * @param {String} search: A string to filter attribute value on.
+ * @param {String} attributeType: The type of attribute we want to retrieve the values.
+ * @param {Element} node: The element we want to get possible attributes for. This will
+ * be used to get the document where the search is happening.
+ * @returns {Array<String>} An array of strings
+ */
+ async getAttributesInOwnerDocument(search, attributeType, node) {
+ if (!attributeType) {
+ throw new Error("`type` should not be empty");
+ }
+
+ if (!search) {
+ return [];
+ }
+
+ const lcFilter = search.toLowerCase();
+
+ // If the new filter includes the string that was used on our last trip to the server,
+ // we can filter the cached results instead of calling the server again.
+ if (
+ this._attributesCache &&
+ this._attributesCache.has(attributeType) &&
+ search.startsWith(this._attributesCache.get(attributeType).search)
+ ) {
+ const cachedResults = this._attributesCache
+ .get(attributeType)
+ .results.filter(item => item.toLowerCase().startsWith(lcFilter));
+ this.emitForTests(
+ "getAttributesInOwnerDocument-cache-hit",
+ cachedResults
+ );
+ return cachedResults;
+ }
+
+ const results = await super.getAttributesInOwnerDocument(
+ search,
+ attributeType,
+ node
+ );
+ this._attributesCache.set(attributeType, { search, results });
+ return results;
+ }
+
+ _clearAttributesCache() {
+ this._attributesCache.clear();
+ }
+}
+
+exports.PageStyleFront = PageStyleFront;
+registerFront(PageStyleFront);
diff --git a/devtools/client/fronts/perf.js b/devtools/client/fronts/perf.js
new file mode 100644
index 0000000000..9ca0fb8b9d
--- /dev/null
+++ b/devtools/client/fronts/perf.js
@@ -0,0 +1,22 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const { perfSpec } = require("resource://devtools/shared/specs/perf.js");
+
+class PerfFront extends FrontClassWithSpec(perfSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "perfActor";
+ }
+}
+
+registerFront(PerfFront);
diff --git a/devtools/client/fronts/preference.js b/devtools/client/fronts/preference.js
new file mode 100644
index 0000000000..e2c498ba4b
--- /dev/null
+++ b/devtools/client/fronts/preference.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 {
+ preferenceSpec,
+} = require("resource://devtools/shared/specs/preference.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+
+class PreferenceFront extends FrontClassWithSpec(preferenceSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "preferenceActor";
+ }
+
+ async getTraits() {
+ if (!this._traits) {
+ this._traits = await super.getTraits();
+ }
+
+ return this._traits;
+ }
+}
+
+exports.PreferenceFront = PreferenceFront;
+registerFront(PreferenceFront);
diff --git a/devtools/client/fronts/private-properties-iterator.js b/devtools/client/fronts/private-properties-iterator.js
new file mode 100644
index 0000000000..6b7bd3c14a
--- /dev/null
+++ b/devtools/client/fronts/private-properties-iterator.js
@@ -0,0 +1,60 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ privatePropertiesIteratorSpec,
+} = require("resource://devtools/shared/specs/private-properties-iterator.js");
+const {
+ getAdHocFrontOrPrimitiveGrip,
+} = require("resource://devtools/client/fronts/object.js");
+
+class PrivatePropertiesIteratorFront extends FrontClassWithSpec(
+ privatePropertiesIteratorSpec
+) {
+ form(data) {
+ this.actorID = data.actor;
+ this.count = data.count;
+ }
+
+ async slice(start, count) {
+ const result = await super.slice({ start, count });
+ return this._onResult(result);
+ }
+
+ async all() {
+ const result = await super.all();
+ return this._onResult(result);
+ }
+
+ _onResult(result) {
+ if (!result.privateProperties) {
+ return result;
+ }
+
+ // The result packet can have multiple properties that hold grips which we may need
+ // to turn into fronts.
+ const gripKeys = ["value", "getterValue", "get", "set"];
+
+ result.privateProperties.forEach((item, i) => {
+ if (item?.descriptor) {
+ for (const gripKey of gripKeys) {
+ if (item.descriptor.hasOwnProperty(gripKey)) {
+ result.privateProperties[i].descriptor[gripKey] =
+ getAdHocFrontOrPrimitiveGrip(item.descriptor[gripKey], this);
+ }
+ }
+ }
+ });
+ return result;
+ }
+}
+
+exports.PrivatePropertiesIteratorFront = PrivatePropertiesIteratorFront;
+registerFront(PrivatePropertiesIteratorFront);
diff --git a/devtools/client/fronts/property-iterator.js b/devtools/client/fronts/property-iterator.js
new file mode 100644
index 0000000000..6348e435f9
--- /dev/null
+++ b/devtools/client/fronts/property-iterator.js
@@ -0,0 +1,67 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ propertyIteratorSpec,
+} = require("resource://devtools/shared/specs/property-iterator.js");
+const {
+ getAdHocFrontOrPrimitiveGrip,
+} = require("resource://devtools/client/fronts/object.js");
+
+/**
+ * A PropertyIteratorFront provides a way to access to property names and
+ * values of an object efficiently, slice by slice.
+ * Note that the properties can be sorted in the backend,
+ * this is controled while creating the PropertyIteratorFront
+ * from ObjectFront.enumProperties.
+ */
+class PropertyIteratorFront extends FrontClassWithSpec(propertyIteratorSpec) {
+ form(data) {
+ this.actorID = data.actor;
+ this.count = data.count;
+ }
+
+ async slice(start, count) {
+ const result = await super.slice({ start, count });
+ return this._onResult(result);
+ }
+
+ async all() {
+ const result = await super.all();
+ return this._onResult(result);
+ }
+
+ _onResult(result) {
+ if (!result.ownProperties) {
+ return result;
+ }
+
+ // The result packet can have multiple properties that hold grips which we may need
+ // to turn into fronts.
+ const gripKeys = ["value", "getterValue", "get", "set"];
+
+ Object.entries(result.ownProperties).forEach(([key, descriptor]) => {
+ if (descriptor) {
+ for (const gripKey of gripKeys) {
+ if (descriptor.hasOwnProperty(gripKey)) {
+ result.ownProperties[key][gripKey] = getAdHocFrontOrPrimitiveGrip(
+ descriptor[gripKey],
+ this
+ );
+ }
+ }
+ }
+ });
+ return result;
+ }
+}
+
+exports.PropertyIteratorFront = PropertyIteratorFront;
+registerFront(PropertyIteratorFront);
diff --git a/devtools/client/fronts/reflow.js b/devtools/client/fronts/reflow.js
new file mode 100644
index 0000000000..027e09e9a0
--- /dev/null
+++ b/devtools/client/fronts/reflow.js
@@ -0,0 +1,31 @@
+/* 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 { reflowSpec } = require("resource://devtools/shared/specs/reflow.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+
+/**
+ * Usage example of the reflow front:
+ *
+ * let front = await target.getFront("reflow");
+ * front.on("reflows", this._onReflows);
+ * front.start();
+ * // now wait for events to come
+ */
+class ReflowFront extends FrontClassWithSpec(reflowSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "reflowActor";
+ }
+}
+
+exports.ReflowFront = ReflowFront;
+registerFront(ReflowFront);
diff --git a/devtools/client/fronts/responsive.js b/devtools/client/fronts/responsive.js
new file mode 100644
index 0000000000..c6f991c8a6
--- /dev/null
+++ b/devtools/client/fronts/responsive.js
@@ -0,0 +1,28 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ responsiveSpec,
+} = require("resource://devtools/shared/specs/responsive.js");
+
+/**
+ * The corresponding Front object for the Responsive actor.
+ */
+class ResponsiveFront extends FrontClassWithSpec(responsiveSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "responsiveActor";
+ }
+}
+
+exports.ResponsiveFront = ResponsiveFront;
+registerFront(ResponsiveFront);
diff --git a/devtools/client/fronts/root.js b/devtools/client/fronts/root.js
new file mode 100644
index 0000000000..92d780ea24
--- /dev/null
+++ b/devtools/client/fronts/root.js
@@ -0,0 +1,332 @@
+/* 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 { rootSpec } = require("resource://devtools/shared/specs/root.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+
+loader.lazyRequireGetter(
+ this,
+ "getFront",
+ "resource://devtools/shared/protocol.js",
+ true
+);
+
+class RootFront extends FrontClassWithSpec(rootSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Cache root form as this will always be the same value.
+ Object.defineProperty(this, "rootForm", {
+ get() {
+ delete this.rootForm;
+ this.rootForm = this.getRoot();
+ return this.rootForm;
+ },
+ configurable: true,
+ });
+
+ // Cache of already created global scoped fronts
+ // [typeName:string => Front instance]
+ this.fronts = new Map();
+
+ this._client = client;
+ }
+
+ form(form) {
+ // Root Front is a special Front. It is the only one to set its actor ID manually
+ // out of the form object returned by RootActor.sayHello which is called when calling
+ // DevToolsClient.connect().
+ this.actorID = form.from;
+
+ this.applicationType = form.applicationType;
+ this.traits = form.traits;
+ }
+ /**
+ * Retrieve all service worker registrations with their corresponding workers.
+ * @param {Array} [workerTargets] (optional)
+ * Array containing the result of a call to `listAllWorkerTargets`.
+ * (this exists to avoid duplication of calls to that method)
+ * @return {Object[]} result - An Array of Objects with the following format
+ * - {result[].registration} - The registration front
+ * - {result[].workers} Array of form-like objects for service workers
+ */
+ async listAllServiceWorkers(workerTargets) {
+ const result = [];
+ const { registrations } = await this.listServiceWorkerRegistrations();
+ const allWorkers = workerTargets
+ ? workerTargets
+ : await this.listAllWorkerTargets();
+
+ for (const registrationFront of registrations) {
+ // workers from the registration, ordered from most recent to older
+ const workers = [
+ registrationFront.activeWorker,
+ registrationFront.waitingWorker,
+ registrationFront.installingWorker,
+ registrationFront.evaluatingWorker,
+ ]
+ // filter out non-existing workers
+ .filter(w => !!w)
+ // build a worker object with its WorkerDescriptorFront
+ .map(workerFront => {
+ const workerDescriptorFront = allWorkers.find(
+ targetFront => targetFront.id === workerFront.id
+ );
+
+ return {
+ id: workerFront.id,
+ name: workerFront.url,
+ state: workerFront.state,
+ stateText: workerFront.stateText,
+ url: workerFront.url,
+ workerDescriptorFront,
+ };
+ });
+
+ // TODO: return only the worker targets. See Bug 1620605
+ result.push({
+ registration: registrationFront,
+ workers,
+ });
+ }
+
+ return result;
+ }
+
+ /**
+ * Retrieve all service worker registrations as well as workers from the parent and
+ * content processes. Listing service workers involves merging information coming from
+ * registrations and workers, this method will combine this information to present a
+ * unified array of serviceWorkers. If you are only interested in other workers, use
+ * listWorkers.
+ *
+ * @return {Object}
+ * - {Array} service
+ * array of form-like objects for serviceworkers
+ * - {Array} shared
+ * Array of WorkerTargetActor forms, containing shared workers.
+ * - {Array} other
+ * Array of WorkerTargetActor forms, containing other workers.
+ */
+ async listAllWorkers() {
+ const allWorkers = await this.listAllWorkerTargets();
+ const serviceWorkers = await this.listAllServiceWorkers(allWorkers);
+
+ // NOTE: listAllServiceWorkers() now returns all the workers belonging to
+ // a registration. To preserve the usual behavior at about:debugging,
+ // in which we show only the most recent one, we grab the first
+ // worker in the array only.
+ const result = {
+ service: serviceWorkers
+ .map(({ registration, workers }) => {
+ return workers.slice(0, 1).map(worker => {
+ return Object.assign(worker, {
+ registrationFront: registration,
+ fetch: registration.fetch,
+ });
+ });
+ })
+ .flat(),
+ shared: [],
+ other: [],
+ };
+
+ allWorkers.forEach(front => {
+ const worker = {
+ id: front.id,
+ url: front.url,
+ name: front.url,
+ workerDescriptorFront: front,
+ };
+
+ switch (front.type) {
+ case Ci.nsIWorkerDebugger.TYPE_SERVICE:
+ // do nothing, since we already fetched them in `serviceWorkers`
+ break;
+ case Ci.nsIWorkerDebugger.TYPE_SHARED:
+ result.shared.push(worker);
+ break;
+ default:
+ result.other.push(worker);
+ }
+ });
+
+ return result;
+ }
+
+ /** Get the target fronts for all worker threads running in any process. */
+ async listAllWorkerTargets() {
+ const listParentWorkers = async () => {
+ const { workers } = await this.listWorkers();
+ return workers;
+ };
+ const listChildWorkers = async () => {
+ const processes = await this.listProcesses();
+ const processWorkers = await Promise.all(
+ processes.map(async processDescriptorFront => {
+ // Ignore parent process
+ if (processDescriptorFront.isParentProcessDescriptor) {
+ return [];
+ }
+ try {
+ const front = await processDescriptorFront.getTarget();
+ if (!front) {
+ return [];
+ }
+ const response = await front.listWorkers();
+ return response.workers;
+ } catch (e) {
+ if (e.message.includes("Connection closed")) {
+ return [];
+ }
+ throw e;
+ }
+ })
+ );
+
+ return processWorkers.flat();
+ };
+
+ const [parentWorkers, childWorkers] = await Promise.all([
+ listParentWorkers(),
+ listChildWorkers(),
+ ]);
+
+ return parentWorkers.concat(childWorkers);
+ }
+
+ /**
+ * Fetch the ProcessDescriptorFront for the main process.
+ *
+ * `getProcess` requests allows to fetch the descriptor for any process and
+ * the main process is having the process ID zero.
+ */
+ getMainProcess() {
+ return this.getProcess(0);
+ }
+
+ /**
+ * Fetch the tab descriptor for the currently selected tab, or for a specific
+ * tab given as first parameter.
+ *
+ * @param [optional] object filter
+ * A dictionary object with following optional attributes:
+ * - browserId: use to match any tab
+ * - tab: a reference to xul:tab element (used for local tab debugging)
+ * - isWebExtension: an optional boolean to flag TabDescriptors
+ * If nothing is specified, returns the actor for the currently
+ * selected tab.
+ */
+ async getTab(filter) {
+ const packet = {};
+ if (filter) {
+ if (typeof filter.browserId == "number") {
+ packet.browserId = filter.browserId;
+ } else if ("tab" in filter) {
+ const browser = filter.tab.linkedBrowser;
+ packet.browserId = browser.browserId;
+ } else {
+ // Throw if a filter object have been passed but without
+ // any clearly idenfified filter.
+ throw new Error("Unsupported argument given to getTab request");
+ }
+ }
+
+ const descriptorFront = await super.getTab(packet);
+
+ // Will flag TabDescriptor used by WebExtension codebase.
+ if (filter?.isWebExtension) {
+ descriptorFront.setIsForWebExtension(true);
+ }
+
+ // If the tab is a local tab, forward it to the descriptor.
+ if (filter?.tab?.tagName == "tab") {
+ // Ignore the fake `tab` object we receive, where there is only a
+ // `linkedBrowser` attribute, but this isn't a real <tab> element.
+ // devtools/client/framework/test/browser_toolbox_target.js is passing such
+ // a fake tab.
+ descriptorFront.setLocalTab(filter.tab);
+ }
+
+ return descriptorFront;
+ }
+
+ /**
+ * Fetch the target front for a given add-on.
+ * This is just an helper on top of `listAddons` request.
+ *
+ * @param object filter
+ * A dictionary object with following attribute:
+ * - id: used to match the add-on to connect to.
+ */
+ async getAddon({ id }) {
+ const addons = await this.listAddons();
+ const webextensionDescriptorFront = addons.find(addon => addon.id === id);
+ return webextensionDescriptorFront;
+ }
+
+ /**
+ * Fetch the target front for a given worker.
+ * This is just an helper on top of `listAllWorkers` request.
+ *
+ * @param id
+ */
+ async getWorker(id) {
+ const { service, shared, other } = await this.listAllWorkers();
+ const worker = [...service, ...shared, ...other].find(w => w.id === id);
+ if (!worker) {
+ return null;
+ }
+ return worker.workerDescriptorFront || worker.registrationFront;
+ }
+
+ /**
+ * Test request that returns the object passed as first argument.
+ *
+ * `echo` is special as all the property of the given object have to be passed
+ * on the packet object. That's not something that can be achieve by requester helper.
+ */
+
+ echo(packet) {
+ packet.type = "echo";
+ return this.request(packet);
+ }
+
+ /*
+ * This function returns a protocol.js Front for any root actor.
+ * i.e. the one directly served from RootActor.listTabs or getRoot.
+ *
+ * @param String typeName
+ * The type name used in protocol.js's spec for this actor.
+ */
+ async getFront(typeName) {
+ let front = this.fronts.get(typeName);
+ if (front) {
+ return front;
+ }
+ const rootForm = await this.rootForm;
+ front = getFront(this._client, typeName, rootForm);
+ this.fronts.set(typeName, front);
+ return front;
+ }
+
+ /*
+ * This function returns true if the root actor has a registered global actor
+ * with a given name.
+ * @param {String} actorName
+ * The name of a global actor.
+ *
+ * @return {Boolean}
+ */
+ async hasActor(actorName) {
+ const rootForm = await this.rootForm;
+ return !!rootForm[actorName + "Actor"];
+ }
+}
+exports.RootFront = RootFront;
+registerFront(RootFront);
diff --git a/devtools/client/fronts/screenshot-content.js b/devtools/client/fronts/screenshot-content.js
new file mode 100644
index 0000000000..a1b8849a16
--- /dev/null
+++ b/devtools/client/fronts/screenshot-content.js
@@ -0,0 +1,25 @@
+/* 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 {
+ screenshotContentSpec,
+} = require("resource://devtools/shared/specs/screenshot-content.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+
+class ScreenshotContentFront extends FrontClassWithSpec(screenshotContentSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "screenshotContentActor";
+ }
+}
+
+exports.ScreenshotContentFront = ScreenshotContentFront;
+registerFront(ScreenshotContentFront);
diff --git a/devtools/client/fronts/screenshot.js b/devtools/client/fronts/screenshot.js
new file mode 100644
index 0000000000..cbceec319e
--- /dev/null
+++ b/devtools/client/fronts/screenshot.js
@@ -0,0 +1,25 @@
+/* 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 {
+ screenshotSpec,
+} = require("resource://devtools/shared/specs/screenshot.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+
+class ScreenshotFront extends FrontClassWithSpec(screenshotSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "screenshotActor";
+ }
+}
+
+exports.ScreenshotFront = ScreenshotFront;
+registerFront(ScreenshotFront);
diff --git a/devtools/client/fronts/source.js b/devtools/client/fronts/source.js
new file mode 100644
index 0000000000..d8a195bbb2
--- /dev/null
+++ b/devtools/client/fronts/source.js
@@ -0,0 +1,102 @@
+/* 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 { sourceSpec } = require("resource://devtools/shared/specs/source.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ ArrayBufferFront,
+} = require("resource://devtools/client/fronts/array-buffer.js");
+
+/**
+ * A SourceFront provides a way to access the source text of a script.
+ *
+ * @param client DevToolsClient
+ * The DevTools Client instance.
+ * @param form Object
+ * The form sent across the remote debugging protocol.
+ */
+class SourceFront extends FrontClassWithSpec(sourceSpec) {
+ constructor(client, form) {
+ super(client);
+ if (form) {
+ this._url = form.url;
+ // this is here for the time being, until the source front is managed
+ // via protocol.js marshalling
+ this.actorID = form.actor;
+ }
+ }
+
+ form(json) {
+ this._url = json.url;
+ }
+
+ get actor() {
+ return this.actorID;
+ }
+
+ get url() {
+ return this._url;
+ }
+
+ // Alias for source.blackbox to avoid changing protocol.js packets
+ blackBox(range) {
+ return this.blackbox(range);
+ }
+
+ // Alias for source.unblackbox to avoid changing protocol.js packets
+ unblackBox() {
+ return this.unblackbox();
+ }
+
+ /**
+ * Get a Front for either an ArrayBuffer or LongString
+ * for this SourceFront's source.
+ */
+ async source() {
+ const response = await super.source();
+ return this._onSourceResponse(response);
+ }
+
+ _onSourceResponse(response) {
+ const { contentType, source } = response;
+ if (source instanceof ArrayBufferFront) {
+ return source.slice(0, source.length).then(function (resp) {
+ if (resp.error) {
+ return resp;
+ }
+ // Keeping str as a string, ArrayBuffer/Uint8Array will not survive
+ // setIn/mergeIn operations.
+ const str = atob(resp.encoded);
+ const newResponse = {
+ source: {
+ binary: str,
+ toString: () => "[wasm]",
+ },
+ contentType,
+ };
+ return newResponse;
+ });
+ }
+
+ return source.substring(0, source.length).then(function (resp) {
+ if (resp.error) {
+ return resp;
+ }
+
+ const newResponse = {
+ source: resp,
+ contentType,
+ };
+ return newResponse;
+ });
+ }
+}
+
+exports.SourceFront = SourceFront;
+registerFront(SourceFront);
diff --git a/devtools/client/fronts/storage.js b/devtools/client/fronts/storage.js
new file mode 100644
index 0000000000..7637471126
--- /dev/null
+++ b/devtools/client/fronts/storage.js
@@ -0,0 +1,55 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const { childSpecs } = require("resource://devtools/shared/specs/storage.js");
+
+for (const childSpec of Object.values(childSpecs)) {
+ class ChildStorageFront extends FrontClassWithSpec(childSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this.on("single-store-update", this._onStoreUpdate.bind(this));
+ }
+
+ form(form) {
+ this.actorID = form.actor;
+ this.hosts = form.hosts;
+ this.traits = form.traits || {};
+ return null;
+ }
+
+ // Update the storage fronts `hosts` properties with potential new hosts and remove the deleted ones
+ async _onStoreUpdate({ changed, added, deleted }) {
+ // `resourceKey` comes from the storage resource and is set by the legacy listener
+ // -or- the resource transformer.
+ const { resourceKey } = this;
+ if (added) {
+ for (const host in added[resourceKey]) {
+ if (!this.hosts[host]) {
+ this.hosts[host] = added[resourceKey][host];
+ }
+ }
+ }
+ if (deleted) {
+ // While addition have to be added immediately, before ui.js receive single-store-update event
+ // Deletions have to be removed after ui.js processed single-store-update.
+ //
+ // Unfortunately it makes some tests to fail, for ex: browser_storage_cookies_delete_all.js
+ //
+ //setTimeout(()=> {
+ // for (const host in deleted[resourceKey]) {
+ // delete this.hosts[host];
+ // }
+ //}, 2000);
+ }
+ }
+ }
+ registerFront(ChildStorageFront);
+}
diff --git a/devtools/client/fronts/string.js b/devtools/client/fronts/string.js
new file mode 100644
index 0000000000..0a05d20c3e
--- /dev/null
+++ b/devtools/client/fronts/string.js
@@ -0,0 +1,62 @@
+/* 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 {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const {
+ longStringSpec,
+ SimpleStringFront,
+} = require("resource://devtools/shared/specs/string.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+
+class LongStringFront extends FrontClassWithSpec(longStringSpec) {
+ destroy() {
+ this.initial = null;
+ this.length = null;
+ this.strPromise = null;
+ super.destroy();
+ }
+
+ form(data) {
+ this.actorID = data.actor;
+ this.initial = data.initial;
+ this.length = data.length;
+
+ this._grip = data;
+ }
+
+ // We expose the grip so consumers (e.g. ObjectInspector) that handle webconsole
+ // evaluations (which can return primitive, object fronts or longString front),
+ // can directly call this without further check.
+ getGrip() {
+ return this._grip;
+ }
+
+ string() {
+ if (!this.strPromise) {
+ const promiseRest = thusFar => {
+ if (thusFar.length === this.length) {
+ return Promise.resolve(thusFar);
+ }
+ return this.substring(
+ thusFar.length,
+ thusFar.length + DevToolsServer.LONG_STRING_READ_LENGTH
+ ).then(next => promiseRest(thusFar + next));
+ };
+
+ this.strPromise = promiseRest(this.initial);
+ }
+ return this.strPromise;
+ }
+}
+
+exports.LongStringFront = LongStringFront;
+registerFront(LongStringFront);
+exports.SimpleStringFront = SimpleStringFront;
+registerFront(SimpleStringFront);
diff --git a/devtools/client/fronts/style-rule.js b/devtools/client/fronts/style-rule.js
new file mode 100644
index 0000000000..afcfb54693
--- /dev/null
+++ b/devtools/client/fronts/style-rule.js
@@ -0,0 +1,281 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ styleRuleSpec,
+} = require("resource://devtools/shared/specs/style-rule.js");
+
+loader.lazyRequireGetter(
+ this,
+ "RuleRewriter",
+ "resource://devtools/client/fronts/inspector/rule-rewriter.js"
+);
+
+/**
+ * StyleRuleFront, the front for the StyleRule actor.
+ */
+class StyleRuleFront extends FrontClassWithSpec(styleRuleSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this.before("location-changed", this._locationChangedPre.bind(this));
+ }
+
+ form(form) {
+ this.actorID = form.actor;
+ this._form = form;
+ this.traits = form.traits || {};
+ }
+
+ /**
+ * Ensure _form is updated when location-changed is emitted.
+ */
+ _locationChangedPre(line, column) {
+ this._form.line = line;
+ this._form.column = column;
+ }
+
+ /**
+ * Return a new RuleModificationList or RuleRewriter for this node.
+ * A RuleRewriter will be returned when the rule's canSetRuleText
+ * trait is true; otherwise a RuleModificationList will be
+ * returned.
+ *
+ * @param {CssPropertiesFront} cssProperties
+ * This is needed by the RuleRewriter.
+ * @return {RuleModificationList}
+ */
+ startModifyingProperties(cssProperties) {
+ if (this.canSetRuleText) {
+ return new RuleRewriter(cssProperties.isKnown, this, this.authoredText);
+ }
+ return new RuleModificationList(this);
+ }
+
+ get type() {
+ return this._form.type;
+ }
+ get line() {
+ return this._form.line || -1;
+ }
+ get column() {
+ return this._form.column || -1;
+ }
+ get cssText() {
+ return this._form.cssText;
+ }
+ get authoredText() {
+ return typeof this._form.authoredText === "string"
+ ? this._form.authoredText
+ : this._form.cssText;
+ }
+ get declarations() {
+ return this._form.declarations || [];
+ }
+ get keyText() {
+ return this._form.keyText;
+ }
+ get name() {
+ return this._form.name;
+ }
+ get selectors() {
+ return this._form.selectors;
+ }
+
+ get parentStyleSheet() {
+ const resourceCommand = this.targetFront.commands.resourceCommand;
+ return resourceCommand.getResourceById(
+ resourceCommand.TYPES.STYLESHEET,
+ this._form.parentStyleSheet
+ );
+ }
+
+ get element() {
+ return this.conn.getFrontByID(this._form.element);
+ }
+
+ get href() {
+ if (this._form.href) {
+ return this._form.href;
+ }
+ const sheet = this.parentStyleSheet;
+ return sheet ? sheet.href : "";
+ }
+
+ get nodeHref() {
+ const sheet = this.parentStyleSheet;
+ return sheet ? sheet.nodeHref : "";
+ }
+
+ get canSetRuleText() {
+ return this._form.traits && this._form.traits.canSetRuleText;
+ }
+
+ get location() {
+ return {
+ source: this.parentStyleSheet,
+ href: this.href,
+ line: this.line,
+ column: this.column,
+ };
+ }
+
+ get ancestorData() {
+ return this._form.ancestorData;
+ }
+
+ async modifySelector(node, value) {
+ const response = await super.modifySelector(
+ node,
+ value,
+ this.canSetRuleText
+ );
+
+ if (response.ruleProps) {
+ response.ruleProps = response.ruleProps.entries[0];
+ }
+ return response;
+ }
+
+ setRuleText(newText, modifications) {
+ this._form.authoredText = newText;
+ return super.setRuleText(newText, modifications);
+ }
+}
+
+exports.StyleRuleFront = StyleRuleFront;
+registerFront(StyleRuleFront);
+
+/**
+ * Convenience API for building a list of attribute modifications
+ * for the `modifyProperties` request. A RuleModificationList holds a
+ * list of modifications that will be applied to a StyleRuleActor.
+ * The modifications are processed in the order in which they are
+ * added to the RuleModificationList.
+ *
+ * Objects of this type expose the same API as @see RuleRewriter.
+ * This lets the inspector use (mostly) the same code, regardless of
+ * whether the server implements setRuleText.
+ */
+class RuleModificationList {
+ /**
+ * Initialize a RuleModificationList.
+ * @param {StyleRuleFront} rule the associated rule
+ */
+ constructor(rule) {
+ this.rule = rule;
+ this.modifications = [];
+ }
+
+ /**
+ * Apply the modifications in this object to the associated rule.
+ *
+ * @return {Promise} A promise which will be resolved when the modifications
+ * are complete; @see StyleRuleActor.modifyProperties.
+ */
+ apply() {
+ return this.rule.modifyProperties(this.modifications);
+ }
+
+ /**
+ * Add a "set" entry to the modification list.
+ *
+ * @param {Number} index index of the property in the rule.
+ * This can be -1 in the case where
+ * the rule does not support setRuleText;
+ * generally for setting properties
+ * on an element's style.
+ * @param {String} name the property's name
+ * @param {String} value the property's value
+ * @param {String} priority the property's priority, either the empty
+ * string or "important"
+ */
+ setProperty(index, name, value, priority) {
+ this.modifications.push({ type: "set", index, name, value, priority });
+ }
+
+ /**
+ * Add a "remove" entry to the modification list.
+ *
+ * @param {Number} index index of the property in the rule.
+ * This can be -1 in the case where
+ * the rule does not support setRuleText;
+ * generally for setting properties
+ * on an element's style.
+ * @param {String} name the name of the property to remove
+ */
+ removeProperty(index, name) {
+ this.modifications.push({ type: "remove", index, name });
+ }
+
+ /**
+ * Rename a property. This implementation acts like
+ * |removeProperty|, because |setRuleText| is not available.
+ *
+ * @param {Number} index index of the property in the rule.
+ * This can be -1 in the case where
+ * the rule does not support setRuleText;
+ * generally for setting properties
+ * on an element's style.
+ * @param {String} name current name of the property
+ *
+ * This parameter is also passed, but as it is not used in this
+ * implementation, it is omitted. It is documented here as this
+ * code also defined the interface implemented by @see RuleRewriter.
+ * @param {String} newName new name of the property
+ */
+ renameProperty(index, name) {
+ this.removeProperty(index, name);
+ }
+
+ /**
+ * Enable or disable a property. This implementation acts like
+ * a no-op when enabling, because |setRuleText| is not available.
+ *
+ * @param {Number} index index of the property in the rule.
+ * This can be -1 in the case where
+ * the rule does not support setRuleText;
+ * generally for setting properties
+ * on an element's style.
+ * @param {String} name current name of the property
+ * @param {Boolean} isEnabled true if the property should be enabled;
+ * false if it should be disabled
+ */
+ setPropertyEnabled(index, name, isEnabled) {
+ if (!isEnabled) {
+ this.modifications.push({ type: "disable", index, name });
+ }
+ }
+
+ /**
+ * Create a new property. This implementation does nothing, because
+ * |setRuleText| is not available.
+ *
+ * These parameters are passed, but as they are not used in this
+ * implementation, they are omitted. They are documented here as
+ * this code also defined the interface implemented by @see
+ * RuleRewriter.
+ *
+ * @param {Number} index index of the property in the rule.
+ * This can be -1 in the case where
+ * the rule does not support setRuleText;
+ * generally for setting properties
+ * on an element's style.
+ * @param {String} name name of the new property
+ * @param {String} value value of the new property
+ * @param {String} priority priority of the new property; either
+ * the empty string or "important"
+ * @param {Boolean} enabled True if the new property should be
+ * enabled, false if disabled
+ */
+ createProperty() {
+ // Nothing.
+ }
+}
diff --git a/devtools/client/fronts/style-sheets.js b/devtools/client/fronts/style-sheets.js
new file mode 100644
index 0000000000..de6010aa74
--- /dev/null
+++ b/devtools/client/fronts/style-sheets.js
@@ -0,0 +1,37 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ styleSheetsSpec,
+} = require("resource://devtools/shared/specs/style-sheets.js");
+
+/**
+ * The corresponding Front object for the StyleSheetsActor.
+ */
+class StyleSheetsFront extends FrontClassWithSpec(styleSheetsSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "styleSheetsActor";
+ }
+
+ async getTraits() {
+ if (this._traits) {
+ return this._traits;
+ }
+ const { traits } = await super.getTraits();
+ this._traits = traits;
+ return this._traits;
+ }
+}
+
+exports.StyleSheetsFront = StyleSheetsFront;
+registerFront(StyleSheetsFront);
diff --git a/devtools/client/fronts/symbol-iterator.js b/devtools/client/fronts/symbol-iterator.js
new file mode 100644
index 0000000000..1f6cd2581c
--- /dev/null
+++ b/devtools/client/fronts/symbol-iterator.js
@@ -0,0 +1,56 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ symbolIteratorSpec,
+} = require("resource://devtools/shared/specs/symbol-iterator.js");
+const {
+ getAdHocFrontOrPrimitiveGrip,
+} = require("resource://devtools/client/fronts/object.js");
+
+/**
+ * A SymbolIteratorFront is used as a front end for the SymbolIterator that is
+ * created on the server, hiding implementation details.
+ */
+class SymbolIteratorFront extends FrontClassWithSpec(symbolIteratorSpec) {
+ form(data) {
+ this.actorID = data.actor;
+ this.count = data.count;
+ }
+
+ async slice(start, count) {
+ const result = await super.slice({ start, count });
+ return this._onResult(result);
+ }
+
+ async all() {
+ const result = await super.all();
+ return this._onResult(result);
+ }
+
+ _onResult(result) {
+ if (!result.ownSymbols) {
+ return result;
+ }
+
+ result.ownSymbols.forEach((item, i) => {
+ if (item?.descriptor) {
+ result.ownSymbols[i].descriptor.value = getAdHocFrontOrPrimitiveGrip(
+ item.descriptor.value,
+ this
+ );
+ }
+ });
+ return result;
+ }
+}
+
+exports.SymbolIteratorFront = SymbolIteratorFront;
+registerFront(SymbolIteratorFront);
diff --git a/devtools/client/fronts/target-configuration.js b/devtools/client/fronts/target-configuration.js
new file mode 100644
index 0000000000..33ed2cd593
--- /dev/null
+++ b/devtools/client/fronts/target-configuration.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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ targetConfigurationSpec,
+} = require("resource://devtools/shared/specs/target-configuration.js");
+
+/**
+ * The TargetConfigurationFront/Actor should be used to populate the DevTools server
+ * with settings read from the client side but which impact the server.
+ * For instance, "disable cache" is a feature toggled via DevTools UI (client),
+ * but which should be communicated to the targets (server).
+ *
+ * See the TargetConfigurationActor for a list of supported configuration options.
+ */
+class TargetConfigurationFront extends FrontClassWithSpec(
+ targetConfigurationSpec
+) {
+ form(json) {
+ // Read the initial configuration.
+ this.initialConfiguration = json.configuration;
+ this.traits = json.traits;
+ }
+}
+
+registerFront(TargetConfigurationFront);
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);
diff --git a/devtools/client/fronts/thread-configuration.js b/devtools/client/fronts/thread-configuration.js
new file mode 100644
index 0000000000..c6b7171c70
--- /dev/null
+++ b/devtools/client/fronts/thread-configuration.js
@@ -0,0 +1,25 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ threadConfigurationSpec,
+} = require("resource://devtools/shared/specs/thread-configuration.js");
+
+/**
+ * The ThreadConfigurationFront/Actor should be used to maintain thread settings
+ * sent from the client for the thread actor.
+ *
+ * See the ThreadConfigurationActor for a list of supported configuration options.
+ */
+class ThreadConfigurationFront extends FrontClassWithSpec(
+ threadConfigurationSpec
+) {}
+
+registerFront(ThreadConfigurationFront);
diff --git a/devtools/client/fronts/thread.js b/devtools/client/fronts/thread.js
new file mode 100644
index 0000000000..f5d05aa868
--- /dev/null
+++ b/devtools/client/fronts/thread.js
@@ -0,0 +1,285 @@
+/* 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 { ThreadStateTypes } = require("resource://devtools/client/constants.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+
+const { threadSpec } = require("resource://devtools/shared/specs/thread.js");
+
+loader.lazyRequireGetter(
+ this,
+ "ObjectFront",
+ "resource://devtools/client/fronts/object.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "FrameFront",
+ "resource://devtools/client/fronts/frame.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "SourceFront",
+ "resource://devtools/client/fronts/source.js",
+ true
+);
+
+/**
+ * Creates a thread front for the remote debugging protocol server. This client
+ * is a front to the thread actor created in the server side, hiding the
+ * protocol details in a traditional JavaScript API.
+ *
+ * @param client DevToolsClient
+ * @param actor string
+ * The actor ID for this thread.
+ */
+class ThreadFront extends FrontClassWithSpec(threadSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this.client = client;
+ this._pauseGrips = {};
+ this._threadGrips = {};
+ // Note that this isn't matching ThreadActor state field.
+ // ThreadFront is only using two values: paused or attached.
+ this._state = "attached";
+
+ this._beforePaused = this._beforePaused.bind(this);
+ this._beforeResumed = this._beforeResumed.bind(this);
+ this.before("paused", this._beforePaused);
+ this.before("resumed", this._beforeResumed);
+ this.targetFront.on("will-navigate", this._onWillNavigate.bind(this));
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "threadActor";
+ }
+
+ get state() {
+ return this._state;
+ }
+
+ get paused() {
+ return this._state === "paused";
+ }
+
+ get actor() {
+ return this.actorID;
+ }
+
+ _assertPaused(command) {
+ if (!this.paused) {
+ throw Error(
+ command + " command sent while not paused. Currently " + this._state
+ );
+ }
+ }
+
+ getFrames(start, count) {
+ return super.frames(start, count);
+ }
+
+ /**
+ * Resume a paused thread. If the optional limit parameter is present, then
+ * the thread will also pause when that limit is reached.
+ *
+ * @param [optional] object limit
+ * An object with a type property set to the appropriate limit (next,
+ * step, or finish) per the remote debugging protocol specification.
+ * Use null to specify no limit.
+ */
+ async _doResume(resumeLimit, frameActorID) {
+ this._assertPaused("resume");
+
+ // Put the client in a tentative "resuming" state so we can prevent
+ // further requests that should only be sent in the paused state.
+ this._previousState = this._state;
+ this._state = "resuming";
+ try {
+ await super.resume(resumeLimit, frameActorID);
+ } catch (e) {
+ if (this._state == "resuming") {
+ // There was an error resuming, update the state to the new one
+ // reported by the server, if given (only on wrongState), otherwise
+ // reset back to the previous state.
+ if (e.state) {
+ this._state = ThreadStateTypes[e.state];
+ } else {
+ this._state = this._previousState;
+ }
+ }
+ }
+
+ delete this._previousState;
+ }
+
+ /**
+ * Resume a paused thread.
+ */
+ resume() {
+ return this._doResume(null);
+ }
+
+ /**
+ * Resume then pause without stepping.
+ *
+ */
+ resumeThenPause() {
+ return this._doResume({ type: "break" });
+ }
+
+ /**
+ * Step over a function call.
+ */
+ stepOver(frameActorID) {
+ return this._doResume({ type: "next" }, frameActorID);
+ }
+
+ /**
+ * Step into a function call.
+ */
+ stepIn(frameActorID) {
+ return this._doResume({ type: "step" }, frameActorID);
+ }
+
+ /**
+ * Step out of a function call.
+ */
+ stepOut(frameActorID) {
+ return this._doResume({ type: "finish" }, frameActorID);
+ }
+
+ /**
+ * Restart selected frame.
+ */
+ restart(frameActorID) {
+ return this._doResume({ type: "restart" }, frameActorID);
+ }
+
+ /**
+ * Immediately interrupt a running thread.
+ */
+ interrupt() {
+ return this._doInterrupt(null);
+ }
+
+ /**
+ * Pause execution right before the next JavaScript bytecode is executed.
+ */
+ breakOnNext() {
+ return this._doInterrupt("onNext");
+ }
+
+ /**
+ * Interrupt a running thread.
+ */
+ _doInterrupt(when) {
+ return super.interrupt(when);
+ }
+
+ /**
+ * Request the loaded sources for the current thread.
+ */
+ async getSources() {
+ let sources = [];
+ try {
+ sources = await super.sources();
+ } catch (e) {
+ // we may have closed the connection
+ console.log(`getSources failed. Connection may have closed: ${e}`);
+ }
+ return { sources };
+ }
+
+ /**
+ * Return a ObjectFront object for the given object grip.
+ *
+ * @param grip object
+ * A pause-lifetime object grip returned by the protocol.
+ */
+ pauseGrip(grip) {
+ if (grip.actor in this._pauseGrips) {
+ return this._pauseGrips[grip.actor];
+ }
+
+ const objectFront = new ObjectFront(
+ this.conn,
+ this.targetFront,
+ this,
+ grip
+ );
+ this._pauseGrips[grip.actor] = objectFront;
+ return objectFront;
+ }
+
+ /**
+ * Clear and invalidate all the grip fronts from the given cache.
+ *
+ * @param gripCacheName
+ * The property name of the grip cache we want to clear.
+ */
+ _clearObjectFronts(gripCacheName) {
+ for (const id in this[gripCacheName]) {
+ this[gripCacheName][id].valid = false;
+ }
+ this[gripCacheName] = {};
+ }
+
+ /**
+ * Invalidate pause-lifetime grip clients and clear the list of current grip
+ * clients.
+ */
+ _clearPauseGrips() {
+ this._clearObjectFronts("_pauseGrips");
+ }
+
+ _beforePaused(packet) {
+ this._state = "paused";
+ this._onThreadState(packet);
+ }
+
+ _beforeResumed() {
+ this._state = "attached";
+ this._onThreadState(null);
+ this.unmanageChildren(FrameFront);
+ }
+
+ _onWillNavigate() {
+ this.unmanageChildren(SourceFront);
+ }
+
+ /**
+ * Handle thread state change by doing necessary cleanup
+ */
+ _onThreadState(packet) {
+ // The debugger UI may not be initialized yet so we want to keep
+ // the packet around so it knows what to pause state to display
+ // when it's initialized
+ this._lastPausePacket = packet;
+ this._clearPauseGrips();
+ }
+
+ getLastPausePacket() {
+ return this._lastPausePacket;
+ }
+
+ /**
+ * Return an instance of SourceFront for the given source actor form.
+ */
+ source(form) {
+ if (form.actor in this._threadGrips) {
+ return this._threadGrips[form.actor];
+ }
+
+ const sourceFront = new SourceFront(this.client, form);
+ this.manage(sourceFront);
+ this._threadGrips[form.actor] = sourceFront;
+ return sourceFront;
+ }
+}
+
+exports.ThreadFront = ThreadFront;
+registerFront(ThreadFront);
diff --git a/devtools/client/fronts/tracer.js b/devtools/client/fronts/tracer.js
new file mode 100644
index 0000000000..d5df948be3
--- /dev/null
+++ b/devtools/client/fronts/tracer.js
@@ -0,0 +1,22 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const { tracerSpec } = require("resource://devtools/shared/specs/tracer.js");
+
+class TracerFront extends FrontClassWithSpec(tracerSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "tracerActor";
+ }
+}
+
+registerFront(TracerFront);
diff --git a/devtools/client/fronts/walker.js b/devtools/client/fronts/walker.js
new file mode 100644
index 0000000000..5feae2a343
--- /dev/null
+++ b/devtools/client/fronts/walker.js
@@ -0,0 +1,461 @@
+/* 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 {
+ FrontClassWithSpec,
+ types,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const { walkerSpec } = require("resource://devtools/shared/specs/walker.js");
+const {
+ safeAsyncMethod,
+} = require("resource://devtools/shared/async-utils.js");
+
+/**
+ * Client side of the DOM walker.
+ */
+class WalkerFront extends FrontClassWithSpec(walkerSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this._isPicking = false;
+ this._orphaned = new Set();
+ this._retainedOrphans = new Set();
+
+ // Set to true if cleanup should be requested after every mutation list.
+ this.autoCleanup = true;
+
+ this._rootNodePromise = new Promise(
+ r => (this._rootNodePromiseResolve = r)
+ );
+
+ this._onRootNodeAvailable = this._onRootNodeAvailable.bind(this);
+ this._onRootNodeDestroyed = this._onRootNodeDestroyed.bind(this);
+
+ // pick/cancelPick requests can be triggered while the Walker is being destroyed.
+ this.pick = safeAsyncMethod(this.pick.bind(this), () => this.isDestroyed());
+ this.cancelPick = safeAsyncMethod(this.cancelPick.bind(this), () =>
+ this.isDestroyed()
+ );
+
+ this.before("new-mutations", this.onMutations.bind(this));
+
+ // Those events will be emitted if watchRootNode was called on the
+ // corresponding WalkerActor, which should be handled by the ResourceCommand
+ // as long as a consumer is watching for root-node resources.
+ // This should be fixed by using watchResources directly from the walker
+ // front, either with the ROOT_NODE resource, or with the DOCUMENT_EVENT
+ // resource. See Bug 1663973.
+ this.on("root-available", this._onRootNodeAvailable);
+ this.on("root-destroyed", this._onRootNodeDestroyed);
+ }
+
+ // Update the object given a form representation off the wire.
+ form(json) {
+ this.actorID = json.actor;
+
+ // The rootNode property should usually be provided via watchRootNode.
+ // However tests are currently using the walker front without explicitly
+ // calling watchRootNode, so we keep this assignment as a fallback.
+ this.rootNode = types.getType("domnode").read(json.root, this);
+
+ this.traits = json.traits;
+ }
+
+ /**
+ * Clients can use walker.rootNode to get the current root node of the
+ * walker, but during a reload the root node might be null. This
+ * method returns a promise that will resolve to the root node when it is
+ * set.
+ */
+ async getRootNode() {
+ let rootNode = this.rootNode;
+ if (!rootNode) {
+ rootNode = await this._rootNodePromise;
+ }
+
+ return rootNode;
+ }
+
+ /**
+ * When reading an actor form off the wire, we want to hook it up to its
+ * parent or host front. The protocol guarantees that the parent will
+ * be seen by the client in either a previous or the current request.
+ * So if we've already seen this parent return it, otherwise create
+ * a bare-bones stand-in node. The stand-in node will be updated
+ * with a real form by the end of the deserialization.
+ */
+ ensureDOMNodeFront(id) {
+ const front = this.getActorByID(id);
+ if (front) {
+ return front;
+ }
+
+ return types.getType("domnode").read({ actor: id }, this, "standin");
+ }
+
+ /**
+ * See the documentation for WalkerActor.prototype.retainNode for
+ * information on retained nodes.
+ *
+ * From the client's perspective, `retainNode` can fail if the node in
+ * question is removed from the ownership tree before the `retainNode`
+ * request reaches the server. This can only happen if the client has
+ * asked the server to release nodes but hasn't gotten a response
+ * yet: Either a `releaseNode` request or a `getMutations` with `cleanup`
+ * set is outstanding.
+ *
+ * If either of those requests is outstanding AND releases the retained
+ * node, this request will fail with noSuchActor, but the ownership tree
+ * will stay in a consistent state.
+ *
+ * Because the protocol guarantees that requests will be processed and
+ * responses received in the order they were sent, we get the right
+ * semantics by setting our local retained flag on the node only AFTER
+ * a SUCCESSFUL retainNode call.
+ */
+ async retainNode(node) {
+ await super.retainNode(node);
+ node.retained = true;
+ }
+
+ async unretainNode(node) {
+ await super.unretainNode(node);
+ node.retained = false;
+ if (this._retainedOrphans.has(node)) {
+ this._retainedOrphans.delete(node);
+ this._releaseFront(node);
+ }
+ }
+
+ releaseNode(node, options = {}) {
+ // NodeFront.destroy will destroy children in the ownership tree too,
+ // mimicking what the server will do here.
+ const actorID = node.actorID;
+ this._releaseFront(node, !!options.force);
+ return super.releaseNode({ actorID });
+ }
+
+ async findInspectingNode() {
+ const response = await super.findInspectingNode();
+ return response.node;
+ }
+
+ async querySelector(queryNode, selector) {
+ const response = await super.querySelector(queryNode, selector);
+ return response.node;
+ }
+
+ async getNodeActorFromWindowID(windowID) {
+ const response = await super.getNodeActorFromWindowID(windowID);
+ return response ? response.node : null;
+ }
+
+ async getNodeActorFromContentDomReference(contentDomReference) {
+ const response = await super.getNodeActorFromContentDomReference(
+ contentDomReference
+ );
+ return response ? response.node : null;
+ }
+
+ async getStyleSheetOwnerNode(styleSheetActorID) {
+ const response = await super.getStyleSheetOwnerNode(styleSheetActorID);
+ return response ? response.node : null;
+ }
+
+ async getNodeFromActor(actorID, path) {
+ const response = await super.getNodeFromActor(actorID, path);
+ return response ? response.node : null;
+ }
+
+ _releaseFront(node, force) {
+ if (node.retained && !force) {
+ node.reparent(null);
+ this._retainedOrphans.add(node);
+ return;
+ }
+
+ if (node.retained) {
+ // Forcing a removal.
+ this._retainedOrphans.delete(node);
+ }
+
+ // Release any children
+ for (const child of node.treeChildren()) {
+ this._releaseFront(child, force);
+ }
+
+ // All children will have been removed from the node by this point.
+ node.reparent(null);
+ node.destroy();
+ }
+
+ /**
+ * Get any unprocessed mutation records and process them.
+ */
+ // eslint-disable-next-line complexity
+ async getMutations(options = {}) {
+ const mutations = await super.getMutations(options);
+ const emitMutations = [];
+ for (const change of mutations) {
+ // The target is only an actorID, get the associated front.
+ const targetID = change.target;
+ const targetFront = this.getActorByID(targetID);
+
+ if (!targetFront) {
+ console.warn(
+ "Got a mutation for an unexpected actor: " +
+ targetID +
+ ", please file a bug on bugzilla.mozilla.org!"
+ );
+ console.trace();
+ continue;
+ }
+
+ const emittedMutation = Object.assign(change, { target: targetFront });
+
+ if (change.type === "childList") {
+ // Update the ownership tree according to the mutation record.
+ const addedFronts = [];
+ const removedFronts = [];
+ for (const removed of change.removed) {
+ const removedFront = this.getActorByID(removed);
+ if (!removedFront) {
+ console.error(
+ "Got a removal of an actor we didn't know about: " + removed
+ );
+ continue;
+ }
+ // Remove from the ownership tree
+ removedFront.reparent(null);
+
+ // This node is orphaned unless we get it in the 'added' list
+ // eventually.
+ this._orphaned.add(removedFront);
+ removedFronts.push(removedFront);
+ }
+ for (const added of change.added) {
+ const addedFront = this.getActorByID(added);
+ if (!addedFront) {
+ console.error(
+ "Got an addition of an actor we didn't know " + "about: " + added
+ );
+ continue;
+ }
+ addedFront.reparent(targetFront);
+
+ // The actor is reconnected to the ownership tree, unorphan
+ // it.
+ this._orphaned.delete(addedFront);
+ addedFronts.push(addedFront);
+ }
+
+ // Before passing to users, replace the added and removed actor
+ // ids with front in the mutation record.
+ emittedMutation.added = addedFronts;
+ emittedMutation.removed = removedFronts;
+
+ // If this is coming from a DOM mutation, the actor's numChildren
+ // was passed in. Otherwise, it is simulated from a frame load or
+ // unload, so don't change the front's form.
+ if ("numChildren" in change) {
+ targetFront._form.numChildren = change.numChildren;
+ }
+ } else if (change.type === "shadowRootAttached") {
+ targetFront._form.isShadowHost = true;
+ } else if (change.type === "customElementDefined") {
+ targetFront._form.customElementLocation = change.customElementLocation;
+ } else if (change.type === "unretained") {
+ // Retained orphans were force-released without the intervention of
+ // client (probably a navigated frame).
+ for (const released of change.nodes) {
+ const releasedFront = this.getActorByID(released);
+ this._retainedOrphans.delete(released);
+ this._releaseFront(releasedFront, true);
+ }
+ } else {
+ targetFront.updateMutation(change);
+ }
+
+ // Update the inlineTextChild property of the target for a selected list of
+ // mutation types.
+ if (
+ change.type === "inlineTextChild" ||
+ change.type === "childList" ||
+ change.type === "shadowRootAttached"
+ ) {
+ if (change.inlineTextChild) {
+ targetFront.inlineTextChild = types
+ .getType("domnode")
+ .read(change.inlineTextChild, this);
+ } else {
+ targetFront.inlineTextChild = undefined;
+ }
+ }
+
+ emitMutations.push(emittedMutation);
+ }
+
+ if (options.cleanup) {
+ for (const node of this._orphaned) {
+ // This will move retained nodes to this._retainedOrphans.
+ this._releaseFront(node);
+ }
+ this._orphaned = new Set();
+ }
+
+ this.emit("mutations", emitMutations);
+ }
+
+ /**
+ * Handle the `new-mutations` notification by fetching the
+ * available mutation records.
+ */
+ onMutations() {
+ // Fetch and process the mutations.
+ this.getMutations({ cleanup: this.autoCleanup }).catch(() => {});
+ }
+
+ isLocal() {
+ return !!this.conn._transport._serverConnection;
+ }
+
+ async removeNode(node) {
+ const previousSibling = await this.previousSibling(node);
+ const nextSibling = await super.removeNode(node);
+ return {
+ previousSibling,
+ nextSibling,
+ };
+ }
+
+ async children(node, options) {
+ if (!node.useChildTargetToFetchChildren) {
+ return super.children(node, options);
+ }
+ const target = await node.connectToFrame();
+
+ // We had several issues in the past where `connectToFrame` was returning the same
+ // target as the owner document one, which led to the inspector being broken.
+ // Ultimately, we shouldn't get to this point (fix should happen in connectToFrame or
+ // on the server, e.g. for Bug 1752342), but at least this will serve as a safe guard
+ // so we don't freeze/crash the inspector.
+ if (
+ target == this.targetFront &&
+ Services.prefs.getBoolPref(
+ "devtools.testing.bypass-walker-children-iframe-guard",
+ false
+ ) !== true
+ ) {
+ console.warn("connectToFrame returned an unexpected target");
+ return {
+ nodes: [],
+ hasFirst: true,
+ hasLast: true,
+ };
+ }
+
+ const walker = (await target.getFront("inspector")).walker;
+
+ // Finally retrieve the NodeFront of the remote frame's document
+ const documentNode = await walker.getRootNode();
+
+ // Force reparenting through the remote frame boundary.
+ documentNode.reparent(node);
+
+ // And return the same kind of response `walker.children` returns
+ return {
+ nodes: [documentNode],
+ hasFirst: true,
+ hasLast: true,
+ };
+ }
+
+ /**
+ * Ensure that the RootNode of this Walker has the right parent NodeFront.
+ *
+ * This method does nothing if we are on the top level target's WalkerFront,
+ * as the RootNode won't have any parent.
+ *
+ * Otherwise, if we are in an iframe's WalkerFront, we would expect the parent
+ * of the RootNode (i.e. the NodeFront for the document loaded within the iframe)
+ * to be the <iframe>'s NodeFront. Because of fission, the two NodeFront may refer
+ * to DOM Element running in distinct processes and so the NodeFront comes from
+ * two distinct Targets and two distinct WalkerFront.
+ * This is why we need this manual "reparent" code to do the glue between the
+ * two documents.
+ */
+ async reparentRemoteFrame() {
+ const parentTarget = await this.targetFront.getParentTarget();
+ if (!parentTarget) {
+ return;
+ }
+ // Don't reparent if we are on the top target
+ if (parentTarget == this.targetFront) {
+ return;
+ }
+ // Get the NodeFront for the embedder element
+ // i.e. the <iframe> element which is hosting the document that
+ const parentWalker = (await parentTarget.getFront("inspector")).walker;
+ // As this <iframe> most likely runs in another process, we have to get it through the parent
+ // target's WalkerFront.
+ const parentNode = (
+ await parentWalker.getEmbedderElement(this.targetFront.browsingContextID)
+ ).node;
+
+ // Finally, set this embedder element's node front as the
+ const documentNode = await this.getRootNode();
+ documentNode.reparent(parentNode);
+ }
+
+ _onRootNodeAvailable(rootNode) {
+ if (rootNode.isTopLevelDocument) {
+ this.rootNode = rootNode;
+ this._rootNodePromiseResolve(this.rootNode);
+ }
+ }
+
+ _onRootNodeDestroyed(rootNode) {
+ if (rootNode.isTopLevelDocument) {
+ this._rootNodePromise = new Promise(
+ r => (this._rootNodePromiseResolve = r)
+ );
+ this.rootNode = null;
+ }
+ }
+
+ /**
+ * Start the element picker on the debuggee target.
+ * @param {Boolean} doFocus - Optionally focus the content area once the picker is
+ * activated.
+ */
+ pick(doFocus) {
+ if (this._isPicking) {
+ return Promise.resolve();
+ }
+
+ this._isPicking = true;
+
+ return super.pick(
+ doFocus,
+ this.targetFront.commands.descriptorFront.isLocalTab
+ );
+ }
+
+ /**
+ * Stop the element picker.
+ */
+ cancelPick() {
+ if (!this._isPicking) {
+ return Promise.resolve();
+ }
+
+ this._isPicking = false;
+ return super.cancelPick();
+ }
+}
+
+exports.WalkerFront = WalkerFront;
+registerFront(WalkerFront);
diff --git a/devtools/client/fronts/watcher.js b/devtools/client/fronts/watcher.js
new file mode 100644
index 0000000000..1a7499561a
--- /dev/null
+++ b/devtools/client/fronts/watcher.js
@@ -0,0 +1,202 @@
+/* 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 { watcherSpec } = require("resource://devtools/shared/specs/watcher.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+
+loader.lazyRequireGetter(
+ this,
+ "WindowGlobalTargetFront",
+ "resource://devtools/client/fronts/targets/window-global.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "ContentProcessTargetFront",
+ "resource://devtools/client/fronts/targets/content-process.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "WorkerTargetFront",
+ "resource://devtools/client/fronts/targets/worker.js",
+ true
+);
+
+class WatcherFront extends FrontClassWithSpec(watcherSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+
+ this._onTargetAvailable = this._onTargetAvailable.bind(this);
+ this._onTargetDestroyed = this._onTargetDestroyed.bind(this);
+
+ // Convert form, which is just JSON object to Fronts for these two events
+ this.on("target-available-form", this._onTargetAvailable);
+ this.on("target-destroyed-form", this._onTargetDestroyed);
+ }
+
+ form(json) {
+ this.actorID = json.actor;
+ this.traits = json.traits;
+ }
+
+ _onTargetAvailable(form) {
+ let front;
+ if (form.actor.includes("/contentProcessTarget")) {
+ front = new ContentProcessTargetFront(this.conn, null, this);
+ } else if (form.actor.includes("/workerTarget")) {
+ front = new WorkerTargetFront(this.conn, null, this);
+ } else {
+ front = new WindowGlobalTargetFront(this.conn, null, this);
+ }
+ front.actorID = form.actor;
+ front.form(form);
+ this.manage(front);
+ this.emit("target-available", front);
+ }
+
+ _onTargetDestroyed(form, options = {}) {
+ const front = this._getTargetFront(form);
+
+ // When server side target switching is off,
+ // the watcher may notify us about the top level target destruction a bit late.
+ // The descriptor (`this.parentFront`) already switched to the new target.
+ // Missing `target-destroyed` isn't critical when target switching is off
+ // as `TargetCommand.switchToTarget` will end calling `TargetCommandonTargetDestroyed` for all
+ // existing targets.
+ // https://searchfox.org/mozilla-central/rev/af8e5d37fd56be90ccddae2203e7b875d3f3ae87/devtools/shared/commands/target/target-command.js#166-173
+ if (front) {
+ this.emit("target-destroyed", front, options);
+ }
+ }
+
+ _getTargetFront(form) {
+ let front = this.getActorByID(form.actor);
+ // For top level target, the target will be a child of the descriptor front,
+ // which happens to be the parent front of the watcher.
+ if (!front) {
+ front = this.parentFront.getActorByID(form.actor);
+ }
+ return front;
+ }
+
+ /**
+ * Retrieve the already existing WindowGlobalTargetFront for the parent
+ * BrowsingContext of the given BrowsingContext ID.
+ */
+ async getParentWindowGlobalTarget(browsingContextID) {
+ const id = await this.getParentBrowsingContextID(browsingContextID);
+ if (!id) {
+ return null;
+ }
+ return this.getWindowGlobalTarget(id);
+ }
+
+ /**
+ * Memoized getter for the "blackboxing" actor
+ */
+ async getBlackboxingActor() {
+ if (!this._blackboxingActor) {
+ this._blackboxingActor = await super.getBlackboxingActor();
+ }
+ return this._blackboxingActor;
+ }
+ /**
+ * Memoized getter for the "breakpoint-list" actor
+ */
+ async getBreakpointListActor() {
+ if (!this._breakpointListActor) {
+ this._breakpointListActor = await super.getBreakpointListActor();
+ }
+ return this._breakpointListActor;
+ }
+
+ /**
+ * Memoized getter for the "target-configuration" actor
+ */
+ async getTargetConfigurationActor() {
+ if (!this._targetConfigurationActor) {
+ this._targetConfigurationActor =
+ await super.getTargetConfigurationActor();
+ }
+ return this._targetConfigurationActor;
+ }
+
+ /**
+ * Memoized getter for the "thread-configuration" actor
+ */
+ async getThreadConfigurationActor() {
+ if (!this._threadConfigurationActor) {
+ this._threadConfigurationActor =
+ await super.getThreadConfigurationActor();
+ }
+ return this._threadConfigurationActor;
+ }
+
+ /**
+ * For a given BrowsingContext ID, return the already existing WindowGlobalTargetFront
+ */
+ async getWindowGlobalTarget(id) {
+ // First scan the watcher children as the watcher manages all the targets
+ for (const front of this.poolChildren()) {
+ if (front.browsingContextID == id) {
+ return front;
+ }
+ }
+ // But the top level target will be created by the Descriptor.getTarget() method
+ // and so be hosted in the Descriptor's pool.
+ // The parent front of the WatcherActor happens to be the Descriptor Actor.
+ // This code could go away or be simplified if the Descriptor starts fetch all
+ // the targets, including the top level one via the Watcher. i.e. drop Descriptor.getTarget().
+ const topLevelTarget = await this.parentFront.getTarget();
+ if (topLevelTarget?.browsingContextID == id) {
+ return topLevelTarget;
+ }
+
+ // If we could not find a window global target for the provided id, the
+ // window global might not be the topmost one of a given process (isProcessRoot == true).
+ // For now we only create targets for the top window global of each process,
+ // so we recursively check the parent browsing context ids
+ // until we find a valid target.
+ const parentBrowsingContextID = await this.getParentBrowsingContextID(id);
+ if (parentBrowsingContextID && parentBrowsingContextID !== id) {
+ return this.getWindowGlobalTarget(parentBrowsingContextID);
+ }
+
+ return null;
+ }
+
+ getWindowGlobalTargetByInnerWindowId(innerWindowId) {
+ for (const front of this.poolChildren()) {
+ if (front.innerWindowId == innerWindowId) {
+ return front;
+ }
+ }
+ // Use getCachedTarget in order to have a fully synchronous method
+ // as the callsite in ResourceCommand benefit from being synchronous.
+ // Here we care only about already existing resource and do not need to
+ // wait for the next target to come.
+ const topLevelTarget = this.parentFront.getCachedTarget();
+ if (topLevelTarget?.innerWindowId == innerWindowId) {
+ return topLevelTarget;
+ }
+ console.error("Unable to find target with innerWindowId:" + innerWindowId);
+ return null;
+ }
+
+ /**
+ * Memoized getter for the "networkParent" actor
+ */
+ async getNetworkParentActor() {
+ if (!this._networkParentActor) {
+ this._networkParentActor = await super.getNetworkParentActor();
+ }
+ return this._networkParentActor;
+ }
+}
+registerFront(WatcherFront);
diff --git a/devtools/client/fronts/webconsole.js b/devtools/client/fronts/webconsole.js
new file mode 100644
index 0000000000..e71b04841f
--- /dev/null
+++ b/devtools/client/fronts/webconsole.js
@@ -0,0 +1,315 @@
+/* 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 {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const {
+ webconsoleSpec,
+} = require("resource://devtools/shared/specs/webconsole.js");
+const {
+ getAdHocFrontOrPrimitiveGrip,
+} = require("resource://devtools/client/fronts/object.js");
+
+/**
+ * A WebConsoleFront is used as a front end for the WebConsoleActor that is
+ * created on the server, hiding implementation details.
+ *
+ * @param object client
+ * The DevToolsClient instance we live for.
+ */
+class WebConsoleFront extends FrontClassWithSpec(webconsoleSpec) {
+ constructor(client, targetFront, parentFront) {
+ super(client, targetFront, parentFront);
+ this._client = client;
+ this.events = [];
+
+ // Attribute name from which to retrieve the actorID out of the target actor's form
+ this.formAttributeName = "consoleActor";
+
+ this._onNetworkEventUpdate = this._onNetworkEventUpdate.bind(this);
+
+ this.before("consoleAPICall", this.beforeConsoleAPICall);
+ this.before("pageError", this.beforePageError);
+ this.before("serverNetworkEvent", this.beforeServerNetworkEvent);
+
+ this._client.on("networkEventUpdate", this._onNetworkEventUpdate);
+ }
+
+ get actor() {
+ return this.actorID;
+ }
+
+ /**
+ * The "networkEventUpdate" message type handler. We redirect any message to
+ * the UI for displaying.
+ *
+ * @private
+ * @param object packet
+ * The message received from the server.
+ */
+ _onNetworkEventUpdate(packet) {
+ this.emit("serverNetworkUpdateEvent", packet);
+ }
+
+ beforeServerNetworkEvent(packet) {
+ // The stacktrace info needs to be sent before
+ // the network event.
+ this.emit("serverNetworkStackTrace", packet);
+ }
+
+ beforeConsoleAPICall(packet) {
+ if (packet.message && Array.isArray(packet.message.arguments)) {
+ // We might need to create fronts for each of the message arguments.
+ packet.message.arguments = packet.message.arguments.map(arg =>
+ getAdHocFrontOrPrimitiveGrip(arg, this)
+ );
+ }
+ return packet;
+ }
+
+ beforePageError(packet) {
+ if (packet?.pageError?.errorMessage) {
+ packet.pageError.errorMessage = getAdHocFrontOrPrimitiveGrip(
+ packet.pageError.errorMessage,
+ this
+ );
+ }
+
+ if (packet?.pageError?.exception) {
+ packet.pageError.exception = getAdHocFrontOrPrimitiveGrip(
+ packet.pageError.exception,
+ this
+ );
+ }
+ return packet;
+ }
+
+ async getCachedMessages(messageTypes) {
+ const response = await super.getCachedMessages(messageTypes);
+ if (Array.isArray(response.messages)) {
+ response.messages = response.messages.map(packet => {
+ if (Array.isArray(packet?.message?.arguments)) {
+ // We might need to create fronts for each of the message arguments.
+ packet.message.arguments = packet.message.arguments.map(arg =>
+ getAdHocFrontOrPrimitiveGrip(arg, this)
+ );
+ }
+
+ if (packet.pageError?.exception) {
+ packet.pageError.exception = getAdHocFrontOrPrimitiveGrip(
+ packet.pageError.exception,
+ this
+ );
+ }
+
+ return packet;
+ });
+ }
+ return response;
+ }
+
+ /**
+ * Retrieve the request headers from the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getRequestHeaders(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getRequestHeaders",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the request cookies from the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getRequestCookies(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getRequestCookies",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the request post data from the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getRequestPostData(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getRequestPostData",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the response headers from the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getResponseHeaders(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getResponseHeaders",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the response cookies from the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getResponseCookies(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getResponseCookies",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the response content from the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getResponseContent(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getResponseContent",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the response cache from the given NetworkEventActor
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces.
+ */
+ getResponseCache(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getResponseCache",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the timing information for the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getEventTimings(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getEventTimings",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the security information for the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the response is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getSecurityInfo(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getSecurityInfo",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Retrieve the stack-trace information for the given NetworkEventActor.
+ *
+ * @param string actor
+ * The NetworkEventActor ID.
+ * @param function onResponse
+ * The function invoked when the stack-trace is received.
+ * @return request
+ * Request object that implements both Promise and EventEmitter interfaces
+ */
+ getStackTrace(actor, onResponse) {
+ const packet = {
+ to: actor,
+ type: "getStackTrace",
+ };
+ return this._client.request(packet, onResponse);
+ }
+
+ /**
+ * Close the WebConsoleFront.
+ *
+ */
+ destroy() {
+ if (!this._client) {
+ return null;
+ }
+
+ this._client.off("networkEventUpdate", this._onNetworkEventUpdate);
+ // This will make future calls to this function harmless because of the early return
+ // at the top of the function.
+ this._client = null;
+
+ return super.destroy();
+ }
+}
+
+exports.WebConsoleFront = WebConsoleFront;
+registerFront(WebConsoleFront);
diff --git a/devtools/client/fronts/worker/moz.build b/devtools/client/fronts/worker/moz.build
new file mode 100644
index 0000000000..dae0e2d606
--- /dev/null
+++ b/devtools/client/fronts/worker/moz.build
@@ -0,0 +1,11 @@
+# -*- 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(
+ "push-subscription.js",
+ "service-worker-registration.js",
+ "service-worker.js",
+)
diff --git a/devtools/client/fronts/worker/push-subscription.js b/devtools/client/fronts/worker/push-subscription.js
new file mode 100644
index 0000000000..790b32350e
--- /dev/null
+++ b/devtools/client/fronts/worker/push-subscription.js
@@ -0,0 +1,30 @@
+/* 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 {
+ pushSubscriptionSpec,
+} = require("resource://devtools/shared/specs/worker/push-subscription.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+
+class PushSubscriptionFront extends FrontClassWithSpec(pushSubscriptionSpec) {
+ get endpoint() {
+ return this._form.endpoint;
+ }
+
+ get quota() {
+ return this._form.quota;
+ }
+
+ form(form) {
+ this.actorID = form.actor;
+ this._form = form;
+ }
+}
+
+exports.PushSubscriptionFront = PushSubscriptionFront;
+registerFront(PushSubscriptionFront);
diff --git a/devtools/client/fronts/worker/service-worker-registration.js b/devtools/client/fronts/worker/service-worker-registration.js
new file mode 100644
index 0000000000..57e2bc400b
--- /dev/null
+++ b/devtools/client/fronts/worker/service-worker-registration.js
@@ -0,0 +1,79 @@
+/* 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 {
+ serviceWorkerRegistrationSpec,
+} = require("resource://devtools/shared/specs/worker/service-worker-registration.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+ types,
+} = require("resource://devtools/shared/protocol.js");
+
+class ServiceWorkerRegistrationFront extends FrontClassWithSpec(
+ serviceWorkerRegistrationSpec
+) {
+ get active() {
+ return this._form.active;
+ }
+
+ get fetch() {
+ return this._form.fetch;
+ }
+
+ get id() {
+ return this.url;
+ }
+
+ get lastUpdateTime() {
+ return this._form.lastUpdateTime;
+ }
+
+ get scope() {
+ return this._form.scope;
+ }
+
+ get type() {
+ return this._form.type;
+ }
+
+ get url() {
+ return this._form.url;
+ }
+
+ get evaluatingWorker() {
+ return this._getServiceWorker("evaluatingWorker");
+ }
+
+ get activeWorker() {
+ return this._getServiceWorker("activeWorker");
+ }
+
+ get installingWorker() {
+ return this._getServiceWorker("installingWorker");
+ }
+
+ get waitingWorker() {
+ return this._getServiceWorker("waitingWorker");
+ }
+
+ _getServiceWorker(type) {
+ const workerForm = this._form[type];
+ if (!workerForm) {
+ return null;
+ }
+ return types.getType("serviceWorker").read(workerForm, this);
+ }
+
+ form(form) {
+ this.actorID = form.actor;
+ this._form = form;
+ // @backward-compat { version 70 } ServiceWorkerRegistration actor now exposes traits
+ this.traits = form.traits || {};
+ }
+}
+
+exports.ServiceWorkerRegistrationFront = ServiceWorkerRegistrationFront;
+registerFront(ServiceWorkerRegistrationFront);
diff --git a/devtools/client/fronts/worker/service-worker.js b/devtools/client/fronts/worker/service-worker.js
new file mode 100644
index 0000000000..604eb388e5
--- /dev/null
+++ b/devtools/client/fronts/worker/service-worker.js
@@ -0,0 +1,62 @@
+/* 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 {
+ serviceWorkerSpec,
+} = require("resource://devtools/shared/specs/worker/service-worker.js");
+const {
+ FrontClassWithSpec,
+ registerFront,
+} = require("resource://devtools/shared/protocol.js");
+const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
+
+const L10N = new LocalizationHelper(
+ "devtools/client/locales/debugger.properties"
+);
+
+class ServiceWorkerFront extends FrontClassWithSpec(serviceWorkerSpec) {
+ get fetch() {
+ return this._form.fetch;
+ }
+
+ get url() {
+ return this._form.url;
+ }
+
+ get state() {
+ return this._form.state;
+ }
+
+ get stateText() {
+ switch (this.state) {
+ case Ci.nsIServiceWorkerInfo.STATE_PARSED:
+ return L10N.getStr("serviceWorkerInfo.parsed");
+ case Ci.nsIServiceWorkerInfo.STATE_INSTALLING:
+ return L10N.getStr("serviceWorkerInfo.installing");
+ case Ci.nsIServiceWorkerInfo.STATE_INSTALLED:
+ return L10N.getStr("serviceWorkerInfo.installed");
+ case Ci.nsIServiceWorkerInfo.STATE_ACTIVATING:
+ return L10N.getStr("serviceWorkerInfo.activating");
+ case Ci.nsIServiceWorkerInfo.STATE_ACTIVATED:
+ return L10N.getStr("serviceWorkerInfo.activated");
+ case Ci.nsIServiceWorkerInfo.STATE_REDUNDANT:
+ return L10N.getStr("serviceWorkerInfo.redundant");
+ default:
+ return L10N.getStr("serviceWorkerInfo.unknown");
+ }
+ }
+
+ get id() {
+ return this._form.id;
+ }
+
+ form(form) {
+ this.actorID = form.actor;
+ this._form = form;
+ }
+}
+
+exports.ServiceWorkerFront = ServiceWorkerFront;
+registerFront(ServiceWorkerFront);