123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826 |
- /* 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 { Ci } = require("chrome");
- const promise = require("promise");
- const defer = require("devtools/shared/defer");
- const EventEmitter = require("devtools/shared/event-emitter");
- const Services = require("Services");
- const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
- loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
- loader.lazyRequireGetter(this, "DebuggerClient",
- "devtools/shared/client/main", true);
- loader.lazyRequireGetter(this, "gDevTools",
- "devtools/client/framework/devtools", true);
- const targets = new WeakMap();
- const promiseTargets = new WeakMap();
- /**
- * Functions for creating Targets
- */
- const TargetFactory = exports.TargetFactory = {
- /**
- * Construct a Target
- * @param {XULTab} tab
- * The tab to use in creating a new target.
- *
- * @return A target object
- */
- forTab: function (tab) {
- let target = targets.get(tab);
- if (target == null) {
- target = new TabTarget(tab);
- targets.set(tab, target);
- }
- return target;
- },
- /**
- * Return a promise of a Target for a remote tab.
- * @param {Object} options
- * The options object has the following properties:
- * {
- * form: the remote protocol form of a tab,
- * client: a DebuggerClient instance
- * (caller owns this and is responsible for closing),
- * chrome: true if the remote target is the whole process
- * }
- *
- * @return A promise of a target object
- */
- forRemoteTab: function (options) {
- let targetPromise = promiseTargets.get(options);
- if (targetPromise == null) {
- let target = new TabTarget(options);
- targetPromise = target.makeRemote().then(() => target);
- promiseTargets.set(options, targetPromise);
- }
- return targetPromise;
- },
- forWorker: function (workerClient) {
- let target = targets.get(workerClient);
- if (target == null) {
- target = new WorkerTarget(workerClient);
- targets.set(workerClient, target);
- }
- return target;
- },
- /**
- * Creating a target for a tab that is being closed is a problem because it
- * allows a leak as a result of coming after the close event which normally
- * clears things up. This function allows us to ask if there is a known
- * target for a tab without creating a target
- * @return true/false
- */
- isKnownTab: function (tab) {
- 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, isRemote, 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 TabTarget represents a page living in a browser tab. Generally these will
- * be web pages served over http(s), but they don't have to be.
- */
- function TabTarget(tab) {
- EventEmitter.decorate(this);
- this.destroy = this.destroy.bind(this);
- this.activeTab = this.activeConsole = null;
- // Only real tabs need initialization here. Placeholder objects for remote
- // targets will be initialized after a makeRemote method call.
- if (tab && !["client", "form", "chrome"].every(tab.hasOwnProperty, tab)) {
- this._tab = tab;
- this._setupListeners();
- } else {
- this._form = tab.form;
- this._url = this._form.url;
- this._title = this._form.title;
- this._client = tab.client;
- this._chrome = tab.chrome;
- }
- // Default isTabActor to true if not explicitly specified
- if (typeof tab.isTabActor == "boolean") {
- this._isTabActor = tab.isTabActor;
- } else {
- this._isTabActor = true;
- }
- }
- TabTarget.prototype = {
- _webProgressListener: 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.
- *
- * Must be a remote target.
- *
- * @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": {}
- * }
- */
- getActorDescription: function (actorName) {
- if (!this.client) {
- throw new Error("TabTarget#getActorDescription() can only be called on " +
- "remote tabs.");
- }
- let deferred = defer();
- if (this._protocolDescription &&
- this._protocolDescription.types[actorName]) {
- deferred.resolve(this._protocolDescription.types[actorName]);
- } else {
- this.client.mainRoot.protocolDescription(description => {
- this._protocolDescription = description;
- deferred.resolve(description.types[actorName]);
- });
- }
- return deferred.promise;
- },
- /**
- * Returns a boolean indicating whether or not the specific actor
- * type exists. Must be a remote target.
- *
- * @param {String} actorName
- * @return {Boolean}
- */
- hasActor: function (actorName) {
- if (!this.client) {
- throw new Error("TabTarget#hasActor() can only be called on remote " +
- "tabs.");
- }
- 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. Must be a remote target.
- *
- * @param {String} actorName
- * @param {String} methodName
- * @return {Promise}
- */
- actorHasMethod: function (actorName, methodName) {
- if (!this.client) {
- throw new Error("TabTarget#actorHasMethod() can only be called on " +
- "remote tabs.");
- }
- 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: function (traitName) {
- if (!this.client) {
- throw new Error("TabTarget#getTrait() can only be called on remote " +
- "tabs.");
- }
- // 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._form;
- },
- // Get a promise of the root form returned by a listTabs request. This promise
- // is cached.
- get root() {
- if (!this._root) {
- this._root = this._getRoot();
- }
- return this._root;
- },
- _getRoot: function () {
- return new Promise((resolve, reject) => {
- this.client.listTabs(response => {
- if (response.error) {
- reject(new Error(response.error + ": " + response.message));
- return;
- }
- resolve(response);
- });
- });
- },
- 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 TabActor interface
- // and requires to call `attach` request before being used
- // and `detach` during cleanup
- get isTabActor() {
- return this._isTabActor;
- },
- get window() {
- // XXX - this is a footgun for e10s - there .contentWindow will be null,
- // and even though .contentWindowAsCPOW *might* work, it will not work
- // in all contexts. Consumers of .window need to be refactored to not
- // rely on this.
- if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
- console.error("The .window getter on devtools' |target| object isn't " +
- "e10s friendly!\n" + Error().stack);
- }
- // Be extra careful here, since this may be called by HS_getHudByWindow
- // during shutdown.
- if (this._tab && this._tab.linkedBrowser) {
- return this._tab.linkedBrowser.contentWindow;
- }
- return null;
- },
- get name() {
- if (this.isAddon) {
- return this._form.name;
- }
- return this._title;
- },
- get url() {
- return this._url;
- },
- get isRemote() {
- return !this.isLocalTab;
- },
- get isAddon() {
- return !!(this._form && this._form.actor && (
- this._form.actor.match(/conn\d+\.addon\d+/) ||
- this._form.actor.match(/conn\d+\.webExtension\d+/)
- ));
- },
- get isWebExtension() {
- return !!(this._form && this._form.actor &&
- this._form.actor.match(/conn\d+\.webExtension\d+/));
- },
- get isLocalTab() {
- return !!this._tab;
- },
- get isMultiProcess() {
- return !this.window;
- },
- /**
- * Adds remote protocol capabilities to the target, so that it can be used
- * for tools that support the Remote Debugging Protocol even for local
- * connections.
- */
- makeRemote: function () {
- if (this._remote) {
- return this._remote.promise;
- }
- this._remote = defer();
- if (this.isLocalTab) {
- // Since a remote protocol connection will be made, let's start the
- // DebuggerServer here, once and for all tools.
- if (!DebuggerServer.initialized) {
- DebuggerServer.init();
- DebuggerServer.addBrowserActors();
- }
- this._client = new DebuggerClient(DebuggerServer.connectPipe());
- // A local TabTarget will never perform chrome debugging.
- this._chrome = false;
- }
- this._setupRemoteListeners();
- let attachTab = () => {
- this._client.attachTab(this._form.actor, (response, tabClient) => {
- if (!tabClient) {
- this._remote.reject("Unable to attach to the tab");
- return;
- }
- this.activeTab = tabClient;
- this.threadActor = response.threadActor;
- attachConsole();
- });
- };
- let onConsoleAttached = (response, consoleClient) => {
- if (!consoleClient) {
- this._remote.reject("Unable to attach to the console");
- return;
- }
- this.activeConsole = consoleClient;
- this._remote.resolve(null);
- };
- let attachConsole = () => {
- this._client.attachConsole(this._form.consoleActor,
- [ "NetworkActivity" ],
- onConsoleAttached);
- };
- if (this.isLocalTab) {
- this._client.connect()
- .then(() => this._client.getTab({ tab: this.tab }))
- .then(response => {
- this._form = response.tab;
- this._url = this._form.url;
- this._title = this._form.title;
- attachTab();
- }, e => this._remote.reject(e));
- } else if (this.isTabActor) {
- // In the remote debugging case, the protocol connection will have been
- // already initialized in the connection screen code.
- attachTab();
- } else {
- // AddonActor and chrome debugging on RootActor doesn't inherits from
- // TabActor and doesn't need to be attached.
- attachConsole();
- }
- return this._remote.promise;
- },
- /**
- * Listen to the different events.
- */
- _setupListeners: function () {
- this._webProgressListener = new TabWebProgressListener(this);
- this.tab.linkedBrowser.addProgressListener(this._webProgressListener);
- this.tab.addEventListener("TabClose", this);
- this.tab.parentNode.addEventListener("TabSelect", this);
- this.tab.ownerDocument.defaultView.addEventListener("unload", this);
- this.tab.addEventListener("TabRemotenessChange", this);
- },
- /**
- * Teardown event listeners.
- */
- _teardownListeners: function () {
- if (this._webProgressListener) {
- this._webProgressListener.destroy();
- }
- this._tab.ownerDocument.defaultView.removeEventListener("unload", this);
- this._tab.removeEventListener("TabClose", this);
- this._tab.parentNode.removeEventListener("TabSelect", this);
- this._tab.removeEventListener("TabRemotenessChange", this);
- },
- /**
- * Setup listeners for remote debugging, updating existing ones as necessary.
- */
- _setupRemoteListeners: function () {
- this.client.addListener("closed", this.destroy);
- this._onTabDetached = (aType, aPacket) => {
- // We have to filter message to ensure that this detach is for this tab
- if (aPacket.from == this._form.actor) {
- this.destroy();
- }
- };
- this.client.addListener("tabDetached", this._onTabDetached);
- this._onTabNavigated = (aType, aPacket) => {
- let event = Object.create(null);
- event.url = aPacket.url;
- event.title = aPacket.title;
- event.nativeConsoleAPI = aPacket.nativeConsoleAPI;
- event.isFrameSwitching = aPacket.isFrameSwitching;
- if (!aPacket.isFrameSwitching) {
- // Update the title and url unless this is a frame switch.
- this._url = aPacket.url;
- this._title = aPacket.title;
- }
- // Send any stored event payload (DOMWindow or nsIRequest) for backwards
- // compatibility with non-remotable tools.
- if (aPacket.state == "start") {
- event._navPayload = this._navRequest;
- this.emit("will-navigate", event);
- this._navRequest = null;
- } else {
- event._navPayload = this._navWindow;
- this.emit("navigate", event);
- this._navWindow = null;
- }
- };
- this.client.addListener("tabNavigated", this._onTabNavigated);
- this._onFrameUpdate = (aType, aPacket) => {
- this.emit("frame-update", aPacket);
- };
- this.client.addListener("frameUpdate", this._onFrameUpdate);
- this._onSourceUpdated = (event, packet) => this.emit("source-updated", packet);
- this.client.addListener("newSource", this._onSourceUpdated);
- this.client.addListener("updatedSource", this._onSourceUpdated);
- },
- /**
- * Teardown listeners for remote debugging.
- */
- _teardownRemoteListeners: function () {
- this.client.removeListener("closed", this.destroy);
- this.client.removeListener("tabNavigated", this._onTabNavigated);
- this.client.removeListener("tabDetached", this._onTabDetached);
- this.client.removeListener("frameUpdate", this._onFrameUpdate);
- this.client.removeListener("newSource", this._onSourceUpdated);
- this.client.removeListener("updatedSource", this._onSourceUpdated);
- },
- /**
- * Handle tabs events.
- */
- handleEvent: function (event) {
- switch (event.type) {
- case "TabClose":
- case "unload":
- this.destroy();
- break;
- case "TabSelect":
- if (this.tab.selected) {
- this.emit("visible", event);
- } else {
- this.emit("hidden", event);
- }
- 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: function () {
- // 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
- let tab = this._tab;
- let onToolboxDestroyed = (event, target) => {
- if (target != this) {
- return;
- }
- gDevTools.off("toolbox-destroyed", target);
- // Recreate a fresh target instance as the current one is now destroyed
- let newTarget = TargetFactory.forTab(tab);
- gDevTools.showToolbox(newTarget);
- };
- gDevTools.on("toolbox-destroyed", onToolboxDestroyed);
- },
- /**
- * Target is not alive anymore.
- */
- destroy: function () {
- // 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.promise;
- }
- this._destroyer = defer();
- // Before taking any action, notify listeners that destruction is imminent.
- this.emit("close");
- if (this._tab) {
- this._teardownListeners();
- }
- let cleanupAndResolve = () => {
- this._cleanup();
- this._destroyer.resolve(null);
- };
- // If this target was not remoted, the promise will be resolved before the
- // function returns.
- if (this._tab && !this._client) {
- cleanupAndResolve();
- } else if (this._client) {
- // If, on the other hand, this target was remoted, the promise will be
- // resolved after the remote connection is closed.
- this._teardownRemoteListeners();
- if (this.isLocalTab) {
- // We started with a local tab and created the client ourselves, so we
- // should close it.
- this._client.close().then(cleanupAndResolve);
- } else if (this.activeTab) {
- // 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.
- this.activeTab.detach();
- cleanupAndResolve();
- } else {
- cleanupAndResolve();
- }
- }
- return this._destroyer.promise;
- },
- /**
- * Clean up references to what this target points to.
- */
- _cleanup: function () {
- 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._form = null;
- this._remote = null;
- this._root = null;
- this._title = null;
- this._url = null;
- this.threadActor = null;
- },
- toString: function () {
- let id = this._tab ? this._tab : (this._form && this._form.actor);
- return `TabTarget:${id}`;
- },
- /**
- * @see TabActor.prototype.onResolveLocation
- */
- resolveLocation(loc) {
- let deferred = defer();
- this.client.request(Object.assign({
- to: this._form.actor,
- type: "resolveLocation",
- }, loc), deferred.resolve);
- return deferred.promise;
- },
- };
- /**
- * WebProgressListener for TabTarget.
- *
- * @param object aTarget
- * The TabTarget instance to work with.
- */
- function TabWebProgressListener(aTarget) {
- this.target = aTarget;
- }
- TabWebProgressListener.prototype = {
- target: null,
- QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
- Ci.nsISupportsWeakReference]),
- onStateChange: function (progress, request, flag) {
- let isStart = flag & Ci.nsIWebProgressListener.STATE_START;
- let isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
- let isNetwork = flag & Ci.nsIWebProgressListener.STATE_IS_NETWORK;
- let isRequest = flag & Ci.nsIWebProgressListener.STATE_IS_REQUEST;
- // Skip non-interesting states.
- if (!isStart || !isDocument || !isRequest || !isNetwork) {
- return;
- }
- // emit event if the top frame is navigating
- if (progress.isTopLevel) {
- // Emit the event if the target is not remoted or store the payload for
- // later emission otherwise.
- if (this.target._client) {
- this.target._navRequest = request;
- } else {
- this.target.emit("will-navigate", request);
- }
- }
- },
- onProgressChange: function () {},
- onSecurityChange: function () {},
- onStatusChange: function () {},
- onLocationChange: function (webProgress, request, URI, flags) {
- if (this.target &&
- !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
- let window = webProgress.DOMWindow;
- // Emit the event if the target is not remoted or store the payload for
- // later emission otherwise.
- if (this.target._client) {
- this.target._navWindow = window;
- } else {
- this.target.emit("navigate", window);
- }
- }
- },
- /**
- * Destroy the progress listener instance.
- */
- destroy: function () {
- if (this.target.tab) {
- try {
- this.target.tab.linkedBrowser.removeProgressListener(this);
- } catch (ex) {
- // This can throw when a tab crashes in e10s.
- }
- }
- this.target._webProgressListener = null;
- this.target._navRequest = null;
- this.target._navWindow = null;
- this.target = null;
- }
- };
- function WorkerTarget(workerClient) {
- EventEmitter.decorate(this);
- this._workerClient = workerClient;
- }
- /**
- * A WorkerTarget represents a worker. Unlike TabTarget, which can represent
- * either a local or remote tab, WorkerTarget always represents a remote worker.
- * Moreover, unlike TabTarget, which is constructed with a placeholder object
- * for remote tabs (from which a TabClient can then be lazily obtained),
- * WorkerTarget is constructed with a WorkerClient directly.
- *
- * WorkerClient is designed to mimic the interface of TabClient as closely as
- * possible. This allows us to debug workers as if they were ordinary tabs,
- * requiring only minimal changes to the rest of the frontend.
- */
- WorkerTarget.prototype = {
- get isRemote() {
- return true;
- },
- get isTabActor() {
- return true;
- },
- get name() {
- return "Worker";
- },
- get url() {
- return this._workerClient.url;
- },
- get isWorkerTarget() {
- return true;
- },
- get form() {
- return {
- consoleActor: this._workerClient.consoleActor
- };
- },
- get activeTab() {
- return this._workerClient;
- },
- get client() {
- return this._workerClient.client;
- },
- destroy: function () {
- this._workerClient.detach();
- },
- hasActor: function (name) {
- // console is the only one actor implemented by WorkerActor
- if (name == "console") {
- return true;
- }
- return false;
- },
- getTrait: function () {
- return undefined;
- },
- makeRemote: function () {
- return Promise.resolve();
- }
- };
|