1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 // We are requiring a module from client whereas this module is from shared.
8 // This shouldn't happen, but Fronts should rather be part of client anyway.
9 // Otherwise gDevTools is only used for local tabs and should propably only
10 // used by a subclass, specific to local tabs.
11 loader.lazyRequireGetter(
14 "devtools/client/framework/devtools",
17 loader.lazyRequireGetter(
20 "devtools/client/framework/target",
23 loader.lazyRequireGetter(
26 "devtools/shared/client/deprecated-thread-client"
28 loader.lazyRequireGetter(this, "getFront", "devtools/shared/protocol", true);
31 * A Target represents a debuggable context. It can be a browser tab, a tab on
32 * a remote device, like a tab on Firefox for Android. But it can also be an add-on,
33 * as well as firefox parent process, or just one of its content process.
34 * A Target is related to a given TargetActor, for which we derive this class.
36 * Providing a generalized abstraction of a web-page or web-browser (available
37 * either locally or remotely) is beyond the scope of this class (and maybe
38 * also beyond the scope of this universe) However Target does attempt to
39 * abstract some common events and read-only properties common to many Tools.
41 * Supported read-only properties:
44 * Target extends EventEmitter and provides support for the following events:
45 * - close: The target window has been closed. All tools attached to this
46 * target should close. This event is not currently cancelable.
48 * Optional events only dispatched by BrowsingContextTarget:
49 * - will-navigate: The target window will navigate to a different URL
50 * - navigate: The target window has navigated to a different URL
52 function TargetMixin(parentClass) {
53 class Target extends parentClass {
54 constructor(client, form) {
57 this._forceChrome = false;
59 this.destroy = this.destroy.bind(this);
60 this._onNewSource = this._onNewSource.bind(this);
62 this.activeConsole = null;
63 this.threadFront = null;
65 this._client = client;
67 // Cache of already created targed-scoped fronts
68 // [typeName:string => Front instance]
69 this.fronts = new Map();
70 // Temporary fix for bug #1493131 - inspector has a different life cycle
71 // than most other fronts because it is closely related to the toolbox.
72 // TODO: remove once inspector is separated from the toolbox
73 this._inspector = null;
75 this._setupRemoteListeners();
79 // When debugging local tabs, we also have a reference to the Firefox tab
81 // * distinguish local tabs from remote (see target.isLocalTab)
82 // * being able to hookup into Firefox UI (see Hosts)
84 this._setupListeners();
88 * Returns a promise for the protocol description from the root actor. Used
89 * internally with `target.actorHasMethod`. Takes advantage of caching if
90 * definition was fetched previously with the corresponding actor information.
91 * Actors are lazily loaded, so not only must the tool using a specific actor
92 * be in use, the actors are only registered after invoking a method (for
93 * performance reasons, added in bug 988237), so to use these actor detection
94 * methods, one must already be communicating with a specific actor of that
99 * "category": "actor",
100 * "typeName": "longstractor",
102 * "name": "substring",
104 * "type": "substring",
107 * "type": "primitive"
111 * "type": "primitive"
116 * "_retval": "primitive"
123 async getActorDescription(actorName) {
125 this._protocolDescription &&
126 this._protocolDescription.types[actorName]
128 return this._protocolDescription.types[actorName];
130 const description = await this.client.mainRoot.protocolDescription();
131 this._protocolDescription = description;
132 return description.types[actorName];
136 * Returns a boolean indicating whether or not the specific actor
139 * @param {String} actorName
142 hasActor(actorName) {
143 if (this.targetForm) {
144 return !!this.targetForm[actorName + "Actor"];
150 * Queries the protocol description to see if an actor has
151 * an available method. The actor must already be lazily-loaded (read
152 * the restrictions in the `getActorDescription` comments),
153 * so this is for use inside of tool. Returns a promise that
154 * resolves to a boolean.
156 * @param {String} actorName
157 * @param {String} methodName
160 actorHasMethod(actorName, methodName) {
161 return this.getActorDescription(actorName).then(desc => {
162 if (desc && desc.methods) {
163 return !!desc.methods.find(method => method.name === methodName);
170 * Returns a trait from the root actor.
172 * @param {String} traitName
175 getTrait(traitName) {
176 // If the targeted actor exposes traits and has a defined value for this
177 // traits, override the root actor traits
178 if (this.targetForm.traits && traitName in this.targetForm.traits) {
179 return this.targetForm.traits[traitName];
182 return this.client.traits[traitName];
189 // Get a promise of the RootActor's form
191 return this.client.mainRoot.rootForm;
194 // Temporary fix for bug #1493131 - inspector has a different life cycle
195 // than most other fronts because it is closely related to the toolbox.
196 // TODO: remove once inspector is separated from the toolbox
197 async getInspector() {
198 // the front might have been destroyed and no longer have an actor ID
199 if (this._inspector && this._inspector.actorID) {
200 return this._inspector;
202 this._inspector = await getFront(
208 this.emit("inspector", this._inspector);
209 return this._inspector;
212 // Run callback on every front of this type that currently exists, and on every
213 // instantiation of front type in the future.
214 onFront(typeName, callback) {
215 const front = this.fronts.get(typeName);
217 return callback(front);
219 return this.on(typeName, callback);
222 // Get a Front for a target-scoped actor.
223 // i.e. an actor served by RootActor.listTabs or RootActorActor.getTab requests
224 async getFront(typeName) {
225 let front = this.fronts.get(typeName);
226 // the front might have been destroyed and no longer have an actor ID
228 (front && front.actorID) ||
229 (front && typeof front.then === "function")
233 front = getFront(this.client, typeName, this.targetForm, this);
234 this.fronts.set(typeName, front);
235 // replace the placeholder with the instance of the front once it has loaded
237 this.emit(typeName, front);
238 this.fronts.set(typeName, front);
242 getCachedFront(typeName) {
243 // do not wait for async fronts;
244 const front = this.fronts.get(typeName);
245 // ensure that the front is a front, and not async front
246 if (front && front.actorID) {
256 // Tells us if we are debugging content document
257 // or if we are debugging chrome stuff.
258 // Allows to controls which features are available against
259 // a chrome or a content document.
263 this.isContentProcess ||
264 this.isParentProcess ||
265 this.isWindowTarget ||
271 this._forceChrome = true;
274 // Tells us if the related actor implements BrowsingContextTargetActor
275 // interface and requires to call `attach` request before being used and
276 // `detach` during cleanup.
277 get isBrowsingContext() {
278 return this.typeName === "browsingContextTarget";
283 return this.targetForm.name;
289 return this._title || this.url;
297 return this.isLegacyAddon || this.isWebExtension;
300 get isWorkerTarget() {
301 return this.typeName === "workerTarget";
304 get isLegacyAddon() {
307 this.targetForm.actor &&
308 this.targetForm.actor.match(/conn\d+\.addon(Target)?\d+/)
312 get isWebExtension() {
315 this.targetForm.actor &&
316 (this.targetForm.actor.match(/conn\d+\.webExtension(Target)?\d+/) ||
317 this.targetForm.actor.match(/child\d+\/webExtension(Target)?\d+/))
321 get isContentProcess() {
322 // browser content toolbox's form will be of the form:
323 // server0.conn0.content-process0/contentProcessTarget7
324 // while xpcshell debugging will be:
325 // server1.conn0.contentProcessTarget7
328 this.targetForm.actor &&
329 this.targetForm.actor.match(
330 /conn\d+\.(content-process\d+\/)?contentProcessTarget\d+/
335 get isParentProcess() {
338 this.targetForm.actor &&
339 this.targetForm.actor.match(/conn\d+\.parentProcessTarget\d+/)
343 get isWindowTarget() {
346 this.targetForm.actor &&
347 this.targetForm.actor.match(/conn\d+\.chromeWindowTarget\d+/)
355 get isMultiProcess() {
360 return this.traits.canRewind;
364 return this.canRewind && this.isLocalTab;
367 getExtensionPathName(url) {
368 // Return the url if the target is not a webextension.
369 if (!this.isWebExtension) {
370 throw new Error("Target is not a WebExtension");
374 const parsedURL = new URL(url);
375 // Only moz-extension URL should be shortened into the URL pathname.
376 if (parsedURL.protocol !== "moz-extension:") {
379 return parsedURL.pathname;
381 // Return the url if unable to resolve the pathname.
387 * For local tabs, returns the tab's contentPrincipal, which can be used as a
388 * `triggeringPrincipal` when opening links. However, this is a hack as it is not
389 * correct for subdocuments and it won't work for remote debugging. Bug 1467945 hopes
390 * to devise a better approach.
392 get contentPrincipal() {
393 if (!this.isLocalTab) {
396 return this.tab.linkedBrowser.contentPrincipal;
400 * Similar to the above get contentPrincipal(), the get csp()
401 * returns the CSP which should be used for opening links.
404 if (!this.isLocalTab) {
407 return this.tab.linkedBrowser.csp;
410 // Attach the console actor
411 async attachConsole() {
412 this.activeConsole = await this.getFront("console");
413 await this.activeConsole.startListeners([]);
415 this._onInspectObject = packet => this.emit("inspect-object", packet);
416 this.activeConsole.on("inspectObject", this._onInspectObject);
420 * Attach to thread actor.
422 * This depends on having the sub-class to set the thread actor ID in `_threadActor`.
424 * @param object options
425 * Configuration options.
427 async attachThread(options = {}) {
428 if (!this._threadActor) {
430 "TargetMixin sub class should set _threadActor before calling " +
434 if (this.getTrait("hasThreadFront")) {
435 this.threadFront = await this.getFront("thread");
437 // Backwards compat for Firefox 68
438 // mimics behavior of a front
439 this.threadFront = new ThreadClient(this._client, this._threadActor);
440 this.fronts.set("thread", this.threadFront);
441 this.threadFront.actorID = this._threadActor;
442 this.manage(this.threadFront);
444 const result = await this.threadFront.attach(options);
446 this.threadFront.on("newSource", this._onNewSource);
448 return [result, this.threadFront];
451 // Listener for "newSource" event fired by the thread actor
452 _onNewSource(packet) {
453 this.emit("source-updated", packet);
457 * Listen to the different events.
460 this.tab.addEventListener("TabClose", this);
461 this.tab.ownerDocument.defaultView.addEventListener("unload", this);
462 this.tab.addEventListener("TabRemotenessChange", this);
466 * Teardown event listeners.
468 _teardownListeners() {
469 if (this._tab.ownerDocument.defaultView) {
470 this._tab.ownerDocument.defaultView.removeEventListener("unload", this);
472 this._tab.removeEventListener("TabClose", this);
473 this._tab.removeEventListener("TabRemotenessChange", this);
477 * Setup listeners for remote debugging, updating existing ones as necessary.
479 _setupRemoteListeners() {
480 this.client.on("closed", this.destroy);
482 this.on("tabDetached", this.destroy);
486 * Teardown listeners for remote debugging.
488 _teardownRemoteListeners() {
489 // Remove listeners set in _setupRemoteListeners
491 this.client.off("closed", this.destroy);
493 this.off("tabDetached", this.destroy);
495 // Remove listeners set in attachThread
496 if (this.threadFront) {
497 this.threadFront.off("newSource", this._onNewSource);
500 // Remove listeners set in attachConsole
501 if (this.activeConsole && this._onInspectObject) {
502 this.activeConsole.off("inspectObject", this._onInspectObject);
507 * Handle tabs events.
510 switch (event.type) {
515 case "TabRemotenessChange":
516 this.onRemotenessChange();
522 * Automatically respawn the toolbox when the tab changes between being
523 * loaded within the parent process and loaded from a content process.
524 * Process change can go in both ways.
526 onRemotenessChange() {
527 // Responsive design do a crazy dance around tabs and triggers
528 // remotenesschange events. But we should ignore them as at the end
529 // the content doesn't change its remoteness.
530 if (this._tab.isResponsiveDesignMode) {
534 // Save a reference to the tab as it will be nullified on destroy
535 const tab = this._tab;
536 const onToolboxDestroyed = async target => {
537 if (target != this) {
540 gDevTools.off("toolbox-destroyed", target);
542 // Recreate a fresh target instance as the current one is now destroyed
543 const newTarget = await TargetFactory.forTab(tab);
544 gDevTools.showToolbox(newTarget);
546 gDevTools.on("toolbox-destroyed", onToolboxDestroyed);
550 * Target is not alive anymore.
553 // If several things call destroy then we give them all the same
554 // destruction promise so we're sure to destroy only once
555 if (this._destroyer) {
556 return this._destroyer;
559 this._destroyer = (async () => {
560 // Before taking any action, notify listeners that destruction is imminent.
563 for (let [, front] of this.fronts) {
565 await front.destroy();
569 this._teardownListeners();
572 this._teardownRemoteListeners();
574 this.threadFront = null;
576 if (this.isLocalTab) {
577 // We started with a local tab and created the client ourselves, so we
578 // should close it. Ignore any errors while closing, since there is
579 // not much that can be done at this point.
581 await this._client.close();
583 console.warn(`Error while closing client: ${e.message}`);
586 // Not all targets supports attach/detach. For example content process doesn't.
587 // Also ensure that the front is still active before trying to do the request.
588 } else if (this.detach && this.actorID) {
589 // The client was handed to us, so we are not responsible for closing
590 // it. We just need to detach from the tab, if already attached.
591 // |detach| may fail if the connection is already dead, so proceed with
592 // cleanup directly after this.
596 console.warn(`Error while detaching target: ${e.message}`);
600 // Do that very last in order to let a chance to dispatch `detach` requests.
606 return this._destroyer;
610 * Clean up references to what this target points to.
613 this.activeConsole = null;
614 this.threadFront = null;
618 // All target front subclasses set this variable in their `attach` method.
619 // None of them overload destroy, so clean this up from here.
629 : this.targetForm && this.targetForm.actor;
630 return `Target:${id}`;
634 * Log an error of some kind to the tab's console.
636 * @param {String} text
638 * @param {String} category
639 * The category of the message. @see nsIScriptError.
642 logErrorInPage(text, category) {
643 if (this.traits.logInPage) {
645 return this.logInPage({ text, category, flags: errorFlag });
647 return Promise.resolve();
651 * Log a warning of some kind to the tab's console.
653 * @param {String} text
655 * @param {String} category
656 * The category of the message. @see nsIScriptError.
659 logWarningInPage(text, category) {
660 if (this.traits.logInPage) {
661 const warningFlag = 1;
662 return this.logInPage({ text, category, flags: warningFlag });
664 return Promise.resolve();
669 exports.TargetMixin = TargetMixin;