diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /devtools/client/fronts/walker.js | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/fronts/walker.js')
-rw-r--r-- | devtools/client/fronts/walker.js | 461 |
1 files changed, 461 insertions, 0 deletions
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); |