michael@0: /* -*- tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: let devtools_ = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools; michael@0: let { createExtraActors, appendExtraActors } = devtools_.require("devtools/server/actors/common"); michael@0: michael@0: /* Root actor for the remote debugging protocol. */ michael@0: michael@0: /** michael@0: * Create a remote debugging protocol root actor. michael@0: * michael@0: * @param aConnection michael@0: * The DebuggerServerConnection whose root actor we are constructing. michael@0: * michael@0: * @param aParameters michael@0: * The properties of |aParameters| provide backing objects for the root michael@0: * actor's requests; if a given property is omitted from |aParameters|, the michael@0: * root actor won't implement the corresponding requests or notifications. michael@0: * Supported properties: michael@0: * michael@0: * - tabList: a live list (see below) of tab actors. If present, the michael@0: * new root actor supports the 'listTabs' request, providing the live michael@0: * list's elements as its tab actors, and sending 'tabListChanged' michael@0: * notifications when the live list's contents change. One actor in michael@0: * this list must have a true '.selected' property. michael@0: * michael@0: * - addonList: a live list (see below) of addon actors. If present, the michael@0: * new root actor supports the 'listAddons' request, providing the live michael@0: * list's elements as its addon actors, and sending 'addonListchanged' michael@0: * notifications when the live list's contents change. michael@0: * michael@0: * - globalActorFactories: an object |A| describing further actors to michael@0: * attach to the 'listTabs' reply. This is the type accumulated by michael@0: * DebuggerServer.addGlobalActor. For each own property |P| of |A|, michael@0: * the root actor adds a property named |P| to the 'listTabs' michael@0: * reply whose value is the name of an actor constructed by michael@0: * |A[P]|. michael@0: * michael@0: * - onShutdown: a function to call when the root actor is disconnected. michael@0: * michael@0: * Instance properties: michael@0: * michael@0: * - applicationType: the string the root actor will include as the michael@0: * "applicationType" property in the greeting packet. By default, this michael@0: * is "browser". michael@0: * michael@0: * Live lists: michael@0: * michael@0: * A "live list", as used for the |tabList|, is an object that presents a michael@0: * list of actors, and also notifies its clients of changes to the list. A michael@0: * live list's interface is two properties: michael@0: * michael@0: * - getList: a method that returns a promise to the contents of the list. michael@0: * michael@0: * - onListChanged: a handler called, with no arguments, when the set of michael@0: * values the iterator would produce has changed since the last michael@0: * time 'iterator' was called. This may only be set to null or a michael@0: * callable value (one for which the typeof operator returns michael@0: * 'function'). (Note that the live list will not call the michael@0: * onListChanged handler until the list has been iterated over michael@0: * once; if nobody's seen the list in the first place, nobody michael@0: * should care if its contents have changed!) michael@0: * michael@0: * When the list changes, the list implementation should ensure that any michael@0: * actors yielded in previous iterations whose referents (tabs) still exist michael@0: * get yielded again in subsequent iterations. If the underlying referent michael@0: * is the same, the same actor should be presented for it. michael@0: * michael@0: * The root actor registers an 'onListChanged' handler on the appropriate michael@0: * list when it may need to send the client 'tabListChanged' notifications, michael@0: * and is careful to remove the handler whenever it does not need to send michael@0: * such notifications (including when it is disconnected). This means that michael@0: * live list implementations can use the state of the handler property (set michael@0: * or null) to install and remove observers and event listeners. michael@0: * michael@0: * Note that, as the only way for the root actor to see the members of the michael@0: * live list is to begin an iteration over the list, the live list need not michael@0: * actually produce any actors until they are reached in the course of michael@0: * iteration: alliterative lazy live lists. michael@0: */ michael@0: function RootActor(aConnection, aParameters) { michael@0: this.conn = aConnection; michael@0: this._parameters = aParameters; michael@0: this._onTabListChanged = this.onTabListChanged.bind(this); michael@0: this._onAddonListChanged = this.onAddonListChanged.bind(this); michael@0: this._extraActors = {}; michael@0: } michael@0: michael@0: RootActor.prototype = { michael@0: constructor: RootActor, michael@0: applicationType: "browser", michael@0: michael@0: /** michael@0: * Return a 'hello' packet as specified by the Remote Debugging Protocol. michael@0: */ michael@0: sayHello: function() { michael@0: return { michael@0: from: this.actorID, michael@0: applicationType: this.applicationType, michael@0: /* This is not in the spec, but it's used by tests. */ michael@0: testConnectionPrefix: this.conn.prefix, michael@0: traits: { michael@0: sources: true, michael@0: editOuterHTML: true, michael@0: // Wether the server-side highlighter actor exists and can be used to michael@0: // remotely highlight nodes (see server/actors/highlighter.js) michael@0: highlightable: true, michael@0: // Wether the inspector actor implements the getImageDataFromURL michael@0: // method that returns data-uris for image URLs. This is used for image michael@0: // tooltips for instance michael@0: urlToImageDataResolver: true, michael@0: networkMonitor: true, michael@0: // Wether the storage inspector actor to inspect cookies, etc. michael@0: storageInspector: true, michael@0: // Wether storage inspector is read only michael@0: storageInspectorReadOnly: true, michael@0: // Wether conditional breakpoints are supported michael@0: conditionalBreakpoints: true michael@0: } michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * This is true for the root actor only, used by some child actors michael@0: */ michael@0: get isRootActor() true, michael@0: michael@0: /** michael@0: * The (chrome) window, for use by child actors michael@0: */ michael@0: get window() Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType), michael@0: michael@0: /** michael@0: * URL of the chrome window. michael@0: */ michael@0: get url() { return this.window ? this.window.document.location.href : null; }, michael@0: michael@0: /** michael@0: * Getter for the best nsIWebProgress for to watching this window. michael@0: */ michael@0: get webProgress() { michael@0: return this.window michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDocShell) michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebProgress); michael@0: }, michael@0: michael@0: /** michael@0: * Disconnects the actor from the browser window. michael@0: */ michael@0: disconnect: function() { michael@0: /* Tell the live lists we aren't watching any more. */ michael@0: if (this._parameters.tabList) { michael@0: this._parameters.tabList.onListChanged = null; michael@0: } michael@0: if (this._parameters.addonList) { michael@0: this._parameters.addonList.onListChanged = null; michael@0: } michael@0: if (typeof this._parameters.onShutdown === 'function') { michael@0: this._parameters.onShutdown(); michael@0: } michael@0: this._extraActors = null; michael@0: }, michael@0: michael@0: /* The 'listTabs' request and the 'tabListChanged' notification. */ michael@0: michael@0: /** michael@0: * Handles the listTabs request. The actors will survive until at least michael@0: * the next listTabs request. michael@0: */ michael@0: onListTabs: function() { michael@0: let tabList = this._parameters.tabList; michael@0: if (!tabList) { michael@0: return { from: this.actorID, error: "noTabs", michael@0: message: "This root actor has no browser tabs." }; michael@0: } michael@0: michael@0: /* michael@0: * Walk the tab list, accumulating the array of tab actors for the michael@0: * reply, and moving all the actors to a new ActorPool. We'll michael@0: * replace the old tab actor pool with the one we build here, thus michael@0: * retiring any actors that didn't get listed again, and preparing any michael@0: * new actors to receive packets. michael@0: */ michael@0: let newActorPool = new ActorPool(this.conn); michael@0: let tabActorList = []; michael@0: let selected; michael@0: return tabList.getList().then((tabActors) => { michael@0: for (let tabActor of tabActors) { michael@0: if (tabActor.selected) { michael@0: selected = tabActorList.length; michael@0: } michael@0: tabActor.parentID = this.actorID; michael@0: newActorPool.addActor(tabActor); michael@0: tabActorList.push(tabActor); michael@0: } michael@0: michael@0: /* DebuggerServer.addGlobalActor support: create actors. */ michael@0: if (!this._globalActorPool) { michael@0: this._globalActorPool = new ActorPool(this.conn); michael@0: this._createExtraActors(this._parameters.globalActorFactories, this._globalActorPool); michael@0: this.conn.addActorPool(this._globalActorPool); michael@0: } michael@0: michael@0: /* michael@0: * Drop the old actorID -> actor map. Actors that still mattered were michael@0: * added to the new map; others will go away. michael@0: */ michael@0: if (this._tabActorPool) { michael@0: this.conn.removeActorPool(this._tabActorPool); michael@0: } michael@0: this._tabActorPool = newActorPool; michael@0: this.conn.addActorPool(this._tabActorPool); michael@0: michael@0: let reply = { michael@0: "from": this.actorID, michael@0: "selected": selected || 0, michael@0: "tabs": [actor.form() for (actor of tabActorList)], michael@0: }; michael@0: michael@0: /* If a root window is accessible, include its URL. */ michael@0: if (this.url) { michael@0: reply.url = this.url; michael@0: } michael@0: michael@0: /* DebuggerServer.addGlobalActor support: name actors in 'listTabs' reply. */ michael@0: this._appendExtraActors(reply); michael@0: michael@0: /* michael@0: * Now that we're actually going to report the contents of tabList to michael@0: * the client, we're responsible for letting the client know if it michael@0: * changes. michael@0: */ michael@0: tabList.onListChanged = this._onTabListChanged; michael@0: michael@0: return reply; michael@0: }); michael@0: }, michael@0: michael@0: onTabListChanged: function () { michael@0: this.conn.send({ from: this.actorID, type:"tabListChanged" }); michael@0: /* It's a one-shot notification; no need to watch any more. */ michael@0: this._parameters.tabList.onListChanged = null; michael@0: }, michael@0: michael@0: onListAddons: function () { michael@0: let addonList = this._parameters.addonList; michael@0: if (!addonList) { michael@0: return { from: this.actorID, error: "noAddons", michael@0: message: "This root actor has no browser addons." }; michael@0: } michael@0: michael@0: return addonList.getList().then((addonActors) => { michael@0: let addonActorPool = new ActorPool(this.conn); michael@0: for (let addonActor of addonActors) { michael@0: addonActorPool.addActor(addonActor); michael@0: } michael@0: michael@0: if (this._addonActorPool) { michael@0: this.conn.removeActorPool(this._addonActorPool); michael@0: } michael@0: this._addonActorPool = addonActorPool; michael@0: this.conn.addActorPool(this._addonActorPool); michael@0: michael@0: addonList.onListChanged = this._onAddonListChanged; michael@0: michael@0: return { michael@0: "from": this.actorID, michael@0: "addons": [addonActor.form() for (addonActor of addonActors)] michael@0: }; michael@0: }); michael@0: }, michael@0: michael@0: onAddonListChanged: function () { michael@0: this.conn.send({ from: this.actorID, type: "addonListChanged" }); michael@0: this._parameters.addonList.onListChanged = null; michael@0: }, michael@0: michael@0: /* This is not in the spec, but it's used by tests. */ michael@0: onEcho: function (aRequest) { michael@0: /* michael@0: * Request packets are frozen. Copy aRequest, so that michael@0: * DebuggerServerConnection.onPacket can attach a 'from' property. michael@0: */ michael@0: return JSON.parse(JSON.stringify(aRequest)); michael@0: }, michael@0: michael@0: onProtocolDescription: function (aRequest) { michael@0: return protocol.dumpProtocolSpec() michael@0: }, michael@0: michael@0: /* Support for DebuggerServer.addGlobalActor. */ michael@0: _createExtraActors: createExtraActors, michael@0: _appendExtraActors: appendExtraActors, michael@0: michael@0: /* ThreadActor hooks. */ michael@0: michael@0: /** michael@0: * Prepare to enter a nested event loop by disabling debuggee events. michael@0: */ michael@0: preNest: function() { michael@0: // Disable events in all open windows. michael@0: let e = Services.wm.getEnumerator(null); michael@0: while (e.hasMoreElements()) { michael@0: let win = e.getNext(); michael@0: let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: windowUtils.suppressEventHandling(true); michael@0: windowUtils.suspendTimeouts(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Prepare to exit a nested event loop by enabling debuggee events. michael@0: */ michael@0: postNest: function(aNestData) { michael@0: // Enable events in all open windows. michael@0: let e = Services.wm.getEnumerator(null); michael@0: while (e.hasMoreElements()) { michael@0: let win = e.getNext(); michael@0: let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: windowUtils.resumeTimeouts(); michael@0: windowUtils.suppressEventHandling(false); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: RootActor.prototype.requestTypes = { michael@0: "listTabs": RootActor.prototype.onListTabs, michael@0: "listAddons": RootActor.prototype.onListAddons, michael@0: "echo": RootActor.prototype.onEcho, michael@0: "protocolDescription": RootActor.prototype.onProtocolDescription michael@0: };