michael@0: /* -*- Mode: javascript; 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: var Ci = Components.interfaces; michael@0: var Cc = Components.classes; michael@0: var Cu = Components.utils; michael@0: var Cr = Components.results; michael@0: // On B2G scope object misbehaves and we have to bind globals to `this` michael@0: // in order to ensure theses variable to be visible in transport.js michael@0: this.Ci = Ci; michael@0: this.Cc = Cc; michael@0: this.Cu = Cu; michael@0: this.Cr = Cr; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["DebuggerTransport", michael@0: "DebuggerClient", michael@0: "RootClient", michael@0: "debuggerSocketConnect", michael@0: "LongStringClient", michael@0: "EnvironmentClient", michael@0: "ObjectClient"]; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/NetUtil.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/Timer.jsm"); michael@0: michael@0: let promise = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js").Promise; michael@0: const { defer, resolve, reject } = promise; michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "socketTransportService", michael@0: "@mozilla.org/network/socket-transport-service;1", michael@0: "nsISocketTransportService"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "console", michael@0: "resource://gre/modules/devtools/Console.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "devtools", michael@0: "resource://gre/modules/devtools/Loader.jsm"); michael@0: michael@0: Object.defineProperty(this, "WebConsoleClient", { michael@0: get: function () { michael@0: return devtools.require("devtools/toolkit/webconsole/client").WebConsoleClient; michael@0: }, michael@0: configurable: true, michael@0: enumerable: true michael@0: }); michael@0: michael@0: Components.utils.import("resource://gre/modules/devtools/DevToolsUtils.jsm"); michael@0: this.makeInfallible = DevToolsUtils.makeInfallible; michael@0: michael@0: let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log"); michael@0: michael@0: function dumpn(str) michael@0: { michael@0: if (wantLogging) { michael@0: dump("DBG-CLIENT: " + str + "\n"); michael@0: } michael@0: } michael@0: michael@0: let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] michael@0: .getService(Ci.mozIJSSubScriptLoader); michael@0: loader.loadSubScript("resource://gre/modules/devtools/server/transport.js", this); michael@0: michael@0: /** michael@0: * Add simple event notification to a prototype object. Any object that has michael@0: * some use for event notifications or the observer pattern in general can be michael@0: * augmented with the necessary facilities by passing its prototype to this michael@0: * function. michael@0: * michael@0: * @param aProto object michael@0: * The prototype object that will be modified. michael@0: */ michael@0: function eventSource(aProto) { michael@0: /** michael@0: * Add a listener to the event source for a given event. michael@0: * michael@0: * @param aName string michael@0: * The event to listen for. michael@0: * @param aListener function michael@0: * Called when the event is fired. If the same listener michael@0: * is added more than once, it will be called once per michael@0: * addListener call. michael@0: */ michael@0: aProto.addListener = function (aName, aListener) { michael@0: if (typeof aListener != "function") { michael@0: throw TypeError("Listeners must be functions."); michael@0: } michael@0: michael@0: if (!this._listeners) { michael@0: this._listeners = {}; michael@0: } michael@0: michael@0: this._getListeners(aName).push(aListener); michael@0: }; michael@0: michael@0: /** michael@0: * Add a listener to the event source for a given event. The michael@0: * listener will be removed after it is called for the first time. michael@0: * michael@0: * @param aName string michael@0: * The event to listen for. michael@0: * @param aListener function michael@0: * Called when the event is fired. michael@0: */ michael@0: aProto.addOneTimeListener = function (aName, aListener) { michael@0: let l = (...args) => { michael@0: this.removeListener(aName, l); michael@0: aListener.apply(null, args); michael@0: }; michael@0: this.addListener(aName, l); michael@0: }; michael@0: michael@0: /** michael@0: * Remove a listener from the event source previously added with michael@0: * addListener(). michael@0: * michael@0: * @param aName string michael@0: * The event name used during addListener to add the listener. michael@0: * @param aListener function michael@0: * The callback to remove. If addListener was called multiple michael@0: * times, all instances will be removed. michael@0: */ michael@0: aProto.removeListener = function (aName, aListener) { michael@0: if (!this._listeners || !this._listeners[aName]) { michael@0: return; michael@0: } michael@0: this._listeners[aName] = michael@0: this._listeners[aName].filter(function (l) { return l != aListener }); michael@0: }; michael@0: michael@0: /** michael@0: * Returns the listeners for the specified event name. If none are defined it michael@0: * initializes an empty list and returns that. michael@0: * michael@0: * @param aName string michael@0: * The event name. michael@0: */ michael@0: aProto._getListeners = function (aName) { michael@0: if (aName in this._listeners) { michael@0: return this._listeners[aName]; michael@0: } michael@0: this._listeners[aName] = []; michael@0: return this._listeners[aName]; michael@0: }; michael@0: michael@0: /** michael@0: * Notify listeners of an event. michael@0: * michael@0: * @param aName string michael@0: * The event to fire. michael@0: * @param arguments michael@0: * All arguments will be passed along to the listeners, michael@0: * including the name argument. michael@0: */ michael@0: aProto.notify = function () { michael@0: if (!this._listeners) { michael@0: return; michael@0: } michael@0: michael@0: let name = arguments[0]; michael@0: let listeners = this._getListeners(name).slice(0); michael@0: michael@0: for each (let listener in listeners) { michael@0: try { michael@0: listener.apply(null, arguments); michael@0: } catch (e) { michael@0: // Prevent a bad listener from interfering with the others. michael@0: DevToolsUtils.reportException("notify event '" + name + "'", e); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Set of protocol messages that affect thread state, and the michael@0: * state the actor is in after each message. michael@0: */ michael@0: const ThreadStateTypes = { michael@0: "paused": "paused", michael@0: "resumed": "attached", michael@0: "detached": "detached" michael@0: }; michael@0: michael@0: /** michael@0: * Set of protocol messages that are sent by the server without a prior request michael@0: * by the client. michael@0: */ michael@0: const UnsolicitedNotifications = { michael@0: "consoleAPICall": "consoleAPICall", michael@0: "eventNotification": "eventNotification", michael@0: "fileActivity": "fileActivity", michael@0: "lastPrivateContextExited": "lastPrivateContextExited", michael@0: "logMessage": "logMessage", michael@0: "networkEvent": "networkEvent", michael@0: "networkEventUpdate": "networkEventUpdate", michael@0: "newGlobal": "newGlobal", michael@0: "newScript": "newScript", michael@0: "newSource": "newSource", michael@0: "tabDetached": "tabDetached", michael@0: "tabListChanged": "tabListChanged", michael@0: "reflowActivity": "reflowActivity", michael@0: "addonListChanged": "addonListChanged", michael@0: "tabNavigated": "tabNavigated", michael@0: "pageError": "pageError", michael@0: "documentLoad": "documentLoad", michael@0: "enteredFrame": "enteredFrame", michael@0: "exitedFrame": "exitedFrame", michael@0: "appOpen": "appOpen", michael@0: "appClose": "appClose", michael@0: "appInstall": "appInstall", michael@0: "appUninstall": "appUninstall" michael@0: }; michael@0: michael@0: /** michael@0: * Set of pause types that are sent by the server and not as an immediate michael@0: * response to a client request. michael@0: */ michael@0: const UnsolicitedPauses = { michael@0: "resumeLimit": "resumeLimit", michael@0: "debuggerStatement": "debuggerStatement", michael@0: "breakpoint": "breakpoint", michael@0: "DOMEvent": "DOMEvent", michael@0: "watchpoint": "watchpoint", michael@0: "exception": "exception" michael@0: }; michael@0: michael@0: /** michael@0: * Creates a client for the remote debugging protocol server. This client michael@0: * provides the means to communicate with the server and exchange the messages michael@0: * required by the protocol in a traditional JavaScript API. michael@0: */ michael@0: this.DebuggerClient = function (aTransport) michael@0: { michael@0: this._transport = aTransport; michael@0: this._transport.hooks = this; michael@0: michael@0: // Map actor ID to client instance for each actor type. michael@0: this._threadClients = new Map; michael@0: this._addonClients = new Map; michael@0: this._tabClients = new Map; michael@0: this._tracerClients = new Map; michael@0: this._consoleClients = new Map; michael@0: michael@0: this._pendingRequests = []; michael@0: this._activeRequests = new Map; michael@0: this._eventsEnabled = true; michael@0: michael@0: this.compat = new ProtocolCompatibility(this, []); michael@0: this.traits = {}; michael@0: michael@0: this.request = this.request.bind(this); michael@0: this.localTransport = this._transport.onOutputStreamReady === undefined; michael@0: michael@0: /* michael@0: * As the first thing on the connection, expect a greeting packet from michael@0: * the connection's root actor. michael@0: */ michael@0: this.mainRoot = null; michael@0: this.expectReply("root", (aPacket) => { michael@0: this.mainRoot = new RootClient(this, aPacket); michael@0: this.notify("connected", aPacket.applicationType, aPacket.traits); michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * A declarative helper for defining methods that send requests to the server. michael@0: * michael@0: * @param aPacketSkeleton michael@0: * The form of the packet to send. Can specify fields to be filled from michael@0: * the parameters by using the |args| function. michael@0: * @param telemetry michael@0: * The unique suffix of the telemetry histogram id. michael@0: * @param before michael@0: * The function to call before sending the packet. Is passed the packet, michael@0: * and the return value is used as the new packet. The |this| context is michael@0: * the instance of the client object we are defining a method for. michael@0: * @param after michael@0: * The function to call after the response is received. It is passed the michael@0: * response, and the return value is considered the new response that michael@0: * will be passed to the callback. The |this| context is the instance of michael@0: * the client object we are defining a method for. michael@0: */ michael@0: DebuggerClient.requester = function (aPacketSkeleton, michael@0: { telemetry, before, after }) { michael@0: return DevToolsUtils.makeInfallible(function (...args) { michael@0: let histogram, startTime; michael@0: if (telemetry) { michael@0: let transportType = this._transport.onOutputStreamReady === undefined michael@0: ? "LOCAL_" michael@0: : "REMOTE_"; michael@0: let histogramId = "DEVTOOLS_DEBUGGER_RDP_" michael@0: + transportType + telemetry + "_MS"; michael@0: histogram = Services.telemetry.getHistogramById(histogramId); michael@0: startTime = +new Date; michael@0: } michael@0: let outgoingPacket = { michael@0: to: aPacketSkeleton.to || this.actor michael@0: }; michael@0: michael@0: let maxPosition = -1; michael@0: for (let k of Object.keys(aPacketSkeleton)) { michael@0: if (aPacketSkeleton[k] instanceof DebuggerClient.Argument) { michael@0: let { position } = aPacketSkeleton[k]; michael@0: outgoingPacket[k] = aPacketSkeleton[k].getArgument(args); michael@0: maxPosition = Math.max(position, maxPosition); michael@0: } else { michael@0: outgoingPacket[k] = aPacketSkeleton[k]; michael@0: } michael@0: } michael@0: michael@0: if (before) { michael@0: outgoingPacket = before.call(this, outgoingPacket); michael@0: } michael@0: michael@0: this.request(outgoingPacket, DevToolsUtils.makeInfallible(function (aResponse) { michael@0: if (after) { michael@0: let { from } = aResponse; michael@0: aResponse = after.call(this, aResponse); michael@0: if (!aResponse.from) { michael@0: aResponse.from = from; michael@0: } michael@0: } michael@0: michael@0: // The callback is always the last parameter. michael@0: let thisCallback = args[maxPosition + 1]; michael@0: if (thisCallback) { michael@0: thisCallback(aResponse); michael@0: } michael@0: michael@0: if (histogram) { michael@0: histogram.add(+new Date - startTime); michael@0: } michael@0: }.bind(this), "DebuggerClient.requester request callback")); michael@0: michael@0: }, "DebuggerClient.requester"); michael@0: }; michael@0: michael@0: function args(aPos) { michael@0: return new DebuggerClient.Argument(aPos); michael@0: } michael@0: michael@0: DebuggerClient.Argument = function (aPosition) { michael@0: this.position = aPosition; michael@0: }; michael@0: michael@0: DebuggerClient.Argument.prototype.getArgument = function (aParams) { michael@0: if (!(this.position in aParams)) { michael@0: throw new Error("Bad index into params: " + this.position); michael@0: } michael@0: return aParams[this.position]; michael@0: }; michael@0: michael@0: DebuggerClient.prototype = { michael@0: /** michael@0: * Connect to the server and start exchanging protocol messages. michael@0: * michael@0: * @param aOnConnected function michael@0: * If specified, will be called when the greeting packet is michael@0: * received from the debugging server. michael@0: */ michael@0: connect: function (aOnConnected) { michael@0: this.addOneTimeListener("connected", (aName, aApplicationType, aTraits) => { michael@0: this.traits = aTraits; michael@0: if (aOnConnected) { michael@0: aOnConnected(aApplicationType, aTraits); michael@0: } michael@0: }); michael@0: michael@0: this._transport.ready(); michael@0: }, michael@0: michael@0: /** michael@0: * Shut down communication with the debugging server. michael@0: * michael@0: * @param aOnClosed function michael@0: * If specified, will be called when the debugging connection michael@0: * has been closed. michael@0: */ michael@0: close: function (aOnClosed) { michael@0: // Disable detach event notifications, because event handlers will be in a michael@0: // cleared scope by the time they run. michael@0: this._eventsEnabled = false; michael@0: michael@0: if (aOnClosed) { michael@0: this.addOneTimeListener('closed', function (aEvent) { michael@0: aOnClosed(); michael@0: }); michael@0: } michael@0: michael@0: const detachClients = (clientMap, next) => { michael@0: const clients = clientMap.values(); michael@0: const total = clientMap.size; michael@0: let numFinished = 0; michael@0: michael@0: if (total == 0) { michael@0: next(); michael@0: return; michael@0: } michael@0: michael@0: for (let client of clients) { michael@0: let method = client instanceof WebConsoleClient ? "close" : "detach"; michael@0: client[method](() => { michael@0: if (++numFinished === total) { michael@0: clientMap.clear(); michael@0: next(); michael@0: } michael@0: }); michael@0: } michael@0: }; michael@0: michael@0: detachClients(this._consoleClients, () => { michael@0: detachClients(this._threadClients, () => { michael@0: detachClients(this._tabClients, () => { michael@0: detachClients(this._addonClients, () => { michael@0: this._transport.close(); michael@0: this._transport = null; michael@0: }); michael@0: }); michael@0: }); michael@0: }); michael@0: }, michael@0: michael@0: /* michael@0: * This function exists only to preserve DebuggerClient's interface; michael@0: * new code should say 'client.mainRoot.listTabs()'. michael@0: */ michael@0: listTabs: function (aOnResponse) { return this.mainRoot.listTabs(aOnResponse); }, michael@0: michael@0: /* michael@0: * This function exists only to preserve DebuggerClient's interface; michael@0: * new code should say 'client.mainRoot.listAddons()'. michael@0: */ michael@0: listAddons: function (aOnResponse) { return this.mainRoot.listAddons(aOnResponse); }, michael@0: michael@0: /** michael@0: * Attach to a tab actor. michael@0: * michael@0: * @param string aTabActor michael@0: * The actor ID for the tab to attach. michael@0: * @param function aOnResponse michael@0: * Called with the response packet and a TabClient michael@0: * (which will be undefined on error). michael@0: */ michael@0: attachTab: function (aTabActor, aOnResponse) { michael@0: if (this._tabClients.has(aTabActor)) { michael@0: let cachedTab = this._tabClients.get(aTabActor); michael@0: let cachedResponse = { michael@0: cacheEnabled: cachedTab.cacheEnabled, michael@0: javascriptEnabled: cachedTab.javascriptEnabled, michael@0: traits: cachedTab.traits, michael@0: }; michael@0: setTimeout(() => aOnResponse(cachedResponse, cachedTab), 0); michael@0: return; michael@0: } michael@0: michael@0: let packet = { michael@0: to: aTabActor, michael@0: type: "attach" michael@0: }; michael@0: this.request(packet, (aResponse) => { michael@0: let tabClient; michael@0: if (!aResponse.error) { michael@0: tabClient = new TabClient(this, aResponse); michael@0: this._tabClients.set(aTabActor, tabClient); michael@0: } michael@0: aOnResponse(aResponse, tabClient); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Attach to an addon actor. michael@0: * michael@0: * @param string aAddonActor michael@0: * The actor ID for the addon to attach. michael@0: * @param function aOnResponse michael@0: * Called with the response packet and a AddonClient michael@0: * (which will be undefined on error). michael@0: */ michael@0: attachAddon: function DC_attachAddon(aAddonActor, aOnResponse) { michael@0: let packet = { michael@0: to: aAddonActor, michael@0: type: "attach" michael@0: }; michael@0: this.request(packet, aResponse => { michael@0: let addonClient; michael@0: if (!aResponse.error) { michael@0: addonClient = new AddonClient(this, aAddonActor); michael@0: this._addonClients[aAddonActor] = addonClient; michael@0: this.activeAddon = addonClient; michael@0: } michael@0: aOnResponse(aResponse, addonClient); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Attach to a Web Console actor. michael@0: * michael@0: * @param string aConsoleActor michael@0: * The ID for the console actor to attach to. michael@0: * @param array aListeners michael@0: * The console listeners you want to start. michael@0: * @param function aOnResponse michael@0: * Called with the response packet and a WebConsoleClient michael@0: * instance (which will be undefined on error). michael@0: */ michael@0: attachConsole: michael@0: function (aConsoleActor, aListeners, aOnResponse) { michael@0: let packet = { michael@0: to: aConsoleActor, michael@0: type: "startListeners", michael@0: listeners: aListeners, michael@0: }; michael@0: michael@0: this.request(packet, (aResponse) => { michael@0: let consoleClient; michael@0: if (!aResponse.error) { michael@0: if (this._consoleClients.has(aConsoleActor)) { michael@0: consoleClient = this._consoleClients.get(aConsoleActor); michael@0: } else { michael@0: consoleClient = new WebConsoleClient(this, aResponse); michael@0: this._consoleClients.set(aConsoleActor, consoleClient); michael@0: } michael@0: } michael@0: aOnResponse(aResponse, consoleClient); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Attach to a global-scoped thread actor for chrome debugging. michael@0: * michael@0: * @param string aThreadActor michael@0: * The actor ID for the thread to attach. michael@0: * @param function aOnResponse michael@0: * Called with the response packet and a ThreadClient michael@0: * (which will be undefined on error). michael@0: * @param object aOptions michael@0: * Configuration options. michael@0: * - useSourceMaps: whether to use source maps or not. michael@0: */ michael@0: attachThread: function (aThreadActor, aOnResponse, aOptions={}) { michael@0: if (this._threadClients.has(aThreadActor)) { michael@0: setTimeout(() => aOnResponse({}, this._threadClients.get(aThreadActor)), 0); michael@0: return; michael@0: } michael@0: michael@0: let packet = { michael@0: to: aThreadActor, michael@0: type: "attach", michael@0: options: aOptions michael@0: }; michael@0: this.request(packet, (aResponse) => { michael@0: if (!aResponse.error) { michael@0: var threadClient = new ThreadClient(this, aThreadActor); michael@0: this._threadClients.set(aThreadActor, threadClient); michael@0: } michael@0: aOnResponse(aResponse, threadClient); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Attach to a trace actor. michael@0: * michael@0: * @param string aTraceActor michael@0: * The actor ID for the tracer to attach. michael@0: * @param function aOnResponse michael@0: * Called with the response packet and a TraceClient michael@0: * (which will be undefined on error). michael@0: */ michael@0: attachTracer: function (aTraceActor, aOnResponse) { michael@0: if (this._tracerClients.has(aTraceActor)) { michael@0: setTimeout(() => aOnResponse({}, this._tracerClients.get(aTraceActor)), 0); michael@0: return; michael@0: } michael@0: michael@0: let packet = { michael@0: to: aTraceActor, michael@0: type: "attach" michael@0: }; michael@0: this.request(packet, (aResponse) => { michael@0: if (!aResponse.error) { michael@0: var traceClient = new TraceClient(this, aTraceActor); michael@0: this._tracerClients.set(aTraceActor, traceClient); michael@0: } michael@0: aOnResponse(aResponse, traceClient); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Release an object actor. michael@0: * michael@0: * @param string aActor michael@0: * The actor ID to send the request to. michael@0: * @param aOnResponse function michael@0: * If specified, will be called with the response packet when michael@0: * debugging server responds. michael@0: */ michael@0: release: DebuggerClient.requester({ michael@0: to: args(0), michael@0: type: "release" michael@0: }, { michael@0: telemetry: "RELEASE" michael@0: }), michael@0: michael@0: /** michael@0: * Send a request to the debugging server. michael@0: * michael@0: * @param aRequest object michael@0: * A JSON packet to send to the debugging server. michael@0: * @param aOnResponse function michael@0: * If specified, will be called with the response packet when michael@0: * debugging server responds. michael@0: */ michael@0: request: function (aRequest, aOnResponse) { michael@0: if (!this.mainRoot) { michael@0: throw Error("Have not yet received a hello packet from the server."); michael@0: } michael@0: if (!aRequest.to) { michael@0: let type = aRequest.type || ""; michael@0: throw Error("'" + type + "' request packet has no destination."); michael@0: } michael@0: michael@0: this._pendingRequests.push({ to: aRequest.to, michael@0: request: aRequest, michael@0: onResponse: aOnResponse }); michael@0: this._sendRequests(); michael@0: }, michael@0: michael@0: /** michael@0: * Send pending requests to any actors that don't already have an michael@0: * active request. michael@0: */ michael@0: _sendRequests: function () { michael@0: this._pendingRequests = this._pendingRequests.filter((request) => { michael@0: if (this._activeRequests.has(request.to)) { michael@0: return true; michael@0: } michael@0: michael@0: this.expectReply(request.to, request.onResponse); michael@0: this._transport.send(request.request); michael@0: michael@0: return false; michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Arrange to hand the next reply from |aActor| to |aHandler|. michael@0: * michael@0: * DebuggerClient.prototype.request usually takes care of establishing michael@0: * the handler for a given request, but in rare cases (well, greetings michael@0: * from new root actors, is the only case at the moment) we must be michael@0: * prepared for a "reply" that doesn't correspond to any request we sent. michael@0: */ michael@0: expectReply: function (aActor, aHandler) { michael@0: if (this._activeRequests.has(aActor)) { michael@0: throw Error("clashing handlers for next reply from " + uneval(aActor)); michael@0: } michael@0: this._activeRequests.set(aActor, aHandler); michael@0: }, michael@0: michael@0: // Transport hooks. michael@0: michael@0: /** michael@0: * Called by DebuggerTransport to dispatch incoming packets as appropriate. michael@0: * michael@0: * @param aPacket object michael@0: * The incoming packet. michael@0: * @param aIgnoreCompatibility boolean michael@0: * Set true to not pass the packet through the compatibility layer. michael@0: */ michael@0: onPacket: function (aPacket, aIgnoreCompatibility=false) { michael@0: let packet = aIgnoreCompatibility michael@0: ? aPacket michael@0: : this.compat.onPacket(aPacket); michael@0: michael@0: resolve(packet).then(aPacket => { michael@0: if (!aPacket.from) { michael@0: DevToolsUtils.reportException( michael@0: "onPacket", michael@0: new Error("Server did not specify an actor, dropping packet: " + michael@0: JSON.stringify(aPacket))); michael@0: return; michael@0: } michael@0: michael@0: // If we have a registered Front for this actor, let it handle the packet michael@0: // and skip all the rest of this unpleasantness. michael@0: let front = this.getActor(aPacket.from); michael@0: if (front) { michael@0: front.onPacket(aPacket); michael@0: return; michael@0: } michael@0: michael@0: let onResponse; michael@0: // See if we have a handler function waiting for a reply from this michael@0: // actor. (Don't count unsolicited notifications or pauses as michael@0: // replies.) michael@0: if (this._activeRequests.has(aPacket.from) && michael@0: !(aPacket.type in UnsolicitedNotifications) && michael@0: !(aPacket.type == ThreadStateTypes.paused && michael@0: aPacket.why.type in UnsolicitedPauses)) { michael@0: onResponse = this._activeRequests.get(aPacket.from); michael@0: this._activeRequests.delete(aPacket.from); michael@0: } michael@0: michael@0: // Packets that indicate thread state changes get special treatment. michael@0: if (aPacket.type in ThreadStateTypes && michael@0: this._threadClients.has(aPacket.from)) { michael@0: this._threadClients.get(aPacket.from)._onThreadState(aPacket); michael@0: } michael@0: // On navigation the server resumes, so the client must resume as well. michael@0: // We achieve that by generating a fake resumption packet that triggers michael@0: // the client's thread state change listeners. michael@0: if (aPacket.type == UnsolicitedNotifications.tabNavigated && michael@0: this._tabClients.has(aPacket.from) && michael@0: this._tabClients.get(aPacket.from).thread) { michael@0: let thread = this._tabClients.get(aPacket.from).thread; michael@0: let resumption = { from: thread._actor, type: "resumed" }; michael@0: thread._onThreadState(resumption); michael@0: } michael@0: // Only try to notify listeners on events, not responses to requests michael@0: // that lack a packet type. michael@0: if (aPacket.type) { michael@0: this.notify(aPacket.type, aPacket); michael@0: } michael@0: michael@0: if (onResponse) { michael@0: onResponse(aPacket); michael@0: } michael@0: michael@0: this._sendRequests(); michael@0: }, ex => DevToolsUtils.reportException("onPacket handler", ex)); michael@0: }, michael@0: michael@0: /** michael@0: * Called by DebuggerTransport when the underlying stream is closed. michael@0: * michael@0: * @param aStatus nsresult michael@0: * The status code that corresponds to the reason for closing michael@0: * the stream. michael@0: */ michael@0: onClosed: function (aStatus) { michael@0: this.notify("closed"); michael@0: }, michael@0: michael@0: /** michael@0: * Actor lifetime management, echos the server's actor pools. michael@0: */ michael@0: __pools: null, michael@0: get _pools() { michael@0: if (this.__pools) { michael@0: return this.__pools; michael@0: } michael@0: this.__pools = new Set(); michael@0: return this.__pools; michael@0: }, michael@0: michael@0: addActorPool: function (pool) { michael@0: this._pools.add(pool); michael@0: }, michael@0: removeActorPool: function (pool) { michael@0: this._pools.delete(pool); michael@0: }, michael@0: getActor: function (actorID) { michael@0: let pool = this.poolFor(actorID); michael@0: return pool ? pool.get(actorID) : null; michael@0: }, michael@0: michael@0: poolFor: function (actorID) { michael@0: for (let pool of this._pools) { michael@0: if (pool.has(actorID)) return pool; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * Currently attached addon. michael@0: */ michael@0: activeAddon: null michael@0: } michael@0: michael@0: eventSource(DebuggerClient.prototype); michael@0: michael@0: // Constants returned by `FeatureCompatibilityShim.onPacketTest`. michael@0: const SUPPORTED = 1; michael@0: const NOT_SUPPORTED = 2; michael@0: const SKIP = 3; michael@0: michael@0: /** michael@0: * This object provides an abstraction layer over all of our backwards michael@0: * compatibility, feature detection, and shimming with regards to the remote michael@0: * debugging prototcol. michael@0: * michael@0: * @param aFeatures Array michael@0: * An array of FeatureCompatibilityShim objects michael@0: */ michael@0: function ProtocolCompatibility(aClient, aFeatures) { michael@0: this._client = aClient; michael@0: this._featuresWithUnknownSupport = new Set(aFeatures); michael@0: this._featuresWithoutSupport = new Set(); michael@0: michael@0: this._featureDeferreds = Object.create(null) michael@0: for (let f of aFeatures) { michael@0: this._featureDeferreds[f.name] = defer(); michael@0: } michael@0: } michael@0: michael@0: ProtocolCompatibility.prototype = { michael@0: /** michael@0: * Returns a promise that resolves to true if the RDP supports the feature, michael@0: * and is rejected otherwise. michael@0: * michael@0: * @param aFeatureName String michael@0: * The name of the feature we are testing. michael@0: */ michael@0: supportsFeature: function (aFeatureName) { michael@0: return this._featureDeferreds[aFeatureName].promise; michael@0: }, michael@0: michael@0: /** michael@0: * Force a feature to be considered unsupported. michael@0: * michael@0: * @param aFeatureName String michael@0: * The name of the feature we are testing. michael@0: */ michael@0: rejectFeature: function (aFeatureName) { michael@0: this._featureDeferreds[aFeatureName].reject(false); michael@0: }, michael@0: michael@0: /** michael@0: * Called for each packet received over the RDP from the server. Tests for michael@0: * protocol features and shims packets to support needed features. michael@0: * michael@0: * @param aPacket Object michael@0: * Packet received over the RDP from the server. michael@0: */ michael@0: onPacket: function (aPacket) { michael@0: this._detectFeatures(aPacket); michael@0: return this._shimPacket(aPacket); michael@0: }, michael@0: michael@0: /** michael@0: * For each of the features we don't know whether the server supports or not, michael@0: * attempt to detect support based on the packet we just received. michael@0: */ michael@0: _detectFeatures: function (aPacket) { michael@0: for (let feature of this._featuresWithUnknownSupport) { michael@0: try { michael@0: switch (feature.onPacketTest(aPacket)) { michael@0: case SKIP: michael@0: break; michael@0: case SUPPORTED: michael@0: this._featuresWithUnknownSupport.delete(feature); michael@0: this._featureDeferreds[feature.name].resolve(true); michael@0: break; michael@0: case NOT_SUPPORTED: michael@0: this._featuresWithUnknownSupport.delete(feature); michael@0: this._featuresWithoutSupport.add(feature); michael@0: this.rejectFeature(feature.name); michael@0: break; michael@0: default: michael@0: DevToolsUtils.reportException( michael@0: "PC__detectFeatures", michael@0: new Error("Bad return value from `onPacketTest` for feature '" michael@0: + feature.name + "'")); michael@0: } michael@0: } catch (ex) { michael@0: DevToolsUtils.reportException("PC__detectFeatures", ex); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Go through each of the features that we know are unsupported by the current michael@0: * server and attempt to shim support. michael@0: */ michael@0: _shimPacket: function (aPacket) { michael@0: let extraPackets = []; michael@0: michael@0: let loop = function (aFeatures, aPacket) { michael@0: if (aFeatures.length === 0) { michael@0: for (let packet of extraPackets) { michael@0: this._client.onPacket(packet, true); michael@0: } michael@0: return aPacket; michael@0: } else { michael@0: let replacePacket = function (aNewPacket) { michael@0: return aNewPacket; michael@0: }; michael@0: let extraPacket = function (aExtraPacket) { michael@0: extraPackets.push(aExtraPacket); michael@0: return aPacket; michael@0: }; michael@0: let keepPacket = function () { michael@0: return aPacket; michael@0: }; michael@0: let newPacket = aFeatures[0].translatePacket(aPacket, michael@0: replacePacket, michael@0: extraPacket, michael@0: keepPacket); michael@0: return resolve(newPacket).then(loop.bind(null, aFeatures.slice(1))); michael@0: } michael@0: }.bind(this); michael@0: michael@0: return loop([f for (f of this._featuresWithoutSupport)], michael@0: aPacket); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Interface defining what methods a feature compatibility shim should have. michael@0: */ michael@0: const FeatureCompatibilityShim = { michael@0: // The name of the feature michael@0: name: null, michael@0: michael@0: /** michael@0: * Takes a packet and returns boolean (or promise of boolean) indicating michael@0: * whether the server supports the RDP feature we are possibly shimming. michael@0: */ michael@0: onPacketTest: function (aPacket) { michael@0: throw new Error("Not yet implemented"); michael@0: }, michael@0: michael@0: /** michael@0: * Takes a packet actually sent from the server and decides whether to replace michael@0: * it with a new packet, create an extra packet, or keep it. michael@0: */ michael@0: translatePacket: function (aPacket, aReplacePacket, aExtraPacket, aKeepPacket) { michael@0: throw new Error("Not yet implemented"); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Creates a tab client for the remote debugging protocol server. This client michael@0: * is a front to the tab actor created in the server side, hiding the protocol michael@0: * details in a traditional JavaScript API. michael@0: * michael@0: * @param aClient DebuggerClient michael@0: * The debugger client parent. michael@0: * @param aForm object michael@0: * The protocol form for this tab. michael@0: */ michael@0: function TabClient(aClient, aForm) { michael@0: this.client = aClient; michael@0: this._actor = aForm.from; michael@0: this._threadActor = aForm.threadActor; michael@0: this.javascriptEnabled = aForm.javascriptEnabled; michael@0: this.cacheEnabled = aForm.cacheEnabled; michael@0: this.thread = null; michael@0: this.request = this.client.request; michael@0: this.traits = aForm.traits || {}; michael@0: } michael@0: michael@0: TabClient.prototype = { michael@0: get actor() { return this._actor }, michael@0: get _transport() { return this.client._transport; }, michael@0: michael@0: /** michael@0: * Attach to a thread actor. michael@0: * michael@0: * @param object aOptions michael@0: * Configuration options. michael@0: * - useSourceMaps: whether to use source maps or not. michael@0: * @param function aOnResponse michael@0: * Called with the response packet and a ThreadClient michael@0: * (which will be undefined on error). michael@0: */ michael@0: attachThread: function(aOptions={}, aOnResponse) { michael@0: if (this.thread) { michael@0: setTimeout(() => aOnResponse({}, this.thread), 0); michael@0: return; michael@0: } michael@0: michael@0: let packet = { michael@0: to: this._threadActor, michael@0: type: "attach", michael@0: options: aOptions michael@0: }; michael@0: this.request(packet, (aResponse) => { michael@0: if (!aResponse.error) { michael@0: this.thread = new ThreadClient(this, this._threadActor); michael@0: this.client._threadClients.set(this._threadActor, this.thread); michael@0: } michael@0: aOnResponse(aResponse, this.thread); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Detach the client from the tab actor. michael@0: * michael@0: * @param function aOnResponse michael@0: * Called with the response packet. michael@0: */ michael@0: detach: DebuggerClient.requester({ michael@0: type: "detach" michael@0: }, { michael@0: before: function (aPacket) { michael@0: if (this.thread) { michael@0: this.thread.detach(); michael@0: } michael@0: return aPacket; michael@0: }, michael@0: after: function (aResponse) { michael@0: this.client._tabClients.delete(this.actor); michael@0: return aResponse; michael@0: }, michael@0: telemetry: "TABDETACH" michael@0: }), michael@0: michael@0: /** michael@0: * Reload the page in this tab. michael@0: */ michael@0: reload: DebuggerClient.requester({ michael@0: type: "reload" michael@0: }, { michael@0: telemetry: "RELOAD" michael@0: }), michael@0: michael@0: /** michael@0: * Navigate to another URL. michael@0: * michael@0: * @param string url michael@0: * The URL to navigate to. michael@0: */ michael@0: navigateTo: DebuggerClient.requester({ michael@0: type: "navigateTo", michael@0: url: args(0) michael@0: }, { michael@0: telemetry: "NAVIGATETO" michael@0: }), michael@0: michael@0: /** michael@0: * Reconfigure the tab actor. michael@0: * michael@0: * @param object aOptions michael@0: * A dictionary object of the new options to use in the tab actor. michael@0: * @param function aOnResponse michael@0: * Called with the response packet. michael@0: */ michael@0: reconfigure: DebuggerClient.requester({ michael@0: type: "reconfigure", michael@0: options: args(0) michael@0: }, { michael@0: telemetry: "RECONFIGURETAB" michael@0: }), michael@0: }; michael@0: michael@0: eventSource(TabClient.prototype); michael@0: michael@0: function AddonClient(aClient, aActor) { michael@0: this._client = aClient; michael@0: this._actor = aActor; michael@0: this.request = this._client.request; michael@0: } michael@0: michael@0: AddonClient.prototype = { michael@0: get actor() { return this._actor; }, michael@0: get _transport() { return this._client._transport; }, michael@0: michael@0: /** michael@0: * Detach the client from the addon actor. michael@0: * michael@0: * @param function aOnResponse michael@0: * Called with the response packet. michael@0: */ michael@0: detach: DebuggerClient.requester({ michael@0: type: "detach" michael@0: }, { michael@0: after: function(aResponse) { michael@0: if (this._client.activeAddon === this._client._addonClients[this.actor]) { michael@0: this._client.activeAddon = null michael@0: } michael@0: delete this._client._addonClients[this.actor]; michael@0: return aResponse; michael@0: }, michael@0: telemetry: "ADDONDETACH" michael@0: }) michael@0: }; michael@0: michael@0: /** michael@0: * A RootClient object represents a root actor on the server. Each michael@0: * DebuggerClient keeps a RootClient instance representing the root actor michael@0: * for the initial connection; DebuggerClient's 'listTabs' and michael@0: * 'listChildProcesses' methods forward to that root actor. michael@0: * michael@0: * @param aClient object michael@0: * The client connection to which this actor belongs. michael@0: * @param aGreeting string michael@0: * The greeting packet from the root actor we're to represent. michael@0: * michael@0: * Properties of a RootClient instance: michael@0: * michael@0: * @property actor string michael@0: * The name of this child's root actor. michael@0: * @property applicationType string michael@0: * The application type, as given in the root actor's greeting packet. michael@0: * @property traits object michael@0: * The traits object, as given in the root actor's greeting packet. michael@0: */ michael@0: function RootClient(aClient, aGreeting) { michael@0: this._client = aClient; michael@0: this.actor = aGreeting.from; michael@0: this.applicationType = aGreeting.applicationType; michael@0: this.traits = aGreeting.traits; michael@0: } michael@0: michael@0: RootClient.prototype = { michael@0: constructor: RootClient, michael@0: michael@0: /** michael@0: * List the open tabs. michael@0: * michael@0: * @param function aOnResponse michael@0: * Called with the response packet. michael@0: */ michael@0: listTabs: DebuggerClient.requester({ type: "listTabs" }, michael@0: { telemetry: "LISTTABS" }), michael@0: michael@0: /** michael@0: * List the installed addons. michael@0: * michael@0: * @param function aOnResponse michael@0: * Called with the response packet. michael@0: */ michael@0: listAddons: DebuggerClient.requester({ type: "listAddons" }, michael@0: { telemetry: "LISTADDONS" }), michael@0: michael@0: /* michael@0: * Methods constructed by DebuggerClient.requester require these forwards michael@0: * on their 'this'. michael@0: */ michael@0: get _transport() { return this._client._transport; }, michael@0: get request() { return this._client.request; } michael@0: }; michael@0: michael@0: /** michael@0: * Creates a thread client for the remote debugging protocol server. This client michael@0: * is a front to the thread actor created in the server side, hiding the michael@0: * protocol details in a traditional JavaScript API. michael@0: * michael@0: * @param aClient DebuggerClient|TabClient michael@0: * The parent of the thread (tab for tab-scoped debuggers, DebuggerClient michael@0: * for chrome debuggers). michael@0: * @param aActor string michael@0: * The actor ID for this thread. michael@0: */ michael@0: function ThreadClient(aClient, aActor) { michael@0: this._parent = aClient; michael@0: this.client = aClient instanceof DebuggerClient ? aClient : aClient.client; michael@0: this._actor = aActor; michael@0: this._frameCache = []; michael@0: this._scriptCache = {}; michael@0: this._pauseGrips = {}; michael@0: this._threadGrips = {}; michael@0: this.request = this.client.request; michael@0: } michael@0: michael@0: ThreadClient.prototype = { michael@0: _state: "paused", michael@0: get state() { return this._state; }, michael@0: get paused() { return this._state === "paused"; }, michael@0: michael@0: _pauseOnExceptions: false, michael@0: _ignoreCaughtExceptions: false, michael@0: _pauseOnDOMEvents: null, michael@0: michael@0: _actor: null, michael@0: get actor() { return this._actor; }, michael@0: michael@0: get compat() { return this.client.compat; }, michael@0: get _transport() { return this.client._transport; }, michael@0: michael@0: _assertPaused: function (aCommand) { michael@0: if (!this.paused) { michael@0: throw Error(aCommand + " command sent while not paused. Currently " + this._state); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Resume a paused thread. If the optional aLimit parameter is present, then michael@0: * the thread will also pause when that limit is reached. michael@0: * michael@0: * @param [optional] object aLimit michael@0: * An object with a type property set to the appropriate limit (next, michael@0: * step, or finish) per the remote debugging protocol specification. michael@0: * Use null to specify no limit. michael@0: * @param function aOnResponse michael@0: * Called with the response packet. michael@0: */ michael@0: _doResume: DebuggerClient.requester({ michael@0: type: "resume", michael@0: resumeLimit: args(0) michael@0: }, { michael@0: before: function (aPacket) { michael@0: this._assertPaused("resume"); michael@0: michael@0: // Put the client in a tentative "resuming" state so we can prevent michael@0: // further requests that should only be sent in the paused state. michael@0: this._state = "resuming"; michael@0: michael@0: if (this._pauseOnExceptions) { michael@0: aPacket.pauseOnExceptions = this._pauseOnExceptions; michael@0: } michael@0: if (this._ignoreCaughtExceptions) { michael@0: aPacket.ignoreCaughtExceptions = this._ignoreCaughtExceptions; michael@0: } michael@0: if (this._pauseOnDOMEvents) { michael@0: aPacket.pauseOnDOMEvents = this._pauseOnDOMEvents; michael@0: } michael@0: return aPacket; michael@0: }, michael@0: after: function (aResponse) { michael@0: if (aResponse.error) { michael@0: // There was an error resuming, back to paused state. michael@0: this._state = "paused"; michael@0: } michael@0: return aResponse; michael@0: }, michael@0: telemetry: "RESUME" michael@0: }), michael@0: michael@0: /** michael@0: * Reconfigure the thread actor. michael@0: * michael@0: * @param object aOptions michael@0: * A dictionary object of the new options to use in the thread actor. michael@0: * @param function aOnResponse michael@0: * Called with the response packet. michael@0: */ michael@0: reconfigure: DebuggerClient.requester({ michael@0: type: "reconfigure", michael@0: options: args(0) michael@0: }, { michael@0: telemetry: "RECONFIGURETHREAD" michael@0: }), michael@0: michael@0: /** michael@0: * Resume a paused thread. michael@0: */ michael@0: resume: function (aOnResponse) { michael@0: this._doResume(null, aOnResponse); michael@0: }, michael@0: michael@0: /** michael@0: * Step over a function call. michael@0: * michael@0: * @param function aOnResponse michael@0: * Called with the response packet. michael@0: */ michael@0: stepOver: function (aOnResponse) { michael@0: this._doResume({ type: "next" }, aOnResponse); michael@0: }, michael@0: michael@0: /** michael@0: * Step into a function call. michael@0: * michael@0: * @param function aOnResponse michael@0: * Called with the response packet. michael@0: */ michael@0: stepIn: function (aOnResponse) { michael@0: this._doResume({ type: "step" }, aOnResponse); michael@0: }, michael@0: michael@0: /** michael@0: * Step out of a function call. michael@0: * michael@0: * @param function aOnResponse michael@0: * Called with the response packet. michael@0: */ michael@0: stepOut: function (aOnResponse) { michael@0: this._doResume({ type: "finish" }, aOnResponse); michael@0: }, michael@0: michael@0: /** michael@0: * Interrupt a running thread. michael@0: * michael@0: * @param function aOnResponse michael@0: * Called with the response packet. michael@0: */ michael@0: interrupt: DebuggerClient.requester({ michael@0: type: "interrupt" michael@0: }, { michael@0: telemetry: "INTERRUPT" michael@0: }), michael@0: michael@0: /** michael@0: * Enable or disable pausing when an exception is thrown. michael@0: * michael@0: * @param boolean aFlag michael@0: * Enables pausing if true, disables otherwise. michael@0: * @param function aOnResponse michael@0: * Called with the response packet. michael@0: */ michael@0: pauseOnExceptions: function (aPauseOnExceptions, michael@0: aIgnoreCaughtExceptions, michael@0: aOnResponse) { michael@0: this._pauseOnExceptions = aPauseOnExceptions; michael@0: this._ignoreCaughtExceptions = aIgnoreCaughtExceptions; michael@0: michael@0: // If the debuggee is paused, we have to send the flag via a reconfigure michael@0: // request. michael@0: if (this.paused) { michael@0: this.reconfigure({ michael@0: pauseOnExceptions: aPauseOnExceptions, michael@0: ignoreCaughtExceptions: aIgnoreCaughtExceptions michael@0: }, aOnResponse); michael@0: return; michael@0: } michael@0: // Otherwise send the flag using a standard resume request. michael@0: this.interrupt(aResponse => { michael@0: if (aResponse.error) { michael@0: // Can't continue if pausing failed. michael@0: aOnResponse(aResponse); michael@0: return; michael@0: } michael@0: this.resume(aOnResponse); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Enable pausing when the specified DOM events are triggered. Disabling michael@0: * pausing on an event can be realized by calling this method with the updated michael@0: * array of events that doesn't contain it. michael@0: * michael@0: * @param array|string events michael@0: * An array of strings, representing the DOM event types to pause on, michael@0: * or "*" to pause on all DOM events. Pass an empty array to michael@0: * completely disable pausing on DOM events. michael@0: * @param function onResponse michael@0: * Called with the response packet in a future turn of the event loop. michael@0: */ michael@0: pauseOnDOMEvents: function (events, onResponse) { michael@0: this._pauseOnDOMEvents = events; michael@0: // If the debuggee is paused, the value of the array will be communicated in michael@0: // the next resumption. Otherwise we have to force a pause in order to send michael@0: // the array. michael@0: if (this.paused) { michael@0: setTimeout(() => onResponse({}), 0); michael@0: return; michael@0: } michael@0: this.interrupt(response => { michael@0: // Can't continue if pausing failed. michael@0: if (response.error) { michael@0: onResponse(response); michael@0: return; michael@0: } michael@0: this.resume(onResponse); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Send a clientEvaluate packet to the debuggee. Response michael@0: * will be a resume packet. michael@0: * michael@0: * @param string aFrame michael@0: * The actor ID of the frame where the evaluation should take place. michael@0: * @param string aExpression michael@0: * The expression that will be evaluated in the scope of the frame michael@0: * above. michael@0: * @param function aOnResponse michael@0: * Called with the response packet. michael@0: */ michael@0: eval: DebuggerClient.requester({ michael@0: type: "clientEvaluate", michael@0: frame: args(0), michael@0: expression: args(1) michael@0: }, { michael@0: before: function (aPacket) { michael@0: this._assertPaused("eval"); michael@0: // Put the client in a tentative "resuming" state so we can prevent michael@0: // further requests that should only be sent in the paused state. michael@0: this._state = "resuming"; michael@0: return aPacket; michael@0: }, michael@0: after: function (aResponse) { michael@0: if (aResponse.error) { michael@0: // There was an error resuming, back to paused state. michael@0: this._state = "paused"; michael@0: } michael@0: return aResponse; michael@0: }, michael@0: telemetry: "CLIENTEVALUATE" michael@0: }), michael@0: michael@0: /** michael@0: * Detach from the thread actor. michael@0: * michael@0: * @param function aOnResponse michael@0: * Called with the response packet. michael@0: */ michael@0: detach: DebuggerClient.requester({ michael@0: type: "detach" michael@0: }, { michael@0: after: function (aResponse) { michael@0: this.client._threadClients.delete(this.actor); michael@0: this._parent.thread = null; michael@0: return aResponse; michael@0: }, michael@0: telemetry: "THREADDETACH" michael@0: }), michael@0: michael@0: /** michael@0: * Request to set a breakpoint in the specified location. michael@0: * michael@0: * @param object aLocation michael@0: * The source location object where the breakpoint will be set. michael@0: * @param function aOnResponse michael@0: * Called with the thread's response. michael@0: */ michael@0: setBreakpoint: function ({ url, line, column, condition }, aOnResponse) { michael@0: // A helper function that sets the breakpoint. michael@0: let doSetBreakpoint = function (aCallback) { michael@0: const location = { michael@0: url: url, michael@0: line: line, michael@0: column: column michael@0: }; michael@0: michael@0: let packet = { michael@0: to: this._actor, michael@0: type: "setBreakpoint", michael@0: location: location, michael@0: condition: condition michael@0: }; michael@0: this.client.request(packet, function (aResponse) { michael@0: // Ignoring errors, since the user may be setting a breakpoint in a michael@0: // dead script that will reappear on a page reload. michael@0: if (aOnResponse) { michael@0: let root = this.client.mainRoot; michael@0: let bpClient = new BreakpointClient( michael@0: this.client, michael@0: aResponse.actor, michael@0: location, michael@0: root.traits.conditionalBreakpoints ? condition : undefined michael@0: ); michael@0: aOnResponse(aResponse, bpClient); michael@0: } michael@0: if (aCallback) { michael@0: aCallback(); michael@0: } michael@0: }.bind(this)); michael@0: }.bind(this); michael@0: michael@0: // If the debuggee is paused, just set the breakpoint. michael@0: if (this.paused) { michael@0: doSetBreakpoint(); michael@0: return; michael@0: } michael@0: // Otherwise, force a pause in order to set the breakpoint. michael@0: this.interrupt(function (aResponse) { michael@0: if (aResponse.error) { michael@0: // Can't set the breakpoint if pausing failed. michael@0: aOnResponse(aResponse); michael@0: return; michael@0: } michael@0: doSetBreakpoint(this.resume.bind(this)); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Release multiple thread-lifetime object actors. If any pause-lifetime michael@0: * actors are included in the request, a |notReleasable| error will return, michael@0: * but all the thread-lifetime ones will have been released. michael@0: * michael@0: * @param array actors michael@0: * An array with actor IDs to release. michael@0: */ michael@0: releaseMany: DebuggerClient.requester({ michael@0: type: "releaseMany", michael@0: actors: args(0), michael@0: }, { michael@0: telemetry: "RELEASEMANY" michael@0: }), michael@0: michael@0: /** michael@0: * Promote multiple pause-lifetime object actors to thread-lifetime ones. michael@0: * michael@0: * @param array actors michael@0: * An array with actor IDs to promote. michael@0: */ michael@0: threadGrips: DebuggerClient.requester({ michael@0: type: "threadGrips", michael@0: actors: args(0) michael@0: }, { michael@0: telemetry: "THREADGRIPS" michael@0: }), michael@0: michael@0: /** michael@0: * Return the event listeners defined on the page. michael@0: * michael@0: * @param aOnResponse Function michael@0: * Called with the thread's response. michael@0: */ michael@0: eventListeners: DebuggerClient.requester({ michael@0: type: "eventListeners" michael@0: }, { michael@0: telemetry: "EVENTLISTENERS" michael@0: }), michael@0: michael@0: /** michael@0: * Request the loaded sources for the current thread. michael@0: * michael@0: * @param aOnResponse Function michael@0: * Called with the thread's response. michael@0: */ michael@0: getSources: DebuggerClient.requester({ michael@0: type: "sources" michael@0: }, { michael@0: telemetry: "SOURCES" michael@0: }), michael@0: michael@0: _doInterrupted: function (aAction, aError) { michael@0: if (this.paused) { michael@0: aAction(); michael@0: return; michael@0: } michael@0: this.interrupt(function (aResponse) { michael@0: if (aResponse) { michael@0: aError(aResponse); michael@0: return; michael@0: } michael@0: aAction(); michael@0: this.resume(); michael@0: }.bind(this)); michael@0: }, michael@0: michael@0: /** michael@0: * Clear the thread's source script cache. A scriptscleared event michael@0: * will be sent. michael@0: */ michael@0: _clearScripts: function () { michael@0: if (Object.keys(this._scriptCache).length > 0) { michael@0: this._scriptCache = {} michael@0: this.notify("scriptscleared"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Request frames from the callstack for the current thread. michael@0: * michael@0: * @param aStart integer michael@0: * The number of the youngest stack frame to return (the youngest michael@0: * frame is 0). michael@0: * @param aCount integer michael@0: * The maximum number of frames to return, or null to return all michael@0: * frames. michael@0: * @param aOnResponse function michael@0: * Called with the thread's response. michael@0: */ michael@0: getFrames: DebuggerClient.requester({ michael@0: type: "frames", michael@0: start: args(0), michael@0: count: args(1) michael@0: }, { michael@0: telemetry: "FRAMES" michael@0: }), michael@0: michael@0: /** michael@0: * An array of cached frames. Clients can observe the framesadded and michael@0: * framescleared event to keep up to date on changes to this cache, michael@0: * and can fill it using the fillFrames method. michael@0: */ michael@0: get cachedFrames() { return this._frameCache; }, michael@0: michael@0: /** michael@0: * true if there are more stack frames available on the server. michael@0: */ michael@0: get moreFrames() { michael@0: return this.paused && (!this._frameCache || this._frameCache.length == 0 michael@0: || !this._frameCache[this._frameCache.length - 1].oldest); michael@0: }, michael@0: michael@0: /** michael@0: * Ensure that at least aTotal stack frames have been loaded in the michael@0: * ThreadClient's stack frame cache. A framesadded event will be michael@0: * sent when the stack frame cache is updated. michael@0: * michael@0: * @param aTotal number michael@0: * The minimum number of stack frames to be included. michael@0: * @param aCallback function michael@0: * Optional callback function called when frames have been loaded michael@0: * @returns true if a framesadded notification should be expected. michael@0: */ michael@0: fillFrames: function (aTotal, aCallback=noop) { michael@0: this._assertPaused("fillFrames"); michael@0: if (this._frameCache.length >= aTotal) { michael@0: return false; michael@0: } michael@0: michael@0: let numFrames = this._frameCache.length; michael@0: michael@0: this.getFrames(numFrames, aTotal - numFrames, (aResponse) => { michael@0: if (aResponse.error) { michael@0: aCallback(aResponse); michael@0: return; michael@0: } michael@0: michael@0: for each (let frame in aResponse.frames) { michael@0: this._frameCache[frame.depth] = frame; michael@0: } michael@0: michael@0: // If we got as many frames as we asked for, there might be more michael@0: // frames available. michael@0: this.notify("framesadded"); michael@0: michael@0: aCallback(aResponse); michael@0: }); michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Clear the thread's stack frame cache. A framescleared event michael@0: * will be sent. michael@0: */ michael@0: _clearFrames: function () { michael@0: if (this._frameCache.length > 0) { michael@0: this._frameCache = []; michael@0: this.notify("framescleared"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Return a ObjectClient object for the given object grip. michael@0: * michael@0: * @param aGrip object michael@0: * A pause-lifetime object grip returned by the protocol. michael@0: */ michael@0: pauseGrip: function (aGrip) { michael@0: if (aGrip.actor in this._pauseGrips) { michael@0: return this._pauseGrips[aGrip.actor]; michael@0: } michael@0: michael@0: let client = new ObjectClient(this.client, aGrip); michael@0: this._pauseGrips[aGrip.actor] = client; michael@0: return client; michael@0: }, michael@0: michael@0: /** michael@0: * Get or create a long string client, checking the grip client cache if it michael@0: * already exists. michael@0: * michael@0: * @param aGrip Object michael@0: * The long string grip returned by the protocol. michael@0: * @param aGripCacheName String michael@0: * The property name of the grip client cache to check for existing michael@0: * clients in. michael@0: */ michael@0: _longString: function (aGrip, aGripCacheName) { michael@0: if (aGrip.actor in this[aGripCacheName]) { michael@0: return this[aGripCacheName][aGrip.actor]; michael@0: } michael@0: michael@0: let client = new LongStringClient(this.client, aGrip); michael@0: this[aGripCacheName][aGrip.actor] = client; michael@0: return client; michael@0: }, michael@0: michael@0: /** michael@0: * Return an instance of LongStringClient for the given long string grip that michael@0: * is scoped to the current pause. michael@0: * michael@0: * @param aGrip Object michael@0: * The long string grip returned by the protocol. michael@0: */ michael@0: pauseLongString: function (aGrip) { michael@0: return this._longString(aGrip, "_pauseGrips"); michael@0: }, michael@0: michael@0: /** michael@0: * Return an instance of LongStringClient for the given long string grip that michael@0: * is scoped to the thread lifetime. michael@0: * michael@0: * @param aGrip Object michael@0: * The long string grip returned by the protocol. michael@0: */ michael@0: threadLongString: function (aGrip) { michael@0: return this._longString(aGrip, "_threadGrips"); michael@0: }, michael@0: michael@0: /** michael@0: * Clear and invalidate all the grip clients from the given cache. michael@0: * michael@0: * @param aGripCacheName michael@0: * The property name of the grip cache we want to clear. michael@0: */ michael@0: _clearObjectClients: function (aGripCacheName) { michael@0: for each (let grip in this[aGripCacheName]) { michael@0: grip.valid = false; michael@0: } michael@0: this[aGripCacheName] = {}; michael@0: }, michael@0: michael@0: /** michael@0: * Invalidate pause-lifetime grip clients and clear the list of current grip michael@0: * clients. michael@0: */ michael@0: _clearPauseGrips: function () { michael@0: this._clearObjectClients("_pauseGrips"); michael@0: }, michael@0: michael@0: /** michael@0: * Invalidate thread-lifetime grip clients and clear the list of current grip michael@0: * clients. michael@0: */ michael@0: _clearThreadGrips: function () { michael@0: this._clearObjectClients("_threadGrips"); michael@0: }, michael@0: michael@0: /** michael@0: * Handle thread state change by doing necessary cleanup and notifying all michael@0: * registered listeners. michael@0: */ michael@0: _onThreadState: function (aPacket) { michael@0: this._state = ThreadStateTypes[aPacket.type]; michael@0: this._clearFrames(); michael@0: this._clearPauseGrips(); michael@0: aPacket.type === ThreadStateTypes.detached && this._clearThreadGrips(); michael@0: this.client._eventsEnabled && this.notify(aPacket.type, aPacket); michael@0: }, michael@0: michael@0: /** michael@0: * Return an EnvironmentClient instance for the given environment actor form. michael@0: */ michael@0: environment: function (aForm) { michael@0: return new EnvironmentClient(this.client, aForm); michael@0: }, michael@0: michael@0: /** michael@0: * Return an instance of SourceClient for the given source actor form. michael@0: */ michael@0: source: function (aForm) { michael@0: if (aForm.actor in this._threadGrips) { michael@0: return this._threadGrips[aForm.actor]; michael@0: } michael@0: michael@0: return this._threadGrips[aForm.actor] = new SourceClient(this, aForm); michael@0: }, michael@0: michael@0: /** michael@0: * Request the prototype and own properties of mutlipleObjects. michael@0: * michael@0: * @param aOnResponse function michael@0: * Called with the request's response. michael@0: * @param actors [string] michael@0: * List of actor ID of the queried objects. michael@0: */ michael@0: getPrototypesAndProperties: DebuggerClient.requester({ michael@0: type: "prototypesAndProperties", michael@0: actors: args(0) michael@0: }, { michael@0: telemetry: "PROTOTYPESANDPROPERTIES" michael@0: }) michael@0: }; michael@0: michael@0: eventSource(ThreadClient.prototype); michael@0: michael@0: /** michael@0: * Creates a tracing profiler client for the remote debugging protocol michael@0: * server. This client is a front to the trace actor created on the michael@0: * server side, hiding the protocol details in a traditional michael@0: * JavaScript API. michael@0: * michael@0: * @param aClient DebuggerClient michael@0: * The debugger client parent. michael@0: * @param aActor string michael@0: * The actor ID for this thread. michael@0: */ michael@0: function TraceClient(aClient, aActor) { michael@0: this._client = aClient; michael@0: this._actor = aActor; michael@0: this._activeTraces = new Set(); michael@0: this._waitingPackets = new Map(); michael@0: this._expectedPacket = 0; michael@0: this.request = this._client.request; michael@0: } michael@0: michael@0: TraceClient.prototype = { michael@0: get actor() { return this._actor; }, michael@0: get tracing() { return this._activeTraces.size > 0; }, michael@0: michael@0: get _transport() { return this._client._transport; }, michael@0: michael@0: /** michael@0: * Detach from the trace actor. michael@0: */ michael@0: detach: DebuggerClient.requester({ michael@0: type: "detach" michael@0: }, { michael@0: after: function (aResponse) { michael@0: this._client._tracerClients.delete(this.actor); michael@0: return aResponse; michael@0: }, michael@0: telemetry: "TRACERDETACH" michael@0: }), michael@0: michael@0: /** michael@0: * Start a new trace. michael@0: * michael@0: * @param aTrace [string] michael@0: * An array of trace types to be recorded by the new trace. michael@0: * michael@0: * @param aName string michael@0: * The name of the new trace. michael@0: * michael@0: * @param aOnResponse function michael@0: * Called with the request's response. michael@0: */ michael@0: startTrace: DebuggerClient.requester({ michael@0: type: "startTrace", michael@0: name: args(1), michael@0: trace: args(0) michael@0: }, { michael@0: after: function (aResponse) { michael@0: if (aResponse.error) { michael@0: return aResponse; michael@0: } michael@0: michael@0: if (!this.tracing) { michael@0: this._waitingPackets.clear(); michael@0: this._expectedPacket = 0; michael@0: } michael@0: this._activeTraces.add(aResponse.name); michael@0: michael@0: return aResponse; michael@0: }, michael@0: telemetry: "STARTTRACE" michael@0: }), michael@0: michael@0: /** michael@0: * End a trace. If a name is provided, stop the named michael@0: * trace. Otherwise, stop the most recently started trace. michael@0: * michael@0: * @param aName string michael@0: * The name of the trace to stop. michael@0: * michael@0: * @param aOnResponse function michael@0: * Called with the request's response. michael@0: */ michael@0: stopTrace: DebuggerClient.requester({ michael@0: type: "stopTrace", michael@0: name: args(0) michael@0: }, { michael@0: after: function (aResponse) { michael@0: if (aResponse.error) { michael@0: return aResponse; michael@0: } michael@0: michael@0: this._activeTraces.delete(aResponse.name); michael@0: michael@0: return aResponse; michael@0: }, michael@0: telemetry: "STOPTRACE" michael@0: }) michael@0: }; michael@0: michael@0: /** michael@0: * Grip clients are used to retrieve information about the relevant object. michael@0: * michael@0: * @param aClient DebuggerClient michael@0: * The debugger client parent. michael@0: * @param aGrip object michael@0: * A pause-lifetime object grip returned by the protocol. michael@0: */ michael@0: function ObjectClient(aClient, aGrip) michael@0: { michael@0: this._grip = aGrip; michael@0: this._client = aClient; michael@0: this.request = this._client.request; michael@0: } michael@0: michael@0: ObjectClient.prototype = { michael@0: get actor() { return this._grip.actor }, michael@0: get _transport() { return this._client._transport; }, michael@0: michael@0: valid: true, michael@0: michael@0: get isFrozen() this._grip.frozen, michael@0: get isSealed() this._grip.sealed, michael@0: get isExtensible() this._grip.extensible, michael@0: michael@0: getDefinitionSite: DebuggerClient.requester({ michael@0: type: "definitionSite" michael@0: }, { michael@0: before: function (aPacket) { michael@0: if (this._grip.class != "Function") { michael@0: throw new Error("getDefinitionSite is only valid for function grips."); michael@0: } michael@0: return aPacket; michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Request the names of a function's formal parameters. michael@0: * michael@0: * @param aOnResponse function michael@0: * Called with an object of the form: michael@0: * { parameterNames:[, ...] } michael@0: * where each is the name of a parameter. michael@0: */ michael@0: getParameterNames: DebuggerClient.requester({ michael@0: type: "parameterNames" michael@0: }, { michael@0: before: function (aPacket) { michael@0: if (this._grip["class"] !== "Function") { michael@0: throw new Error("getParameterNames is only valid for function grips."); michael@0: } michael@0: return aPacket; michael@0: }, michael@0: telemetry: "PARAMETERNAMES" michael@0: }), michael@0: michael@0: /** michael@0: * Request the names of the properties defined on the object and not its michael@0: * prototype. michael@0: * michael@0: * @param aOnResponse function Called with the request's response. michael@0: */ michael@0: getOwnPropertyNames: DebuggerClient.requester({ michael@0: type: "ownPropertyNames" michael@0: }, { michael@0: telemetry: "OWNPROPERTYNAMES" michael@0: }), michael@0: michael@0: /** michael@0: * Request the prototype and own properties of the object. michael@0: * michael@0: * @param aOnResponse function Called with the request's response. michael@0: */ michael@0: getPrototypeAndProperties: DebuggerClient.requester({ michael@0: type: "prototypeAndProperties" michael@0: }, { michael@0: telemetry: "PROTOTYPEANDPROPERTIES" michael@0: }), michael@0: michael@0: /** michael@0: * Request the property descriptor of the object's specified property. michael@0: * michael@0: * @param aName string The name of the requested property. michael@0: * @param aOnResponse function Called with the request's response. michael@0: */ michael@0: getProperty: DebuggerClient.requester({ michael@0: type: "property", michael@0: name: args(0) michael@0: }, { michael@0: telemetry: "PROPERTY" michael@0: }), michael@0: michael@0: /** michael@0: * Request the prototype of the object. michael@0: * michael@0: * @param aOnResponse function Called with the request's response. michael@0: */ michael@0: getPrototype: DebuggerClient.requester({ michael@0: type: "prototype" michael@0: }, { michael@0: telemetry: "PROTOTYPE" michael@0: }), michael@0: michael@0: /** michael@0: * Request the display string of the object. michael@0: * michael@0: * @param aOnResponse function Called with the request's response. michael@0: */ michael@0: getDisplayString: DebuggerClient.requester({ michael@0: type: "displayString" michael@0: }, { michael@0: telemetry: "DISPLAYSTRING" michael@0: }), michael@0: michael@0: /** michael@0: * Request the scope of the object. michael@0: * michael@0: * @param aOnResponse function Called with the request's response. michael@0: */ michael@0: getScope: DebuggerClient.requester({ michael@0: type: "scope" michael@0: }, { michael@0: before: function (aPacket) { michael@0: if (this._grip.class !== "Function") { michael@0: throw new Error("scope is only valid for function grips."); michael@0: } michael@0: return aPacket; michael@0: }, michael@0: telemetry: "SCOPE" michael@0: }) michael@0: }; michael@0: michael@0: /** michael@0: * A LongStringClient provides a way to access "very long" strings from the michael@0: * debugger server. michael@0: * michael@0: * @param aClient DebuggerClient michael@0: * The debugger client parent. michael@0: * @param aGrip Object michael@0: * A pause-lifetime long string grip returned by the protocol. michael@0: */ michael@0: function LongStringClient(aClient, aGrip) { michael@0: this._grip = aGrip; michael@0: this._client = aClient; michael@0: this.request = this._client.request; michael@0: } michael@0: michael@0: LongStringClient.prototype = { michael@0: get actor() { return this._grip.actor; }, michael@0: get length() { return this._grip.length; }, michael@0: get initial() { return this._grip.initial; }, michael@0: get _transport() { return this._client._transport; }, michael@0: michael@0: valid: true, michael@0: michael@0: /** michael@0: * Get the substring of this LongString from aStart to aEnd. michael@0: * michael@0: * @param aStart Number michael@0: * The starting index. michael@0: * @param aEnd Number michael@0: * The ending index. michael@0: * @param aCallback Function michael@0: * The function called when we receive the substring. michael@0: */ michael@0: substring: DebuggerClient.requester({ michael@0: type: "substring", michael@0: start: args(0), michael@0: end: args(1) michael@0: }, { michael@0: telemetry: "SUBSTRING" michael@0: }), michael@0: }; michael@0: michael@0: /** michael@0: * A SourceClient provides a way to access the source text of a script. michael@0: * michael@0: * @param aClient ThreadClient michael@0: * The thread client parent. michael@0: * @param aForm Object michael@0: * The form sent across the remote debugging protocol. michael@0: */ michael@0: function SourceClient(aClient, aForm) { michael@0: this._form = aForm; michael@0: this._isBlackBoxed = aForm.isBlackBoxed; michael@0: this._isPrettyPrinted = aForm.isPrettyPrinted; michael@0: this._activeThread = aClient; michael@0: this._client = aClient.client; michael@0: } michael@0: michael@0: SourceClient.prototype = { michael@0: get _transport() this._client._transport, michael@0: get isBlackBoxed() this._isBlackBoxed, michael@0: get isPrettyPrinted() this._isPrettyPrinted, michael@0: get actor() this._form.actor, michael@0: get request() this._client.request, michael@0: get url() this._form.url, michael@0: michael@0: /** michael@0: * Black box this SourceClient's source. michael@0: * michael@0: * @param aCallback Function michael@0: * The callback function called when we receive the response from the server. michael@0: */ michael@0: blackBox: DebuggerClient.requester({ michael@0: type: "blackbox" michael@0: }, { michael@0: telemetry: "BLACKBOX", michael@0: after: function (aResponse) { michael@0: if (!aResponse.error) { michael@0: this._isBlackBoxed = true; michael@0: if (this._activeThread) { michael@0: this._activeThread.notify("blackboxchange", this); michael@0: } michael@0: } michael@0: return aResponse; michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Un-black box this SourceClient's source. michael@0: * michael@0: * @param aCallback Function michael@0: * The callback function called when we receive the response from the server. michael@0: */ michael@0: unblackBox: DebuggerClient.requester({ michael@0: type: "unblackbox" michael@0: }, { michael@0: telemetry: "UNBLACKBOX", michael@0: after: function (aResponse) { michael@0: if (!aResponse.error) { michael@0: this._isBlackBoxed = false; michael@0: if (this._activeThread) { michael@0: this._activeThread.notify("blackboxchange", this); michael@0: } michael@0: } michael@0: return aResponse; michael@0: } michael@0: }), michael@0: michael@0: /** michael@0: * Get a long string grip for this SourceClient's source. michael@0: */ michael@0: source: function (aCallback) { michael@0: let packet = { michael@0: to: this._form.actor, michael@0: type: "source" michael@0: }; michael@0: this._client.request(packet, aResponse => { michael@0: this._onSourceResponse(aResponse, aCallback) michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Pretty print this source's text. michael@0: */ michael@0: prettyPrint: function (aIndent, aCallback) { michael@0: const packet = { michael@0: to: this._form.actor, michael@0: type: "prettyPrint", michael@0: indent: aIndent michael@0: }; michael@0: this._client.request(packet, aResponse => { michael@0: if (!aResponse.error) { michael@0: this._isPrettyPrinted = true; michael@0: this._activeThread._clearFrames(); michael@0: this._activeThread.notify("prettyprintchange", this); michael@0: } michael@0: this._onSourceResponse(aResponse, aCallback); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Stop pretty printing this source's text. michael@0: */ michael@0: disablePrettyPrint: function (aCallback) { michael@0: const packet = { michael@0: to: this._form.actor, michael@0: type: "disablePrettyPrint" michael@0: }; michael@0: this._client.request(packet, aResponse => { michael@0: if (!aResponse.error) { michael@0: this._isPrettyPrinted = false; michael@0: this._activeThread._clearFrames(); michael@0: this._activeThread.notify("prettyprintchange", this); michael@0: } michael@0: this._onSourceResponse(aResponse, aCallback); michael@0: }); michael@0: }, michael@0: michael@0: _onSourceResponse: function (aResponse, aCallback) { michael@0: if (aResponse.error) { michael@0: aCallback(aResponse); michael@0: return; michael@0: } michael@0: michael@0: if (typeof aResponse.source === "string") { michael@0: aCallback(aResponse); michael@0: return; michael@0: } michael@0: michael@0: let { contentType, source } = aResponse; michael@0: let longString = this._activeThread.threadLongString(source); michael@0: longString.substring(0, longString.length, function (aResponse) { michael@0: if (aResponse.error) { michael@0: aCallback(aResponse); michael@0: return; michael@0: } michael@0: michael@0: aCallback({ michael@0: source: aResponse.substring, michael@0: contentType: contentType michael@0: }); michael@0: }); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Breakpoint clients are used to remove breakpoints that are no longer used. michael@0: * michael@0: * @param aClient DebuggerClient michael@0: * The debugger client parent. michael@0: * @param aActor string michael@0: * The actor ID for this breakpoint. michael@0: * @param aLocation object michael@0: * The location of the breakpoint. This is an object with two properties: michael@0: * url and line. michael@0: * @param aCondition string michael@0: * The conditional expression of the breakpoint michael@0: */ michael@0: function BreakpointClient(aClient, aActor, aLocation, aCondition) { michael@0: this._client = aClient; michael@0: this._actor = aActor; michael@0: this.location = aLocation; michael@0: this.request = this._client.request; michael@0: michael@0: // The condition property should only exist if it's a truthy value michael@0: if (aCondition) { michael@0: this.condition = aCondition; michael@0: } michael@0: } michael@0: michael@0: BreakpointClient.prototype = { michael@0: michael@0: _actor: null, michael@0: get actor() { return this._actor; }, michael@0: get _transport() { return this._client._transport; }, michael@0: michael@0: /** michael@0: * Remove the breakpoint from the server. michael@0: */ michael@0: remove: DebuggerClient.requester({ michael@0: type: "delete" michael@0: }, { michael@0: telemetry: "DELETE" michael@0: }), michael@0: michael@0: /** michael@0: * Determines if this breakpoint has a condition michael@0: */ michael@0: hasCondition: function() { michael@0: let root = this._client.mainRoot; michael@0: // XXX bug 990137: We will remove support for client-side handling of michael@0: // conditional breakpoints michael@0: if (root.traits.conditionalBreakpoints) { michael@0: return "condition" in this; michael@0: } else { michael@0: return "conditionalExpression" in this; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Get the condition of this breakpoint. Currently we have to michael@0: * support locally emulated conditional breakpoints until the michael@0: * debugger servers are updated (see bug 990137). We used a michael@0: * different property when moving it server-side to ensure that we michael@0: * are testing the right code. michael@0: */ michael@0: getCondition: function() { michael@0: let root = this._client.mainRoot; michael@0: if (root.traits.conditionalBreakpoints) { michael@0: return this.condition; michael@0: } else { michael@0: return this.conditionalExpression; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Set the condition of this breakpoint michael@0: */ michael@0: setCondition: function(gThreadClient, aCondition) { michael@0: let root = this._client.mainRoot; michael@0: let deferred = promise.defer(); michael@0: michael@0: if (root.traits.conditionalBreakpoints) { michael@0: let info = { michael@0: url: this.location.url, michael@0: line: this.location.line, michael@0: column: this.location.column, michael@0: condition: aCondition michael@0: }; michael@0: michael@0: // Remove the current breakpoint and add a new one with the michael@0: // condition. michael@0: this.remove(aResponse => { michael@0: if (aResponse && aResponse.error) { michael@0: deferred.reject(aResponse); michael@0: return; michael@0: } michael@0: michael@0: gThreadClient.setBreakpoint(info, (aResponse, aNewBreakpoint) => { michael@0: if (aResponse && aResponse.error) { michael@0: deferred.reject(aResponse); michael@0: } else { michael@0: deferred.resolve(aNewBreakpoint); michael@0: } michael@0: }); michael@0: }); michael@0: } else { michael@0: // The property shouldn't even exist if the condition is blank michael@0: if(aCondition === "") { michael@0: delete this.conditionalExpression; michael@0: } michael@0: else { michael@0: this.conditionalExpression = aCondition; michael@0: } michael@0: deferred.resolve(this); michael@0: } michael@0: michael@0: return deferred.promise; michael@0: } michael@0: }; michael@0: michael@0: eventSource(BreakpointClient.prototype); michael@0: michael@0: /** michael@0: * Environment clients are used to manipulate the lexical environment actors. michael@0: * michael@0: * @param aClient DebuggerClient michael@0: * The debugger client parent. michael@0: * @param aForm Object michael@0: * The form sent across the remote debugging protocol. michael@0: */ michael@0: function EnvironmentClient(aClient, aForm) { michael@0: this._client = aClient; michael@0: this._form = aForm; michael@0: this.request = this._client.request; michael@0: } michael@0: michael@0: EnvironmentClient.prototype = { michael@0: michael@0: get actor() this._form.actor, michael@0: get _transport() { return this._client._transport; }, michael@0: michael@0: /** michael@0: * Fetches the bindings introduced by this lexical environment. michael@0: */ michael@0: getBindings: DebuggerClient.requester({ michael@0: type: "bindings" michael@0: }, { michael@0: telemetry: "BINDINGS" michael@0: }), michael@0: michael@0: /** michael@0: * Changes the value of the identifier whose name is name (a string) to that michael@0: * represented by value (a grip). michael@0: */ michael@0: assign: DebuggerClient.requester({ michael@0: type: "assign", michael@0: name: args(0), michael@0: value: args(1) michael@0: }, { michael@0: telemetry: "ASSIGN" michael@0: }) michael@0: }; michael@0: michael@0: eventSource(EnvironmentClient.prototype); michael@0: michael@0: /** michael@0: * Connects to a debugger server socket and returns a DebuggerTransport. michael@0: * michael@0: * @param aHost string michael@0: * The host name or IP address of the debugger server. michael@0: * @param aPort number michael@0: * The port number of the debugger server. michael@0: */ michael@0: this.debuggerSocketConnect = function (aHost, aPort) michael@0: { michael@0: let s = socketTransportService.createTransport(null, 0, aHost, aPort, null); michael@0: // By default the CONNECT socket timeout is very long, 65535 seconds, michael@0: // so that if we race to be in CONNECT state while the server socket is still michael@0: // initializing, the connection is stuck in connecting state for 18.20 hours! michael@0: s.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 2); michael@0: michael@0: // openOutputStream may throw NS_ERROR_NOT_INITIALIZED if we hit some race michael@0: // where the nsISocketTransport gets shutdown in between its instantiation and michael@0: // the call to this method. michael@0: let transport; michael@0: try { michael@0: transport = new DebuggerTransport(s.openInputStream(0, 0, 0), michael@0: s.openOutputStream(0, 0, 0)); michael@0: } catch(e) { michael@0: DevToolsUtils.reportException("debuggerSocketConnect", e); michael@0: throw e; michael@0: } michael@0: return transport; michael@0: } michael@0: michael@0: /** michael@0: * Takes a pair of items and returns them as an array. michael@0: */ michael@0: function pair(aItemOne, aItemTwo) { michael@0: return [aItemOne, aItemTwo]; michael@0: } michael@0: function noop() {}