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: const {Cc, Ci, Cu} = require("chrome"); michael@0: const {Promise: promise} = require("resource://gre/modules/Promise.jsm"); michael@0: const EventEmitter = require("devtools/toolkit/event-emitter"); michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer", michael@0: "resource://gre/modules/devtools/dbg-server.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient", michael@0: "resource://gre/modules/devtools/dbg-client.jsm"); michael@0: michael@0: const targets = new WeakMap(); michael@0: const promiseTargets = new WeakMap(); michael@0: michael@0: /** michael@0: * Functions for creating Targets michael@0: */ michael@0: exports.TargetFactory = { michael@0: /** michael@0: * Construct a Target michael@0: * @param {XULTab} tab michael@0: * The tab to use in creating a new target. michael@0: * michael@0: * @return A target object michael@0: */ michael@0: forTab: function TF_forTab(tab) { michael@0: let target = targets.get(tab); michael@0: if (target == null) { michael@0: target = new TabTarget(tab); michael@0: targets.set(tab, target); michael@0: } michael@0: return target; michael@0: }, michael@0: michael@0: /** michael@0: * Return a promise of a Target for a remote tab. michael@0: * @param {Object} options michael@0: * The options object has the following properties: michael@0: * { michael@0: * form: the remote protocol form of a tab, michael@0: * client: a DebuggerClient instance michael@0: * (caller owns this and is responsible for closing), michael@0: * chrome: true if the remote target is the whole process michael@0: * } michael@0: * michael@0: * @return A promise of a target object michael@0: */ michael@0: forRemoteTab: function TF_forRemoteTab(options) { michael@0: let targetPromise = promiseTargets.get(options); michael@0: if (targetPromise == null) { michael@0: let target = new TabTarget(options); michael@0: targetPromise = target.makeRemote().then(() => target); michael@0: promiseTargets.set(options, targetPromise); michael@0: } michael@0: return targetPromise; michael@0: }, michael@0: michael@0: /** michael@0: * Creating a target for a tab that is being closed is a problem because it michael@0: * allows a leak as a result of coming after the close event which normally michael@0: * clears things up. This function allows us to ask if there is a known michael@0: * target for a tab without creating a target michael@0: * @return true/false michael@0: */ michael@0: isKnownTab: function TF_isKnownTab(tab) { michael@0: return targets.has(tab); michael@0: }, michael@0: michael@0: /** michael@0: * Construct a Target michael@0: * @param {nsIDOMWindow} window michael@0: * The chromeWindow to use in creating a new target michael@0: * @return A target object michael@0: */ michael@0: forWindow: function TF_forWindow(window) { michael@0: let target = targets.get(window); michael@0: if (target == null) { michael@0: target = new WindowTarget(window); michael@0: targets.set(window, target); michael@0: } michael@0: return target; michael@0: }, michael@0: michael@0: /** michael@0: * Get all of the targets known to the local browser instance michael@0: * @return An array of target objects michael@0: */ michael@0: allTargets: function TF_allTargets() { michael@0: let windows = []; michael@0: let wm = Cc["@mozilla.org/appshell/window-mediator;1"] michael@0: .getService(Ci.nsIWindowMediator); michael@0: let en = wm.getXULWindowEnumerator(null); michael@0: while (en.hasMoreElements()) { michael@0: windows.push(en.getNext()); michael@0: } michael@0: michael@0: return windows.map(function(window) { michael@0: return TargetFactory.forWindow(window); michael@0: }); michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * The 'version' property allows the developer tools equivalent of browser michael@0: * detection. Browser detection is evil, however while we don't know what we michael@0: * will need to detect in the future, it is an easy way to postpone work. michael@0: * We should be looking to use 'supports()' in place of version where michael@0: * possible. michael@0: */ michael@0: function getVersion() { michael@0: // FIXME: return something better michael@0: return 20; michael@0: } michael@0: michael@0: /** michael@0: * A better way to support feature detection, but we're not yet at a place michael@0: * where we have the features well enough defined for this to make lots of michael@0: * sense. michael@0: */ michael@0: function supports(feature) { michael@0: // FIXME: return something better michael@0: return false; michael@0: }; michael@0: michael@0: /** michael@0: * A Target represents something that we can debug. Targets are generally michael@0: * read-only. Any changes that you wish to make to a target should be done via michael@0: * a Tool that attaches to the target. i.e. a Target is just a pointer saying michael@0: * "the thing to debug is over there". michael@0: * michael@0: * Providing a generalized abstraction of a web-page or web-browser (available michael@0: * either locally or remotely) is beyond the scope of this class (and maybe michael@0: * also beyond the scope of this universe) However Target does attempt to michael@0: * abstract some common events and read-only properties common to many Tools. michael@0: * michael@0: * Supported read-only properties: michael@0: * - name, isRemote, url michael@0: * michael@0: * Target extends EventEmitter and provides support for the following events: michael@0: * - close: The target window has been closed. All tools attached to this michael@0: * target should close. This event is not currently cancelable. michael@0: * - navigate: The target window has navigated to a different URL michael@0: * michael@0: * Optional events: michael@0: * - will-navigate: The target window will navigate to a different URL michael@0: * - hidden: The target is not visible anymore (for TargetTab, another tab is selected) michael@0: * - visible: The target is visible (for TargetTab, tab is selected) michael@0: * michael@0: * Target also supports 2 functions to help allow 2 different versions of michael@0: * Firefox debug each other. The 'version' property is the equivalent of michael@0: * browser detection - simple and easy to implement but gets fragile when things michael@0: * are not quite what they seem. The 'supports' property is the equivalent of michael@0: * feature detection - harder to setup, but more robust long-term. michael@0: * michael@0: * Comparing Targets: 2 instances of a Target object can point at the same michael@0: * thing, so t1 !== t2 and t1 != t2 even when they represent the same object. michael@0: * To compare to targets use 't1.equals(t2)'. michael@0: */ michael@0: function Target() { michael@0: throw new Error("Use TargetFactory.newXXX or Target.getXXX to create a Target in place of 'new Target()'"); michael@0: } michael@0: michael@0: Object.defineProperty(Target.prototype, "version", { michael@0: get: getVersion, michael@0: enumerable: true michael@0: }); michael@0: michael@0: michael@0: /** michael@0: * A TabTarget represents a page living in a browser tab. Generally these will michael@0: * be web pages served over http(s), but they don't have to be. michael@0: */ michael@0: function TabTarget(tab) { michael@0: EventEmitter.decorate(this); michael@0: this.destroy = this.destroy.bind(this); michael@0: this._handleThreadState = this._handleThreadState.bind(this); michael@0: this.on("thread-resumed", this._handleThreadState); michael@0: this.on("thread-paused", this._handleThreadState); michael@0: // Only real tabs need initialization here. Placeholder objects for remote michael@0: // targets will be initialized after a makeRemote method call. michael@0: if (tab && !["client", "form", "chrome"].every(tab.hasOwnProperty, tab)) { michael@0: this._tab = tab; michael@0: this._setupListeners(); michael@0: } else { michael@0: this._form = tab.form; michael@0: this._client = tab.client; michael@0: this._chrome = tab.chrome; michael@0: } michael@0: } michael@0: michael@0: TabTarget.prototype = { michael@0: _webProgressListener: null, michael@0: michael@0: supports: supports, michael@0: get version() { return getVersion(); }, michael@0: michael@0: get tab() { michael@0: return this._tab; michael@0: }, michael@0: michael@0: get form() { michael@0: return this._form; michael@0: }, michael@0: michael@0: get root() { michael@0: return this._root; michael@0: }, michael@0: michael@0: get client() { michael@0: return this._client; michael@0: }, michael@0: michael@0: get chrome() { michael@0: return this._chrome; michael@0: }, michael@0: michael@0: get window() { michael@0: // Be extra careful here, since this may be called by HS_getHudByWindow michael@0: // during shutdown. michael@0: if (this._tab && this._tab.linkedBrowser) { michael@0: return this._tab.linkedBrowser.contentWindow; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: get name() { michael@0: return this._tab ? this._tab.linkedBrowser.contentDocument.title : michael@0: this._form.title; michael@0: }, michael@0: michael@0: get url() { michael@0: return this._tab ? this._tab.linkedBrowser.contentDocument.location.href : michael@0: this._form.url; michael@0: }, michael@0: michael@0: get isRemote() { michael@0: return !this.isLocalTab; michael@0: }, michael@0: michael@0: get isAddon() { michael@0: return !!(this._form && this._form.addonActor); michael@0: }, michael@0: michael@0: get isLocalTab() { michael@0: return !!this._tab; michael@0: }, michael@0: michael@0: get isThreadPaused() { michael@0: return !!this._isThreadPaused; michael@0: }, michael@0: michael@0: /** michael@0: * Adds remote protocol capabilities to the target, so that it can be used michael@0: * for tools that support the Remote Debugging Protocol even for local michael@0: * connections. michael@0: */ michael@0: makeRemote: function TabTarget_makeRemote() { michael@0: if (this._remote) { michael@0: return this._remote.promise; michael@0: } michael@0: michael@0: this._remote = promise.defer(); michael@0: michael@0: if (this.isLocalTab) { michael@0: // Since a remote protocol connection will be made, let's start the michael@0: // DebuggerServer here, once and for all tools. michael@0: if (!DebuggerServer.initialized) { michael@0: DebuggerServer.init(); michael@0: DebuggerServer.addBrowserActors(); michael@0: } michael@0: michael@0: this._client = new DebuggerClient(DebuggerServer.connectPipe()); michael@0: // A local TabTarget will never perform chrome debugging. michael@0: this._chrome = false; michael@0: } michael@0: michael@0: this._setupRemoteListeners(); michael@0: michael@0: let attachTab = () => { michael@0: this._client.attachTab(this._form.actor, (aResponse, aTabClient) => { michael@0: if (!aTabClient) { michael@0: this._remote.reject("Unable to attach to the tab"); michael@0: return; michael@0: } michael@0: this.activeTab = aTabClient; michael@0: this.threadActor = aResponse.threadActor; michael@0: this._remote.resolve(null); michael@0: }); michael@0: }; michael@0: michael@0: if (this.isLocalTab) { michael@0: this._client.connect((aType, aTraits) => { michael@0: this._client.listTabs(aResponse => { michael@0: this._root = aResponse; michael@0: michael@0: let windowUtils = this.window michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: let outerWindow = windowUtils.outerWindowID; michael@0: aResponse.tabs.some((tab) => { michael@0: if (tab.outerWindowID === outerWindow) { michael@0: this._form = tab; michael@0: return true; michael@0: } michael@0: return false; michael@0: }); michael@0: if (!this._form) { michael@0: this._form = aResponse.tabs[aResponse.selected]; michael@0: } michael@0: attachTab(); michael@0: }); michael@0: }); michael@0: } else if (!this.chrome) { michael@0: // In the remote debugging case, the protocol connection will have been michael@0: // already initialized in the connection screen code. michael@0: attachTab(); michael@0: } else { michael@0: // Remote chrome debugging doesn't need anything at this point. michael@0: this._remote.resolve(null); michael@0: } michael@0: michael@0: return this._remote.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Listen to the different events. michael@0: */ michael@0: _setupListeners: function TabTarget__setupListeners() { michael@0: this._webProgressListener = new TabWebProgressListener(this); michael@0: this.tab.linkedBrowser.addProgressListener(this._webProgressListener); michael@0: this.tab.addEventListener("TabClose", this); michael@0: this.tab.parentNode.addEventListener("TabSelect", this); michael@0: this.tab.ownerDocument.defaultView.addEventListener("unload", this); michael@0: }, michael@0: michael@0: /** michael@0: * Teardown event listeners. michael@0: */ michael@0: _teardownListeners: function TabTarget__teardownListeners() { michael@0: if (this._webProgressListener) { michael@0: this._webProgressListener.destroy(); michael@0: } michael@0: michael@0: this._tab.ownerDocument.defaultView.removeEventListener("unload", this); michael@0: this._tab.removeEventListener("TabClose", this); michael@0: this._tab.parentNode.removeEventListener("TabSelect", this); michael@0: }, michael@0: michael@0: /** michael@0: * Setup listeners for remote debugging, updating existing ones as necessary. michael@0: */ michael@0: _setupRemoteListeners: function TabTarget__setupRemoteListeners() { michael@0: this.client.addListener("closed", this.destroy); michael@0: michael@0: this._onTabDetached = (aType, aPacket) => { michael@0: // We have to filter message to ensure that this detach is for this tab michael@0: if (aPacket.from == this._form.actor) { michael@0: this.destroy(); michael@0: } michael@0: }; michael@0: this.client.addListener("tabDetached", this._onTabDetached); michael@0: michael@0: this._onTabNavigated = function onRemoteTabNavigated(aType, aPacket) { michael@0: let event = Object.create(null); michael@0: event.url = aPacket.url; michael@0: event.title = aPacket.title; michael@0: event.nativeConsoleAPI = aPacket.nativeConsoleAPI; michael@0: // Send any stored event payload (DOMWindow or nsIRequest) for backwards michael@0: // compatibility with non-remotable tools. michael@0: if (aPacket.state == "start") { michael@0: event._navPayload = this._navRequest; michael@0: this.emit("will-navigate", event); michael@0: this._navRequest = null; michael@0: } else { michael@0: event._navPayload = this._navWindow; michael@0: this.emit("navigate", event); michael@0: this._navWindow = null; michael@0: } michael@0: }.bind(this); michael@0: this.client.addListener("tabNavigated", this._onTabNavigated); michael@0: }, michael@0: michael@0: /** michael@0: * Teardown listeners for remote debugging. michael@0: */ michael@0: _teardownRemoteListeners: function TabTarget__teardownRemoteListeners() { michael@0: this.client.removeListener("closed", this.destroy); michael@0: this.client.removeListener("tabNavigated", this._onTabNavigated); michael@0: this.client.removeListener("tabDetached", this._onTabDetached); michael@0: }, michael@0: michael@0: /** michael@0: * Handle tabs events. michael@0: */ michael@0: handleEvent: function (event) { michael@0: switch (event.type) { michael@0: case "TabClose": michael@0: case "unload": michael@0: this.destroy(); michael@0: break; michael@0: case "TabSelect": michael@0: if (this.tab.selected) { michael@0: this.emit("visible", event); michael@0: } else { michael@0: this.emit("hidden", event); michael@0: } michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handle script status. michael@0: */ michael@0: _handleThreadState: function(event) { michael@0: switch (event) { michael@0: case "thread-resumed": michael@0: this._isThreadPaused = false; michael@0: break; michael@0: case "thread-paused": michael@0: this._isThreadPaused = true; michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Target is not alive anymore. michael@0: */ michael@0: destroy: function() { michael@0: // If several things call destroy then we give them all the same michael@0: // destruction promise so we're sure to destroy only once michael@0: if (this._destroyer) { michael@0: return this._destroyer.promise; michael@0: } michael@0: michael@0: this._destroyer = promise.defer(); michael@0: michael@0: // Before taking any action, notify listeners that destruction is imminent. michael@0: this.emit("close"); michael@0: michael@0: // First of all, do cleanup tasks that pertain to both remoted and michael@0: // non-remoted targets. michael@0: this.off("thread-resumed", this._handleThreadState); michael@0: this.off("thread-paused", this._handleThreadState); michael@0: michael@0: if (this._tab) { michael@0: this._teardownListeners(); michael@0: } michael@0: michael@0: let cleanupAndResolve = () => { michael@0: this._cleanup(); michael@0: this._destroyer.resolve(null); michael@0: }; michael@0: // If this target was not remoted, the promise will be resolved before the michael@0: // function returns. michael@0: if (this._tab && !this._client) { michael@0: cleanupAndResolve(); michael@0: } else if (this._client) { michael@0: // If, on the other hand, this target was remoted, the promise will be michael@0: // resolved after the remote connection is closed. michael@0: this._teardownRemoteListeners(); michael@0: michael@0: if (this.isLocalTab) { michael@0: // We started with a local tab and created the client ourselves, so we michael@0: // should close it. michael@0: this._client.close(cleanupAndResolve); michael@0: } else { michael@0: // The client was handed to us, so we are not responsible for closing michael@0: // it. We just need to detach from the tab, if already attached. michael@0: if (this.activeTab) { michael@0: this.activeTab.detach(cleanupAndResolve); michael@0: } else { michael@0: cleanupAndResolve(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: return this._destroyer.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Clean up references to what this target points to. michael@0: */ michael@0: _cleanup: function TabTarget__cleanup() { michael@0: if (this._tab) { michael@0: targets.delete(this._tab); michael@0: } else { michael@0: promiseTargets.delete(this._form); michael@0: } michael@0: this.activeTab = null; michael@0: this._client = null; michael@0: this._tab = null; michael@0: this._form = null; michael@0: this._remote = null; michael@0: }, michael@0: michael@0: toString: function() { michael@0: return 'TabTarget:' + (this._tab ? this._tab : (this._form && this._form.actor)); michael@0: }, michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * WebProgressListener for TabTarget. michael@0: * michael@0: * @param object aTarget michael@0: * The TabTarget instance to work with. michael@0: */ michael@0: function TabWebProgressListener(aTarget) { michael@0: this.target = aTarget; michael@0: } michael@0: michael@0: TabWebProgressListener.prototype = { michael@0: target: null, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), michael@0: michael@0: onStateChange: function TWPL_onStateChange(progress, request, flag, status) { michael@0: let isStart = flag & Ci.nsIWebProgressListener.STATE_START; michael@0: let isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; michael@0: let isNetwork = flag & Ci.nsIWebProgressListener.STATE_IS_NETWORK; michael@0: let isRequest = flag & Ci.nsIWebProgressListener.STATE_IS_REQUEST; michael@0: michael@0: // Skip non-interesting states. michael@0: if (!isStart || !isDocument || !isRequest || !isNetwork) { michael@0: return; michael@0: } michael@0: michael@0: // emit event if the top frame is navigating michael@0: if (this.target && this.target.window == progress.DOMWindow) { michael@0: // Emit the event if the target is not remoted or store the payload for michael@0: // later emission otherwise. michael@0: if (this.target._client) { michael@0: this.target._navRequest = request; michael@0: } else { michael@0: this.target.emit("will-navigate", request); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: onProgressChange: function() {}, michael@0: onSecurityChange: function() {}, michael@0: onStatusChange: function() {}, michael@0: michael@0: onLocationChange: function TWPL_onLocationChange(webProgress, request, URI, flags) { michael@0: if (this.target && michael@0: !(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) { michael@0: let window = webProgress.DOMWindow; michael@0: // Emit the event if the target is not remoted or store the payload for michael@0: // later emission otherwise. michael@0: if (this.target._client) { michael@0: this.target._navWindow = window; michael@0: } else { michael@0: this.target.emit("navigate", window); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Destroy the progress listener instance. michael@0: */ michael@0: destroy: function TWPL_destroy() { michael@0: if (this.target.tab) { michael@0: try { michael@0: this.target.tab.linkedBrowser.removeProgressListener(this); michael@0: } catch (ex) { michael@0: // This can throw when a tab crashes in e10s. michael@0: } michael@0: } michael@0: this.target._webProgressListener = null; michael@0: this.target._navRequest = null; michael@0: this.target._navWindow = null; michael@0: this.target = null; michael@0: } michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * A WindowTarget represents a page living in a xul window or panel. Generally michael@0: * these will have a chrome: URL michael@0: */ michael@0: function WindowTarget(window) { michael@0: EventEmitter.decorate(this); michael@0: this._window = window; michael@0: this._setupListeners(); michael@0: } michael@0: michael@0: WindowTarget.prototype = { michael@0: supports: supports, michael@0: get version() { return getVersion(); }, michael@0: michael@0: get window() { michael@0: return this._window; michael@0: }, michael@0: michael@0: get name() { michael@0: return this._window.document.title; michael@0: }, michael@0: michael@0: get url() { michael@0: return this._window.document.location.href; michael@0: }, michael@0: michael@0: get isRemote() { michael@0: return false; michael@0: }, michael@0: michael@0: get isLocalTab() { michael@0: return false; michael@0: }, michael@0: michael@0: get isThreadPaused() { michael@0: return !!this._isThreadPaused; michael@0: }, michael@0: michael@0: /** michael@0: * Listen to the different events. michael@0: */ michael@0: _setupListeners: function() { michael@0: this._handleThreadState = this._handleThreadState.bind(this); michael@0: this.on("thread-paused", this._handleThreadState); michael@0: this.on("thread-resumed", this._handleThreadState); michael@0: }, michael@0: michael@0: _handleThreadState: function(event) { michael@0: switch (event) { michael@0: case "thread-resumed": michael@0: this._isThreadPaused = false; michael@0: break; michael@0: case "thread-paused": michael@0: this._isThreadPaused = true; michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Target is not alive anymore. michael@0: */ michael@0: destroy: function() { michael@0: if (!this._destroyed) { michael@0: this._destroyed = true; michael@0: michael@0: this.off("thread-paused", this._handleThreadState); michael@0: this.off("thread-resumed", this._handleThreadState); michael@0: this.emit("close"); michael@0: michael@0: targets.delete(this._window); michael@0: this._window = null; michael@0: } michael@0: michael@0: return promise.resolve(null); michael@0: }, michael@0: michael@0: toString: function() { michael@0: return 'WindowTarget:' + this.window; michael@0: }, michael@0: };