summaryrefslogtreecommitdiffstats
path: root/devtools/client/fronts/targets/target-mixin.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/fronts/targets/target-mixin.js')
-rw-r--r--devtools/client/fronts/targets/target-mixin.js750
1 files changed, 750 insertions, 0 deletions
diff --git a/devtools/client/fronts/targets/target-mixin.js b/devtools/client/fronts/targets/target-mixin.js
new file mode 100644
index 0000000000..937eabdbd1
--- /dev/null
+++ b/devtools/client/fronts/targets/target-mixin.js
@@ -0,0 +1,750 @@
+/* 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", "devtools/shared/protocol", true);
+loader.lazyRequireGetter(
+ this,
+ "getThreadOptions",
+ "devtools/client/shared/thread-utils",
+ 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 BrowsingContextTarget:
+ * - 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);
+
+ this._forceChrome = false;
+
+ this.destroy = this.destroy.bind(this);
+
+ this.threadFront = null;
+
+ // This flag will be set to true from:
+ // - TabDescriptorFront getTarget(), for local tab targets
+ // - targetFromURL(), for local targets (about:debugging)
+ // - initToolbox(), for some test-only targets
+ this.shouldCloseClient = false;
+
+ this._client = client;
+
+ // Cache of already created targed-scoped fronts
+ // [typeName:string => Front instance]
+ this.fronts = new Map();
+
+ // `resource-available-form` events can be emitted by target actors before the
+ // ResourceWatcher could add event listeners. The target front will cache those
+ // events until the ResourceWatcher has added the listeners.
+ this._resourceCache = [];
+ this._onResourceAvailable = this._onResourceAvailable.bind(this);
+ // In order to avoid destroying the `_resourceCache`, we need to call `super.on()`
+ // instead of `this.on()`.
+ super.on("resource-available-form", this._onResourceAvailable);
+
+ this._addListeners();
+ }
+
+ on(eventName, listener) {
+ if (eventName === "resource-available-form" && this._resourceCache) {
+ this.off("resource-available-form", this._onResourceAvailable);
+ for (const cache of this._resourceCache) {
+ listener(cache);
+ }
+ this._resourceCache = null;
+ }
+
+ 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 the descriptor front for this target.
+ *
+ * TODO: Should be removed. This is misleading as only the top level target should have a descriptor.
+ * This will return null for targets created by the Watcher actor and will still be defined
+ * by targets created by RootActor methods (listSomething methods).
+ */
+ get descriptorFront() {
+ if (this.isDestroyed()) {
+ // If the target was already destroyed, parentFront will be null.
+ return null;
+ }
+
+ if (this.parentFront.typeName.endsWith("Descriptor")) {
+ return this.parentFront;
+ }
+ return null;
+ }
+
+ get targetType() {
+ return this._targetType;
+ }
+
+ get isTopLevel() {
+ return this._isTopLevel;
+ }
+
+ setTargetType(type) {
+ this._targetType = type;
+ }
+
+ setIsTopLevel(isTopLevel) {
+ this._isTopLevel = isTopLevel;
+ }
+
+ /**
+ * Get the top level WatcherFront for this target.
+ *
+ * The targets should all ultimately be managed by a unique Watcher actor,
+ * created from the unique Descriptor actor which is passed to the Toolbox.
+ * For now, the top level target is still created by the top level Descriptor,
+ * but it is also meant to be created by the Watcher.
+ *
+ * @return {TargetMixin} the parent target.
+ */
+ getWatcherFront() {
+ // All additional frame targets are spawn by the WatcherActor and are managed by it.
+ if (this.parentFront.typeName == "watcher") {
+ return this.parentFront;
+ }
+
+ // Otherwise, for top level targets, the parent front is a Descriptor, from which we can retrieve the Watcher.
+ // TODO: top level target should also be exposed by the Watcher actor, like any target.
+ if (
+ this.parentFront.typeName.endsWith("Descriptor") &&
+ this.parentFront.traits &&
+ this.parentFront.traits.watcher
+ ) {
+ return this.parentFront.getWatcher();
+ }
+
+ // For WebExtension, the descriptor doesn't expose a watcher yet (See Bug 1675456).
+ return null;
+ }
+
+ /**
+ * Get the immediate parent target for this target.
+ *
+ * @return {TargetMixin} the parent target.
+ */
+ async getParentTarget() {
+ // We now support frames watching via watchTargets for Tab and Process descriptors.
+ const watcherFront = await this.getWatcherFront();
+ if (watcherFront) {
+ // Safety check, in theory all watcher should support frames. We should be able
+ // to remove this as part of Bug 1680280.
+ if (watcherFront.traits.frame) {
+ // Retrieve the Watcher, which manage all the targets and should already have a reference to
+ // to the parent target.
+ return watcherFront.getParentBrowsingContextTarget(
+ this.browsingContextID
+ );
+ }
+ return null;
+ }
+
+ if (this.parentFront.getParentTarget) {
+ return this.parentFront.getParentTarget();
+ }
+
+ // Other targets, like WebExtensions, don't have a Watcher yet, nor do expose `getParentTarget`.
+ // We can't fetch parent target yet for these targets.
+ return null;
+ }
+
+ /**
+ * Get the target for the given Browsing Context ID.
+ *
+ * @return {TargetMixin} the requested target.
+ */
+ async getBrowsingContextTarget(browsingContextID) {
+ // Tab and Process Descriptors expose a Watcher, which is creating the
+ // targets and should be used to fetch any.
+ const watcherFront = await this.getWatcherFront();
+ if (watcherFront) {
+ // Safety check, in theory all watcher should support frames.
+ if (watcherFront.traits.frame) {
+ return watcherFront.getBrowsingContextTarget(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 getBrowsingContextTarget 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 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 isLocalTab() {
+ return !!this.descriptorFront?.isLocalTab;
+ }
+
+ get localTab() {
+ return this.descriptorFront?.localTab || null;
+ }
+
+ // Get a promise of the RootActor's form
+ get root() {
+ return this.client.mainRoot.rootForm;
+ }
+
+ // Get a Front for a target-scoped actor.
+ // i.e. an actor served by RootActor.listTabs or RootActorActor.getTab requests
+ async getFront(typeName) {
+ 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 we are debugging content document
+ // or if we are debugging chrome stuff.
+ // Allows to controls which features are available against
+ // a chrome or a content document.
+ get chrome() {
+ return (
+ this.isAddon ||
+ this.isContentProcess ||
+ this.isParentProcess ||
+ this.isWindowTarget ||
+ this._forceChrome
+ );
+ }
+
+ forceChrome() {
+ this._forceChrome = true;
+ }
+
+ // Tells us if the related actor implements BrowsingContextTargetActor
+ // interface and requires to call `attach` request before being used and
+ // `detach` during cleanup.
+ get isBrowsingContext() {
+ return this.typeName === "browsingContextTarget";
+ }
+
+ get name() {
+ if (this.isAddon || this.isContentProcess) {
+ return this.targetForm.name;
+ }
+ return this.title;
+ }
+
+ get title() {
+ return this._title || this.url;
+ }
+
+ get url() {
+ return this._url;
+ }
+
+ get isAddon() {
+ return this.isLegacyAddon || this.isWebExtension;
+ }
+
+ get isWorkerTarget() {
+ // XXX Remove the check on `workerDescriptor` as part of Bug 1667404.
+ return (
+ this.typeName === "workerTarget" || this.typeName === "workerDescriptor"
+ );
+ }
+
+ get isLegacyAddon() {
+ return !!(
+ this.targetForm &&
+ this.targetForm.actor &&
+ this.targetForm.actor.match(/conn\d+\.addon(Target)?\d+/)
+ );
+ }
+
+ 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+/)
+ );
+ }
+
+ get isWindowTarget() {
+ return !!(
+ this.targetForm &&
+ this.targetForm.actor &&
+ this.targetForm.actor.match(/conn\d+\.chromeWindowTarget\d+/)
+ );
+ }
+
+ get isMultiProcess() {
+ return !this.window;
+ }
+
+ 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;
+ }
+ }
+
+ // Attach the console actor
+ async attachConsole() {
+ const consoleFront = await this.getFront("console");
+
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return;
+ }
+
+ // Calling startListeners will populate the traits as it's the first request we
+ // make to the front.
+ await consoleFront.startListeners([]);
+
+ this._onInspectObject = packet => this.emit("inspect-object", packet);
+ this.removeOnInspectObjectListener = consoleFront.on(
+ "inspectObject",
+ this._onInspectObject
+ );
+ }
+
+ /**
+ * 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 {TargetList} targetList
+ * @returns {Promise} A promise that resolves once the thread is attached and resumed.
+ */
+ attachAndInitThread(targetList) {
+ if (this._onThreadInitialized) {
+ return this._onThreadInitialized;
+ }
+
+ this._onThreadInitialized = this._attachAndInitThread(targetList);
+ 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 {TargetList} targetList
+ * @returns {Promise} A promise that resolves once the thread is attached and resumed.
+ */
+ async _attachAndInitThread(targetList) {
+ // If the target is destroyed or soon will be, don't go further
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return;
+ }
+
+ // WorkerTargetFront don't have an attach function as the related console and thread
+ // actors are created right away (from devtools/server/startup/worker.js)
+ if (this.attach) {
+ await this.attach();
+ }
+
+ const isBrowserToolbox = targetList.targetFront.isParentProcess;
+ const isNonTopLevelFrameTarget =
+ !this.isTopLevel && this.targetType === targetList.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;
+ }
+
+ const options = await getThreadOptions();
+ // If the target is destroyed or soon will be, don't go further
+ if (this.isDestroyedOrBeingDestroyed()) {
+ return;
+ }
+ const threadFront = await this.attachThread(options);
+
+ // @backward-compat { version 86 } ThreadActor.attach no longer pause the thread,
+ // so that we no longer have to resume.
+ // Once 86 is in release, we can remove the rest of this method.
+ if (this.getTrait("noPauseOnThreadActorAttach")) {
+ return;
+ }
+ try {
+ if (this.isDestroyedOrBeingDestroyed() || threadFront.isDestroyed()) {
+ return;
+ }
+ await threadFront.resume();
+ } catch (ex) {
+ if (ex.error === "wrongOrder") {
+ targetList.emit("target-thread-wrong-order-on-resume");
+ } else {
+ throw ex;
+ }
+ }
+ }
+
+ 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");
+ if (
+ this.isDestroyedOrBeingDestroyed() ||
+ this.threadFront.isDestroyed()
+ ) {
+ return this.threadFront;
+ }
+
+ await this.threadFront.attach(options);
+
+ return this.threadFront;
+ }
+
+ /**
+ * Setup listeners.
+ */
+ _addListeners() {
+ this.client.on("closed", this.destroy);
+
+ // `tabDetached` is sent by all target targets types: frame, process and workers.
+ // This is sent when the target is destroyed:
+ // * the target context destroys itself (the tab closes for ex, or the worker shuts down)
+ // in this case, it may be the connector that send this event in the name of the target actor
+ // * the target actor is destroyed, but the target context stays up and running (for ex, when we call Watcher.unwatchTargets)
+ // * the DevToolsServerConnection closes (client closes the connection)
+ this.on("tabDetached", this.destroy);
+ }
+
+ /**
+ * Teardown listeners.
+ */
+ _removeListeners() {
+ // Remove listeners set in _addListeners
+ if (this.client) {
+ this.client.off("closed", this.destroy);
+ }
+ this.off("tabDetached", this.destroy);
+
+ // Remove listeners set in attachConsole
+ if (this.removeOnInspectObjectListener) {
+ this.removeOnInspectObjectListener();
+ this.removeOnInspectObjectListener = null;
+ }
+ }
+
+ 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() {
+ // Before taking any action, notify listeners that destruction is imminent.
+ this.emit("close");
+
+ // 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._removeListeners();
+
+ this.threadFront = null;
+
+ if (this.shouldCloseClient) {
+ try {
+ await this._client.close();
+ } catch (e) {
+ // Ignore any errors while closing, since there is not much that can be done
+ // at this point.
+ console.warn("Error while closing client:", e);
+ }
+
+ // 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.
+ } else 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);
+ }
+ }
+
+ // 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");
+
+ // 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;
+
+ // All target front subclasses set this variable in their `attach` method.
+ // None of them overload destroy, so clean this up from here.
+ this._attach = null;
+
+ this._title = null;
+ this._url = null;
+ }
+
+ _onResourceAvailable(resources) {
+ if (this._resourceCache) {
+ this._resourceCache.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.traits.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.traits.logInPage) {
+ const warningFlag = 1;
+ return this.logInPage({ text, category, flags: warningFlag });
+ }
+ return Promise.resolve();
+ }
+ }
+ return Target;
+}
+exports.TargetMixin = TargetMixin;