Bug 1465635 - Merge all target fronts with Target class. r=yulia,jdescottes

This patch makes it so that all target fronts inherits from a Target class mixin.
We are using a mixin as fronts should inherit from a custom Front class,
which is augmented with its own RDP request and events defined in its spec.
(This is done via FrontClassWithSpec(spec))

Depends on D15830

Differential Revision: https://phabricator.services.mozilla.com/D15831

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Alexandre Poirot 2019-02-02 11:24:32 +00:00
parent cacb7734d7
commit dddc73fb49
14 changed files with 719 additions and 782 deletions

View File

@ -102,7 +102,7 @@ global.getToolboxEvalOptions = function(context) {
}
// Provide the console actor ID to implement the "inspect" binding.
options.toolboxConsoleActorID = toolbox.target.form.consoleActor;
options.toolboxConsoleActorID = toolbox.target.activeConsole.actor;
return options;
};

View File

@ -4,23 +4,16 @@
"use strict";
const EventEmitter = require("devtools/shared/event-emitter");
const Services = require("Services");
loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
loader.lazyRequireGetter(this, "DebuggerClient",
"devtools/shared/client/debugger-client", true);
loader.lazyRequireGetter(this, "gDevTools",
"devtools/client/framework/devtools", true);
loader.lazyRequireGetter(this, "getFront", "devtools/shared/protocol", true);
const targets = new WeakMap();
const promiseTargets = new WeakMap();
/**
* Functions for creating Targets
*/
const TargetFactory = exports.TargetFactory = {
exports.TargetFactory = {
/**
* Construct a Target. The target will be cached for each Tab so that we create only
@ -42,6 +35,10 @@ const TargetFactory = exports.TargetFactory = {
target = await promise;
// Then replace the promise with the target object
targets.set(tab, target);
target.attachTab(tab);
target.once("close", () => {
targets.delete(tab);
});
return target;
},
@ -87,15 +84,7 @@ const TargetFactory = exports.TargetFactory = {
await client.connect();
// Fetch the FrameTargetActor's Front
const front = await client.mainRoot.getTab({ tab });
return new Target({
client,
activeTab: front,
// A local Target will never perform chrome debugging.
chrome: false,
tab,
});
return client.mainRoot.getTab({ tab });
},
/**
@ -111,30 +100,20 @@ const TargetFactory = exports.TargetFactory = {
*
* @return A promise of a target object
*/
forRemoteTab: function(options) {
let targetPromise = promiseTargets.get(options);
if (targetPromise == null) {
const target = new Target(options);
targetPromise = target.attach().then(() => target);
targetPromise.catch(e => {
console.error("Exception while attaching target", e);
});
promiseTargets.set(options, targetPromise);
forRemoteTab: function({ activeTab, client, chrome }) {
const target = activeTab;
if (chrome) {
target.forceChrome();
}
const targetPromise = target.attach().then(() => target);
targetPromise.catch(e => {
console.error("Exception while attaching target", e);
});
return targetPromise;
},
forWorker: function(workerTargetFront) {
let target = targets.get(workerTargetFront);
if (target == null) {
target = new Target({
client: workerTargetFront.client,
activeTab: workerTargetFront,
chrome: false,
});
targets.set(workerTargetFront, target);
}
return target;
return workerTargetFront;
},
/**
@ -148,683 +127,3 @@ const TargetFactory = exports.TargetFactory = {
return targets.has(tab);
},
};
/**
* A Target represents something that we can debug. Targets are generally
* read-only. Any changes that you wish to make to a target should be done via
* a Tool that attaches to the target. i.e. a Target is just a pointer saying
* "the thing to debug is over there".
*
* 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.
* - navigate: The target window has navigated to a different URL
*
* Optional events:
* - will-navigate: The target window will navigate to a different URL
* - hidden: The target is not visible anymore (for TargetTab, another tab is
* selected)
* - visible: The target is visible (for TargetTab, tab is selected)
*
* Comparing Targets: 2 instances of a Target object can point at the same
* thing, so t1 !== t2 and t1 != t2 even when they represent the same object.
* To compare to targets use 't1.equals(t2)'.
*/
/**
* 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 pass the form as
* argument.
*
* For now, only workers are having a distinct Target class called WorkerTarget.
*
* @param {Front} activeTab
* If we already have a front for this target, pass it here.
* @param {DebuggerClient} client
* The DebuggerClient instance to be used to debug this target.
* @param {Boolean} chrome
* True, if we allow to see privileged resources like JSM, xpcom,
* frame scripts...
* @param {xul:tab} tab (optional)
* If the target is a local Firefox tab, a reference to the firefox
* frontend tab object.
*/
class Target extends EventEmitter {
constructor({ client, chrome, activeTab, tab = null }) {
if (!activeTab) {
throw new Error("Cannot instanciate target without a non-null activeTab");
}
super();
this.destroy = this.destroy.bind(this);
this._onTabNavigated = this._onTabNavigated.bind(this);
this.activeConsole = null;
this.activeTab = activeTab;
this._url = this.form.url;
this._title = this.form.title;
this._client = client;
this._chrome = chrome;
// When debugging local tabs, we also have a reference to the Firefox tab
// This is used to:
// * distinguish local tabs from remote (see target.isLocalTab)
// * being able to hookup into Firefox UI (see Hosts)
if (tab) {
this._tab = tab;
this._setupListeners();
}
// isBrowsingContext is true for all target connected to an actor that inherits from
// BrowsingContextTargetActor. It happens to be the case for almost all targets but:
// * legacy add-ons (old bootstrapped add-ons)
// * content process (browser content toolbox)
// * xpcshell debugging (it uses ParentProcessTargetActor, which inherits from
// BrowsingContextActor, but doesn't have any valid browsing
// context to attach to.)
// Starting with FF64, BrowsingContextTargetActor exposes a traits to help identify
// the target actors inheriting from it. It also help identify the xpcshell debugging
// target actor that doesn't have any valid browsing context.
// (Once FF63 is no longer supported, we can remove the `else` branch and only look
// for the traits)
if (this.form.traits && ("isBrowsingContext" in this.form.traits)) {
this._isBrowsingContext = this.form.traits.isBrowsingContext;
} else {
this._isBrowsingContext = !this.isLegacyAddon && !this.isContentProcess && !this.isWorkerTarget;
}
// Cache of already created targed-scoped fronts
// [typeName:string => Front instance]
this.fronts = new Map();
// Temporary fix for bug #1493131 - inspector has a different life cycle
// than most other fronts because it is closely related to the toolbox.
// TODO: remove once inspector is separated from the toolbox
this._inspector = null;
}
/**
* Returns a promise for the protocol description from the root actor. Used
* internally with `target.actorHasMethod`. Takes advantage of caching if
* definition was fetched previously with the corresponding actor information.
* Actors are lazily loaded, so not only must the tool using a specific actor
* be in use, the actors are only registered after invoking a method (for
* performance reasons, added in bug 988237), so to use these actor detection
* methods, one must already be communicating with a specific actor of that
* type.
*
* @return {Promise}
* {
* "category": "actor",
* "typeName": "longstractor",
* "methods": [{
* "name": "substring",
* "request": {
* "type": "substring",
* "start": {
* "_arg": 0,
* "type": "primitive"
* },
* "end": {
* "_arg": 1,
* "type": "primitive"
* }
* },
* "response": {
* "substring": {
* "_retval": "primitive"
* }
* }
* }],
* "events": {}
* }
*/
async getActorDescription(actorName) {
if (this._protocolDescription &&
this._protocolDescription.types[actorName]) {
return this._protocolDescription.types[actorName];
}
const description = await this.client.mainRoot.protocolDescription();
this._protocolDescription = description;
return description.types[actorName];
}
/**
* Returns a boolean indicating whether or not the specific actor
* type exists.
*
* @param {String} actorName
* @return {Boolean}
*/
hasActor(actorName) {
if (this.form) {
return !!this.form[actorName + "Actor"];
}
return false;
}
/**
* Queries the protocol description to see if an actor has
* an available method. The actor must already be lazily-loaded (read
* the restrictions in the `getActorDescription` comments),
* so this is for use inside of tool. Returns a promise that
* resolves to a boolean.
*
* @param {String} actorName
* @param {String} methodName
* @return {Promise}
*/
actorHasMethod(actorName, methodName) {
return this.getActorDescription(actorName).then(desc => {
if (desc && desc.methods) {
return !!desc.methods.find(method => method.name === methodName);
}
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.form.traits && traitName in this.form.traits) {
return this.form.traits[traitName];
}
return this.client.traits[traitName];
}
get tab() {
return this._tab;
}
get form() {
return this.activeTab.targetForm;
}
// Get a promise of the RootActor's form
get root() {
return this.client.mainRoot.rootForm;
}
// Temporary fix for bug #1493131 - inspector has a different life cycle
// than most other fronts because it is closely related to the toolbox.
// TODO: remove once inspector is separated from the toolbox
async getInspector(typeName) {
// the front might have been destroyed and no longer have an actor ID
if (this._inspector && this._inspector.actorID) {
return this._inspector;
}
this._inspector = await getFront(this.client, "inspector", this.form);
this.emit("inspector", this._inspector);
return this._inspector;
}
// Run callback on every front of this type that currently exists, and on every
// instantiation of front type in the future.
onFront(typeName, callback) {
const front = this.fronts.get(typeName);
if (front) {
return callback(front);
}
return this.on(typeName, callback);
}
// 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);
// the front might have been destroyed and no longer have an actor ID
if (front && front.actorID || front && typeof front.then === "function") {
return front;
}
front = getFront(this.client, typeName, this.form);
this.fronts.set(typeName, front);
// replace the placeholder with the instance of the front once it has loaded
front = await front;
this.emit(typeName, 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 && 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._chrome;
}
// 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._isBrowsingContext;
}
get name() {
if (this.isAddon) {
return this.form.name;
}
return this.title;
}
get url() {
return this._url;
}
get title() {
return this._title || this.url;
}
get isAddon() {
return this.isLegacyAddon || this.isWebExtension;
}
get isWorkerTarget() {
return this.activeTab.typeName === "workerTarget";
}
get isLegacyAddon() {
return !!(this.form && this.form.actor &&
this.form.actor.match(/conn\d+\.addon(Target)?\d+/));
}
get isWebExtension() {
return !!(this.form && this.form.actor && (
this.form.actor.match(/conn\d+\.webExtension(Target)?\d+/) ||
this.form.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.form && this.form.actor &&
this.form.actor.match(/conn\d+\.(content-process\d+\/)?contentProcessTarget\d+/));
}
get isLocalTab() {
return !!this._tab;
}
get isMultiProcess() {
return !this.window;
}
get canRewind() {
return this.activeTab.traits.canRewind;
}
isReplayEnabled() {
return Services.prefs.getBoolPref("devtools.recordreplay.mvp.enabled")
&& this.canRewind
&& this.isLocalTab;
}
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;
}
}
/**
* For local tabs, returns the tab's contentPrincipal, which can be used as a
* `triggeringPrincipal` when opening links. However, this is a hack as it is not
* correct for subdocuments and it won't work for remote debugging. Bug 1467945 hopes
* to devise a better approach.
*/
get contentPrincipal() {
if (!this.isLocalTab) {
return null;
}
return this.tab.linkedBrowser.contentPrincipal;
}
/**
* Attach the target and its console actor.
*
* This method will mainly call `attach` request on the target actor as well
* as the console actor.
* See DebuggerClient.attachTarget and DebuggerClient.attachConsole for more info.
* It also starts listenings to events the target actor will start emitting
* after being attached, like `tabDetached` and `frameUpdate`
*/
attach() {
if (this._attach) {
return this._attach;
}
// Attach the target actor
const attachBrowsingContextTarget = async () => {
await this.activeTab.attach();
this.activeTab.on("tabNavigated", this._onTabNavigated);
this._onFrameUpdate = packet => {
this.emit("frame-update", packet);
};
this.activeTab.on("frameUpdate", this._onFrameUpdate);
};
// Attach the console actor
const attachConsole = async () => {
const [, consoleClient] = await this._client.attachConsole(
this.form.consoleActor, []);
this.activeConsole = consoleClient;
this._onInspectObject = packet => this.emit("inspect-object", packet);
this.activeConsole.on("inspectObject", this._onInspectObject);
};
this._attach = (async () => {
// AddonTargetActor and ContentProcessTargetActor don't inherit from
// BrowsingContextTargetActor (i.e. this.isBrowsingContext=false) and don't need
// to be attached via DebuggerClient.attachTarget.
if (this.isBrowsingContext) {
await attachBrowsingContextTarget();
// Addon Worker and Content process targets are the first targets to have their
// front already instantiated. The plan is to have all targets to have their front
// passed as constructor argument.
} else if (this.isWorkerTarget || this.isLegacyAddon) {
// Worker is the first front to be completely migrated to have only its attach
// method being called from Target.attach. Other fronts should be refactored.
await this.activeTab.attach();
} else if (this.isContentProcess) {
// ContentProcessTarget is the only one target without any attach request.
} else {
throw new Error(`Unsupported type of target. Expected target of one of the` +
` following types: BrowsingContext, ContentProcess, Worker or ` +
`Addon (legacy).`);
}
// _setupRemoteListeners has to be called after the potential call to `attachTarget`
// as it depends on `activeTab` which is set by this method.
this._setupRemoteListeners();
// But all target actor have a console actor to attach
return attachConsole();
})();
return this._attach;
}
/**
* Listen to the different events.
*/
_setupListeners() {
this.tab.addEventListener("TabClose", this);
this.tab.ownerDocument.defaultView.addEventListener("unload", this);
this.tab.addEventListener("TabRemotenessChange", this);
}
/**
* Teardown event listeners.
*/
_teardownListeners() {
if (this._tab.ownerDocument.defaultView) {
this._tab.ownerDocument.defaultView.removeEventListener("unload", this);
}
this._tab.removeEventListener("TabClose", this);
this._tab.removeEventListener("TabRemotenessChange", this);
}
/**
* Event listener for tabNavigated packet sent by activeTab's front.
*/
_onTabNavigated(packet) {
const event = Object.create(null);
event.url = packet.url;
event.title = packet.title;
event.nativeConsoleAPI = packet.nativeConsoleAPI;
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._url = packet.url;
this._title = packet.title;
}
// 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);
}
}
/**
* Setup listeners for remote debugging, updating existing ones as necessary.
*/
_setupRemoteListeners() {
this.client.addListener("closed", this.destroy);
this.activeTab.on("tabDetached", this.destroy);
// These events should be ultimately listened from the thread client as
// they are coming from it and no longer go through the Target Actor/Front.
this._onSourceUpdated = packet => this.emit("source-updated", packet);
this.activeTab.on("newSource", this._onSourceUpdated);
this.activeTab.on("updatedSource", this._onSourceUpdated);
}
/**
* Teardown listeners for remote debugging.
*/
_teardownRemoteListeners() {
// Remove listeners set in _setupRemoteListeners
this.client.removeListener("closed", this.destroy);
this.activeTab.off("tabDetached", this.destroy);
this.activeTab.off("newSource", this._onSourceUpdated);
this.activeTab.off("updatedSource", this._onSourceUpdated);
// Remove listeners set in attachTarget
if (this.isBrowsingContext) {
this.activeTab.off("tabNavigated", this._onTabNavigated);
this.activeTab.off("frameUpdate", this._onFrameUpdate);
}
// Remove listeners set in attachConsole
if (this.activeConsole && this._onInspectObject) {
this.activeConsole.off("inspectObject", this._onInspectObject);
}
}
/**
* Handle tabs events.
*/
handleEvent(event) {
switch (event.type) {
case "TabClose":
case "unload":
this.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.
*/
onRemotenessChange() {
// Responsive design do a crazy dance around tabs and triggers
// remotenesschange events. But we should ignore them as at the end
// the content doesn't change its remoteness.
if (this._tab.isResponsiveDesignMode) {
return;
}
// Save a reference to the tab as it will be nullified on destroy
const tab = this._tab;
const onToolboxDestroyed = async (target) => {
if (target != this) {
return;
}
gDevTools.off("toolbox-destroyed", target);
// Recreate a fresh target instance as the current one is now destroyed
const newTarget = await TargetFactory.forTab(tab);
gDevTools.showToolbox(newTarget);
};
gDevTools.on("toolbox-destroyed", onToolboxDestroyed);
}
/**
* 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._destroyer = (async () => {
// Before taking any action, notify listeners that destruction is imminent.
this.emit("close");
for (let [, front] of this.fronts) {
front = await front;
await front.destroy();
}
if (this._tab) {
this._teardownListeners();
}
this._teardownRemoteListeners();
if (this.isLocalTab) {
// We started with a local tab and created the client ourselves, so we
// should close it.
await this._client.close();
} else {
// 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.activeTab.detach();
} catch (e) {
console.warn(`Error while detaching target: ${e.message}`);
}
}
this._cleanup();
})();
return this._destroyer;
}
/**
* Clean up references to what this target points to.
*/
_cleanup() {
if (this._tab) {
targets.delete(this._tab);
} else {
promiseTargets.delete(this.form);
}
this.activeTab = null;
this.activeConsole = null;
this._client = null;
this._tab = null;
this._attach = null;
this._title = null;
this._url = null;
}
toString() {
const id = this._tab ? this._tab : (this.form && this.form.actor);
return `Target:${id}`;
}
/**
* 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.activeTab.traits.logInPage) {
const errorFlag = 0;
return this.activeTab.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.activeTab.traits.logInPage) {
const warningFlag = 1;
return this.activeTab.logInPage({ text, category, flags: warningFlag });
}
return Promise.resolve();
}
}
exports.Target = Target;

View File

@ -30,10 +30,10 @@ add_task(async () => {
await checkGetTab(client, tab1, tab2, targetFront1, targetFront2);
await checkGetTabFailures(client);
await checkSelectedTargetActor(client, targetFront2);
await checkSelectedTargetActor(targetFront2);
await removeTab(tab2);
await checkFirstTargetActor(client, targetFront1);
await checkFirstTargetActor(targetFront1);
await removeTab(tab1);
await client.close();
@ -76,22 +76,16 @@ async function checkGetTabFailures(client) {
}
}
async function checkSelectedTargetActor(client, targetFront2) {
async function checkSelectedTargetActor(targetFront2) {
// Send a naive request to the second target actor to check if it works
const response = await client.request({
to: targetFront2.targetForm.consoleActor,
type: "startListeners",
listeners: [],
});
await targetFront2.attach();
const response = await targetFront2.activeConsole.startListeners([]);
ok("startedListeners" in response, "Actor from the selected tab should respond to the request.");
}
async function checkFirstTargetActor(client, targetFront1) {
async function checkFirstTargetActor(targetFront1) {
// then send a request to the first target actor to check if it still works
const response = await client.request({
to: targetFront1.targetForm.consoleActor,
type: "startListeners",
listeners: [],
});
await targetFront1.attach();
const response = await targetFront1.activeConsole.startListeners([]);
ok("startedListeners" in response, "Actor from the first tab should still respond.");
}

View File

@ -159,7 +159,7 @@ add_task(async function testSidebarDOMNodeHighlighting() {
const evalResult = await inspectedWindowFront.eval(fakeExtCallerInfo, expression, {
evalResultAsGrip: true,
toolboxConsoleActorID: toolbox.target.form.consoleActor,
toolboxConsoleActorID: toolbox.target.activeConsole.actor,
});
sidebar.setObjectValueGrip(evalResult.valueGrip);

View File

@ -29,11 +29,10 @@ add_task(async function test() {
const tabFront = tabs.filter(a => a.url == TAB1_URL).pop();
ok(tabFront, "Should have an actor for the tab");
let response = await tabFront.attach();
is(response.type, "tabAttached", "Should have attached");
await tabFront.attach();
const previousActorID = tabFront.actorID;
response = await tabFront.detach();
let response = await tabFront.detach();
is(response.type, "detached", "Should have detached");
tabs = await client.mainRoot.listTabs();
@ -41,8 +40,7 @@ add_task(async function test() {
is(newFront.actorID, previousActorID, "Should have the same actor for the same tab");
isnot(newFront, tabFront, "But the front should be a new one");
response = await newFront.attach();
is(response.type, "tabAttached", "Should have attached");
await newFront.attach();
response = await newFront.detach();
is(response.type, "detached", "Should have detached");

View File

@ -390,9 +390,6 @@ async function getTestTab(client, title) {
async function attachTestTab(client, title) {
const targetFront = await getTestTab(client, title);
await targetFront.attach();
const response = await targetFront.attach();
Assert.equal(response.type, "tabAttached");
Assert.ok(typeof response.threadActor === "string");
return targetFront;
}

View File

@ -75,11 +75,17 @@ class AddonTargetFront extends FrontClassWithSpec(addonTargetSpec) {
}
async attach() {
const response = await super.attach();
if (this._attach) {
return this._attach;
}
this._attach = (async () => {
const response = await super.attach();
this.threadActor = response.threadActor;
this.threadActor = response.threadActor;
return response;
return this.attachConsole();
})();
return this._attach;
}
reconfigure() {

View File

@ -5,10 +5,12 @@
const {browsingContextTargetSpec} = require("devtools/shared/specs/targets/browsing-context");
const { FrontClassWithSpec, registerFront } = require("devtools/shared/protocol");
const { TargetMixin } = require("./target-mixin");
loader.lazyRequireGetter(this, "ThreadClient", "devtools/shared/client/thread-client");
class BrowsingContextTargetFront extends FrontClassWithSpec(browsingContextTargetSpec) {
class BrowsingContextTargetFront extends
TargetMixin(FrontClassWithSpec(browsingContextTargetSpec)) {
constructor(client) {
super(client);
@ -23,8 +25,8 @@ class BrowsingContextTargetFront extends FrontClassWithSpec(browsingContextTarge
// RootFront.listTabs is going to update this state via `setIsSelected` method
this._selected = false;
// TODO: remove once ThreadClient becomes a front
this.client = client;
this._onTabNavigated = this._onTabNavigated.bind(this);
this._onFrameUpdate = this._onFrameUpdate.bind(this);
}
form(json) {
@ -36,8 +38,8 @@ class BrowsingContextTargetFront extends FrontClassWithSpec(browsingContextTarge
this.outerWindowID = json.outerWindowID;
this.favicon = json.favicon;
this.title = json.title;
this.url = json.url;
this._title = json.title;
this._url = json.url;
}
// Reports if the related tab is selected. Only applies to BrowsingContextTarget
@ -61,7 +63,6 @@ class BrowsingContextTargetFront extends FrontClassWithSpec(browsingContextTarge
if (this.thread) {
return Promise.resolve([{}, this.thread]);
}
const packet = {
to: this._threadActor,
type: "attach",
@ -74,14 +75,65 @@ class BrowsingContextTargetFront extends FrontClassWithSpec(browsingContextTarge
});
}
/**
* 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.nativeConsoleAPI = packet.nativeConsoleAPI;
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._url = packet.url;
this._title = packet.title;
}
// 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);
}
}
async attach() {
const response = await super.attach();
if (this._attach) {
return this._attach;
}
this._attach = (async () => {
// All Browsing context inherited target emit a few event that are being
// translated on the target class. Listen for them before attaching as they
// can start firing on attach call.
this.on("tabNavigated", this._onTabNavigated);
this.on("frameUpdate", this._onFrameUpdate);
this._threadActor = response.threadActor;
this.configureOptions.javascriptEnabled = response.javascriptEnabled;
this.traits = response.traits || {};
const response = await super.attach();
return response;
this._threadActor = response.threadActor;
this.configureOptions.javascriptEnabled = response.javascriptEnabled;
this.traits = response.traits || {};
// xpcshell tests from devtools/server/tests/unit/ are implementing
// fake BrowsingContextTargetActor which do not expose any console actor.
if (this.targetForm.consoleActor) {
await this.attachConsole();
}
})();
return this._attach;
}
async reconfigure({ options }) {
@ -111,11 +163,13 @@ class BrowsingContextTargetFront extends FrontClassWithSpec(browsingContextTarge
}
}
this.destroy();
// Remove listeners set in attach
this.off("tabNavigated", this._onTabNavigated);
this.off("frameUpdate", this._onFrameUpdate);
return response;
}
}
exports.BrowsingContextTargetFront = BrowsingContextTargetFront;
registerFront(BrowsingContextTargetFront);
registerFront(exports.BrowsingContextTargetFront);

View File

@ -5,13 +5,13 @@
const {contentProcessTargetSpec} = require("devtools/shared/specs/targets/content-process");
const { FrontClassWithSpec, registerFront } = require("devtools/shared/protocol");
const { TargetMixin } = require("./target-mixin");
class ContentProcessTargetFront extends FrontClassWithSpec(contentProcessTargetSpec) {
class ContentProcessTargetFront extends
TargetMixin(FrontClassWithSpec(contentProcessTargetSpec)) {
constructor(client) {
super(client);
this.client = client;
this.traits = {};
}
@ -28,6 +28,15 @@ class ContentProcessTargetFront extends FrontClassWithSpec(contentProcessTargetS
return this.client.attachThread(this.chromeDebugger);
}
attach() {
// All target actors have a console actor to attach.
// All but xpcshell test actors... which is using a ContentProcessTargetActor
if (this.targetForm.consoleActor) {
return this.attachConsole();
}
return Promise.resolve();
}
reconfigure() {
// Toolbox and options panel are calling this method but Worker Target can't be
// reconfigured. So we ignore this call here.
@ -36,4 +45,4 @@ class ContentProcessTargetFront extends FrontClassWithSpec(contentProcessTargetS
}
exports.ContentProcessTargetFront = ContentProcessTargetFront;
registerFront(ContentProcessTargetFront);
registerFront(exports.ContentProcessTargetFront);

View File

@ -8,5 +8,6 @@ DevToolsModules(
'addon.js',
'browsing-context.js',
'content-process.js',
'target-mixin.js',
'worker.js',
)

View File

@ -0,0 +1,571 @@
/* 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 Services = require("Services");
// We are requiring a module from client whereas this module is from shared.
// This shouldn't happen, but Fronts should rather be part of client anyway.
// Otherwise gDevTools is only used for local tabs and should propably only
// used by a subclass, specific to local tabs.
loader.lazyRequireGetter(this, "gDevTools",
"devtools/client/framework/devtools", true);
loader.lazyRequireGetter(this, "TargetFactory",
"devtools/client/framework/target", true);
loader.lazyRequireGetter(this, "getFront", "devtools/shared/protocol", 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, form) {
super(client, form);
this._forceChrome = false;
this.destroy = this.destroy.bind(this);
this.activeConsole = null;
this._client = client;
// Cache of already created targed-scoped fronts
// [typeName:string => Front instance]
this.fronts = new Map();
// Temporary fix for bug #1493131 - inspector has a different life cycle
// than most other fronts because it is closely related to the toolbox.
// TODO: remove once inspector is separated from the toolbox
this._inspector = null;
this._setupRemoteListeners();
}
attachTab(tab) {
// When debugging local tabs, we also have a reference to the Firefox tab
// This is used to:
// * distinguish local tabs from remote (see target.isLocalTab)
// * being able to hookup into Firefox UI (see Hosts)
this._tab = tab;
this._setupListeners();
}
/**
* Returns a promise for the protocol description from the root actor. Used
* internally with `target.actorHasMethod`. Takes advantage of caching if
* definition was fetched previously with the corresponding actor information.
* Actors are lazily loaded, so not only must the tool using a specific actor
* be in use, the actors are only registered after invoking a method (for
* performance reasons, added in bug 988237), so to use these actor detection
* methods, one must already be communicating with a specific actor of that
* type.
*
* @return {Promise}
* {
* "category": "actor",
* "typeName": "longstractor",
* "methods": [{
* "name": "substring",
* "request": {
* "type": "substring",
* "start": {
* "_arg": 0,
* "type": "primitive"
* },
* "end": {
* "_arg": 1,
* "type": "primitive"
* }
* },
* "response": {
* "substring": {
* "_retval": "primitive"
* }
* }
* }],
* "events": {}
* }
*/
async getActorDescription(actorName) {
if (this._protocolDescription &&
this._protocolDescription.types[actorName]) {
return this._protocolDescription.types[actorName];
}
const description = await this.client.mainRoot.protocolDescription();
this._protocolDescription = description;
return description.types[actorName];
}
/**
* 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;
}
/**
* Queries the protocol description to see if an actor has
* an available method. The actor must already be lazily-loaded (read
* the restrictions in the `getActorDescription` comments),
* so this is for use inside of tool. Returns a promise that
* resolves to a boolean.
*
* @param {String} actorName
* @param {String} methodName
* @return {Promise}
*/
actorHasMethod(actorName, methodName) {
return this.getActorDescription(actorName).then(desc => {
if (desc && desc.methods) {
return !!desc.methods.find(method => method.name === methodName);
}
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 tab() {
return this._tab;
}
// Get a promise of the RootActor's form
get root() {
return this.client.mainRoot.rootForm;
}
// Temporary fix for bug #1493131 - inspector has a different life cycle
// than most other fronts because it is closely related to the toolbox.
// TODO: remove once inspector is separated from the toolbox
async getInspector() {
// the front might have been destroyed and no longer have an actor ID
if (this._inspector && this._inspector.actorID) {
return this._inspector;
}
this._inspector = await getFront(this.client, "inspector", this.targetForm);
this.emit("inspector", this._inspector);
return this._inspector;
}
// Run callback on every front of this type that currently exists, and on every
// instantiation of front type in the future.
onFront(typeName, callback) {
const front = this.fronts.get(typeName);
if (front) {
return callback(front);
}
return this.on(typeName, callback);
}
// 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);
// the front might have been destroyed and no longer have an actor ID
if (front && front.actorID || front && typeof front.then === "function") {
return front;
}
front = getFront(this.client, typeName, this.targetForm);
this.fronts.set(typeName, front);
// replace the placeholder with the instance of the front once it has loaded
front = await front;
this.emit(typeName, 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 && 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) {
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() {
return this.typeName === "workerTarget";
}
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 isLocalTab() {
return !!this._tab;
}
get isMultiProcess() {
return !this.window;
}
get canRewind() {
return this.traits.canRewind;
}
isReplayEnabled() {
return Services.prefs.getBoolPref("devtools.recordreplay.mvp.enabled")
&& this.canRewind
&& this.isLocalTab;
}
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;
}
}
/**
* For local tabs, returns the tab's contentPrincipal, which can be used as a
* `triggeringPrincipal` when opening links. However, this is a hack as it is not
* correct for subdocuments and it won't work for remote debugging. Bug 1467945 hopes
* to devise a better approach.
*/
get contentPrincipal() {
if (!this.isLocalTab) {
return null;
}
return this.tab.linkedBrowser.contentPrincipal;
}
// Attach the console actor
async attachConsole() {
const [, consoleClient] = await this._client.attachConsole(
this.targetForm.consoleActor, []);
this.activeConsole = consoleClient;
this._onInspectObject = packet => this.emit("inspect-object", packet);
this.activeConsole.on("inspectObject", this._onInspectObject);
}
/**
* Listen to the different events.
*/
_setupListeners() {
this.tab.addEventListener("TabClose", this);
this.tab.ownerDocument.defaultView.addEventListener("unload", this);
this.tab.addEventListener("TabRemotenessChange", this);
}
/**
* Teardown event listeners.
*/
_teardownListeners() {
if (this._tab.ownerDocument.defaultView) {
this._tab.ownerDocument.defaultView.removeEventListener("unload", this);
}
this._tab.removeEventListener("TabClose", this);
this._tab.removeEventListener("TabRemotenessChange", this);
}
/**
* Setup listeners for remote debugging, updating existing ones as necessary.
*/
_setupRemoteListeners() {
this.client.addListener("closed", this.destroy);
this.on("tabDetached", this.destroy);
// These events should be ultimately listened from the thread client as
// they are coming from it and no longer go through the Target Actor/Front.
this._onSourceUpdated = packet => this.emit("source-updated", packet);
this.on("newSource", this._onSourceUpdated);
this.on("updatedSource", this._onSourceUpdated);
}
/**
* Teardown listeners for remote debugging.
*/
_teardownRemoteListeners() {
// Remove listeners set in _setupRemoteListeners
this.client.removeListener("closed", this.destroy);
this.off("tabDetached", this.destroy);
this.off("newSource", this._onSourceUpdated);
this.off("updatedSource", this._onSourceUpdated);
// Remove listeners set in attachConsole
if (this.activeConsole && this._onInspectObject) {
this.activeConsole.off("inspectObject", this._onInspectObject);
}
}
/**
* Handle tabs events.
*/
handleEvent(event) {
switch (event.type) {
case "TabClose":
case "unload":
this.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.
*/
onRemotenessChange() {
// Responsive design do a crazy dance around tabs and triggers
// remotenesschange events. But we should ignore them as at the end
// the content doesn't change its remoteness.
if (this._tab.isResponsiveDesignMode) {
return;
}
// Save a reference to the tab as it will be nullified on destroy
const tab = this._tab;
const onToolboxDestroyed = async (target) => {
if (target != this) {
return;
}
gDevTools.off("toolbox-destroyed", target);
// Recreate a fresh target instance as the current one is now destroyed
const newTarget = await TargetFactory.forTab(tab);
gDevTools.showToolbox(newTarget);
};
gDevTools.on("toolbox-destroyed", onToolboxDestroyed);
}
/**
* 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._destroyer = (async () => {
// Before taking any action, notify listeners that destruction is imminent.
this.emit("close");
for (let [, front] of this.fronts) {
front = await front;
await front.destroy();
}
if (this._tab) {
this._teardownListeners();
}
this._teardownRemoteListeners();
if (this.isLocalTab) {
// We started with a local tab and created the client ourselves, so we
// should close it.
await this._client.close();
// 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.actorID) {
// 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) {
console.warn(`Error while detaching target: ${e.message}`);
}
}
// Do that very last in order to let a chance to call detach.
super.destroy();
this._cleanup();
})();
return this._destroyer;
}
/**
* Clean up references to what this target points to.
*/
_cleanup() {
this.activeConsole = null;
this._client = null;
this._tab = 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;
}
toString() {
const id = this._tab ? this._tab : (this.targetForm && this.targetForm.actor);
return `Target:${id}`;
}
/**
* 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;

View File

@ -5,23 +5,26 @@
const {workerTargetSpec} = require("devtools/shared/specs/targets/worker");
const { FrontClassWithSpec, registerFront } = require("devtools/shared/protocol");
const { TargetMixin } = require("./target-mixin");
loader.lazyRequireGetter(this, "ThreadClient", "devtools/shared/client/thread-client");
class WorkerTargetFront extends FrontClassWithSpec(workerTargetSpec) {
class WorkerTargetFront extends
TargetMixin(FrontClassWithSpec(workerTargetSpec)) {
constructor(client) {
super(client);
this.thread = null;
this.traits = {};
// TODO: remove once ThreadClient becomes a front
this.client = client;
this._isClosed = false;
this.destroy = this.destroy.bind(this);
this.on("close", this.destroy);
// The actor sends a "close" event, which is translated to "worker-close" by
// the specification in order to not conflict with Target's "close" event.
// This event is similar to tabDetached and means that the worker is destroyed.
// So that we should destroy the target in order to significate that the target
// is no longer debuggable.
this.once("worker-close", this.destroy.bind(this));
}
form(json) {
@ -30,7 +33,7 @@ class WorkerTargetFront extends FrontClassWithSpec(workerTargetSpec) {
// 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._url = json.url;
this.type = json.type;
this.scope = json.scope;
this.fetch = json.fetch;
@ -41,31 +44,35 @@ class WorkerTargetFront extends FrontClassWithSpec(workerTargetSpec) {
}
destroy() {
this.off("close", this.destroy);
this._isClosed = true;
if (this.thread) {
this.client.unregisterClient(this.thread);
this.thread = null;
}
this.unmanage(this);
super.destroy();
}
async attach() {
const response = await super.attach();
if (this._attach) {
return this._attach;
}
this._attach = (async () => {
const response = await super.attach();
this.url = response.url;
this._url = response.url;
// Immediately call `connect` in other to fetch console and thread actors
// that will be later used by Target.
const connectResponse = await this.connect({});
// Set the console actor ID on the form to expose it to Target.attach's attachConsole
this.targetForm.consoleActor = connectResponse.consoleActor;
this.threadActor = connectResponse.threadActor;
// Immediately call `connect` in other to fetch console and thread actors
// that will be later used by Target.
const connectResponse = await this.connect({});
// Set the console actor ID on the form to expose it to Target.attachConsole
this.targetForm.consoleActor = connectResponse.consoleActor;
this.threadActor = connectResponse.threadActor;
return response;
return this.attachConsole();
})();
return this._attach;
}
async detach() {
@ -111,4 +118,4 @@ class WorkerTargetFront extends FrontClassWithSpec(workerTargetSpec) {
}
exports.WorkerTargetFront = WorkerTargetFront;
registerFront(WorkerTargetFront);
registerFront(exports.WorkerTargetFront);

View File

@ -1512,7 +1512,7 @@ var generateRequestMethods = function(actorSpec, frontProto) {
try {
ret = spec.response.read(response, this);
} catch (ex) {
console.error("Error reading response to: " + name);
console.error("Error reading response to: " + name + "\n" + ex);
throw ex;
}
return ret;

View File

@ -32,7 +32,8 @@ const workerTargetSpec = generateActorSpec({
events: {
// WorkerTargetActor still uses old sendActorEvent function,
// but it should use emit instead.
close: {
// Do not emit a `close` event as Target class emit this event on destroy
"worker-close": {
type: "close",
},
// newSource is being sent by ThreadActor in the name of its parent,