/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Mozilla Debug Protocol Client code. * * The Initial Developer of the Original Code is * Mozilla Foundation * Portions created by the Initial Developer are Copyright (C) 2011 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Dave Camp (original author) * Panos Astithas * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ "use strict"; const Ci = Components.interfaces; const Cc = Components.classes; const Cu = Components.utils; const Cr = Components.results; var EXPORTED_SYMBOLS = ["DebuggerTransport", "DebuggerClient", "debuggerSocketConnect"]; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "socketTransportService", "@mozilla.org/network/socket-transport-service;1", "nsISocketTransportService"); function dumpn(str) { dump("DBG-CLIENT: " + str + "\n"); } let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] .getService(Ci.mozIJSSubScriptLoader); loader.loadSubScript("chrome://global/content/devtools/dbg-transport.js"); /** * Add simple event notification to a prototype object. Any object that has * some use for event notifications or the observer pattern in general can be * augmented with the necessary facilities by passing its prototype to this * function. * * @param aProto object * The prototype object that will be modified. */ function eventSource(aProto) { /** * Add a listener to the event source for a given event. * * @param aName string * The event to listen for, or null to listen to all events. * @param aListener function * Called when the event is fired. If the same listener * is added more the once, it will be called once per * addListener call. */ aProto.addListener = function EV_addListener(aName, aListener) { if (typeof aListener != "function") { return; } if (!this._listeners) { this._listeners = {}; } if (!aName) { aName = '*'; } this._getListeners(aName).push(aListener); }; /** * Add a listener to the event source for a given event. The * listener will be removed after it is called for the first time. * * @param aName string * The event to listen for, or null to respond to the first event * fired by the object. * @param aListener function * Called when the event is fired. */ aProto.addOneTimeListener = function EV_addOneTimeListener(aName, aListener) { let self = this; let l = function() { self.removeListener(aName, l); aListener.apply(null, arguments); }; this.addListener(aName, l); }; /** * Remove a listener from the event source previously added with * addListener(). * * @param aName string * The event name used during addListener to add the listener. * @param aListener function * The callback to remove. If addListener was called multiple * times, all instances will be removed. */ aProto.removeListener = function EV_removeListener(aName, aListener) { if (!this._listeners || !this._listeners[aName]) { return; } this._listeners[aName] = this._listeners[aName].filter(function(l) { return l != aListener }); }; /** * Returns the listeners for the specified event name. If none are defined it * initializes an empty list and returns that. * * @param aName string * The event name. */ aProto._getListeners = function EV_getListeners(aName) { if (aName in this._listeners) { return this._listeners[aName]; } this._listeners[aName] = []; return this._listeners[aName]; }; /** * Notify listeners of an event. * * @param aName string * The event to fire. * @param arguments * All arguments will be passed along to the listeners, * including the name argument. */ aProto.notify = function EV_notify() { if (!this._listeners) { return; } let name = arguments[0]; let listeners = this._getListeners(name).slice(0); if (this._listeners['*']) { listeners.concat(this._listeners['*']); } for each (let listener in listeners) { try { listener.apply(null, arguments); } catch (e) { // Prevent a bad listener from interfering with the others. Cu.reportError(e); } } } } /** * Set of protocol messages that affect thread state, and the * state the actor is in after each message. */ const ThreadStateTypes = { "paused": "paused", "resumed": "attached", "detached": "detached" }; /** * Set of debug protocol request types that specify the protocol request being * sent to the server. */ const DebugProtocolTypes = { "attach": "attach", "clientEvaluate": "clientEvaluate", "delete": "delete", "detach": "detach", "frames": "frames", "interrupt": "interrupt", "listTabs": "listTabs", "nameAndParameters": "nameAndParameters", "ownPropertyNames": "ownPropertyNames", "property": "property", "prototype": "prototype", "prototypeAndProperties": "prototypeAndProperties", "resume": "resume", "scripts": "scripts", "setBreakpoint": "setBreakpoint" }; const ROOT_ACTOR_NAME = "root"; /** * Creates a client for the remote debugging protocol server. This client * provides the means to communicate with the server and exchange the messages * required by the protocol in a traditional JavaScript API. */ function DebuggerClient(aTransport) { this._transport = aTransport; this._transport.hooks = this; this._threadClients = {}; this._tabClients = {}; this._pendingRequests = []; this._activeRequests = {}; this._eventsEnabled = true; } DebuggerClient.prototype = { /** * Connect to the server and start exchanging protocol messages. * * @param aOnConnected function * If specified, will be called when the greeting packet is * received from the debugging server. */ connect: function DC_connect(aOnConnected) { if (aOnConnected) { this.addOneTimeListener("connected", function(aName, aApplicationType, aTraits) { aOnConnected(aApplicationType, aTraits); }); } this._transport.ready(); }, /** * Shut down communication with the debugging server. * * @param aOnClosed function * If specified, will be called when the debugging connection * has been closed. */ close: function DC_close(aOnClosed) { // Disable detach event notifications, because event handlers will be in a // cleared scope by the time they run. this._eventsEnabled = false; if (aOnClosed) { this.addOneTimeListener('closed', function(aEvent) { aOnClosed(); }); } let closeTransport = function _closeTransport() { this._transport.close(); this._transport = null; }.bind(this); let detachTab = function _detachTab() { if (this.activeTab) { this.activeTab.detach(closeTransport); } else { closeTransport(); } }.bind(this); if (this.activeThread) { this.activeThread.detach(detachTab); } else { detachTab(); } }, /** * List the open tabs. * * @param function aOnResponse * Called with the response packet. */ listTabs: function DC_listTabs(aOnResponse) { let packet = { to: ROOT_ACTOR_NAME, type: DebugProtocolTypes.listTabs }; this.request(packet, function(aResponse) { aOnResponse(aResponse); }); }, /** * Attach to a tab actor. * * @param string aTabActor * The actor ID for the tab to attach. * @param function aOnResponse * Called with the response packet and a TabClient * (which will be undefined on error). */ attachTab: function DC_attachTab(aTabActor, aOnResponse) { let self = this; let packet = { to: aTabActor, type: DebugProtocolTypes.attach }; this.request(packet, function(aResponse) { if (!aResponse.error) { var tabClient = new TabClient(self, aTabActor); self._tabClients[aTabActor] = tabClient; self.activeTab = tabClient; } aOnResponse(aResponse, tabClient); }); }, /** * Attach to a thread actor. * * @param string aThreadActor * The actor ID for the thread to attach. * @param function aOnResponse * Called with the response packet and a ThreadClient * (which will be undefined on error). */ attachThread: function DC_attachThread(aThreadActor, aOnResponse) { let self = this; let packet = { to: aThreadActor, type: DebugProtocolTypes.attach }; this.request(packet, function(aResponse) { if (!aResponse.error) { var threadClient = new ThreadClient(self, aThreadActor); self._threadClients[aThreadActor] = threadClient; self.activeThread = threadClient; } aOnResponse(aResponse, threadClient); }); }, /** * Send a request to the debugging server. * * @param aRequest object * A JSON packet to send to the debugging server. * @param aOnResponse function * If specified, will be called with the response packet when * debugging server responds. */ request: function DC_request(aRequest, aOnResponse) { if (!this._connected) { throw "Have not yet received a hello packet from the server."; } if (!aRequest.to) { throw "Request packet has no destination."; } this._pendingRequests.push({ to: aRequest.to, request: aRequest, onResponse: aOnResponse }); this._sendRequests(); }, /** * Send pending requests to any actors that don't already have an * active request. */ _sendRequests: function DC_sendRequests() { let self = this; this._pendingRequests = this._pendingRequests.filter(function(request) { if (request.to in self._activeRequests) { return true; } self._activeRequests[request.to] = request; self._transport.send(request.request); return false; }); }, // Transport hooks. /** * Called by DebuggerTransport to dispatch incoming packets as appropriate. * * @param aPacket object * The incoming packet. */ onPacket: function DC_onPacket(aPacket) { if (!this._connected) { // Hello packet. this._connected = true; this.notify("connected", aPacket.applicationType, aPacket.traits); return; } try { if (!aPacket.from) { dumpn("Server did not specify an actor, dropping packet: " + JSON.stringify(aPacket)); return; } let onResponse; if (aPacket.from in this._activeRequests) { onResponse = this._activeRequests[aPacket.from].onResponse; delete this._activeRequests[aPacket.from]; } // paused/resumed/detached get special treatment... if (aPacket.type in ThreadStateTypes && aPacket.from in this._threadClients) { this._threadClients[aPacket.from]._onThreadState(aPacket); } this.notify(aPacket.type, aPacket); if (onResponse) { onResponse(aPacket); } } catch(ex) { dumpn("Error handling response: " + ex + " - " + ex.stack); Cu.reportError(ex); } this._sendRequests(); }, /** * Called by DebuggerTransport when the underlying stream is closed. * * @param aStatus nsresult * The status code that corresponds to the reason for closing * the stream. */ onClosed: function DC_onClosed(aStatus) { this.notify("closed"); }, } eventSource(DebuggerClient.prototype); /** * Creates a tab client for the remote debugging protocol server. This client * is a front to the tab actor created in the server side, hiding the protocol * details in a traditional JavaScript API. * * @param aClient DebuggerClient * The debugger client parent. * @param aActor string * The actor ID for this tab. */ function TabClient(aClient, aActor) { this._client = aClient; this._actor = aActor; } TabClient.prototype = { /** * Detach the client from the tab actor. * * @param function aOnResponse * Called with the response packet. */ detach: function TabC_detach(aOnResponse) { let self = this; let packet = { to: this._actor, type: DebugProtocolTypes.detach }; this._client.request(packet, function(aResponse) { if (self.activeTab === self._client._tabClients[self._actor]) { delete self.activeTab; } delete self._client._tabClients[self._actor]; if (aOnResponse) { aOnResponse(aResponse); } }); } }; eventSource(TabClient.prototype); /** * Creates a thread client for the remote debugging protocol server. This client * is a front to the thread actor created in the server side, hiding the * protocol details in a traditional JavaScript API. * * @param aClient DebuggerClient * The debugger client parent. * @param aActor string * The actor ID for this thread. */ function ThreadClient(aClient, aActor) { this._client = aClient; this._actor = aActor; this._frameCache = []; this._scriptCache = {}; } ThreadClient.prototype = { _state: "paused", get state() { return this._state; }, get paused() { return this._state === "paused"; }, _actor: null, get actor() { return this._actor; }, _assertPaused: function TC_assertPaused(aCommand) { if (!this.paused) { throw aCommand + " command sent while not paused."; } }, /** * Resume a paused thread. * * @param function aOnResponse * Called with the response packet. */ resume: function TC_resume(aOnResponse) { this._assertPaused("resume"); // Put the client in a tentative "resuming" state so we can prevent // further requests that should only be sent in the paused state. this._state = "resuming"; let self = this; let packet = { to: this._actor, type: DebugProtocolTypes.resume }; this._client.request(packet, function(aResponse) { if (aResponse.error) { // There was an error resuming, back to paused state. self._state = "paused"; } if (aOnResponse) { aOnResponse(aResponse); } }); }, /** * Interrupt a running thread. * * @param function aOnResponse * Called with the response packet. */ interrupt: function TC_interrupt(aOnResponse) { let packet = { to: this._actor, type: DebugProtocolTypes.interrupt }; this._client.request(packet, function(aResponse) { if (aOnResponse) { aOnResponse(aResponse); } }); }, /** * Send a clientEvaluate packet to the debuggee. Response * will be a resume packet. */ eval: function TC_eval(aFrame, aExpression, aOnResponse) { this._assertPaused("eval"); // Put the client in a tentative "resuming" state so we can prevent // further requests that should only be sent in the paused state. this._state = "resuming"; let self = this; let request = { to: this._actor, type: DebugProtocolTypes.clientEvaluate, frame: aFrame, expression: aExpression }; this._client.request(request, function(aResponse) { if (aResponse.error) { // There was an error resuming, back to paused state. self._state = "paused"; } if (aOnResponse) { aOnResponse(aResponse); } }); }, /** * Detach from the thread actor. * * @param function aOnResponse * Called with the response packet. */ detach: function TC_detach(aOnResponse) { let self = this; let packet = { to: this._actor, type: DebugProtocolTypes.detach }; this._client.request(packet, function(aResponse) { if (self.activeThread === self._client._threadClients[self._actor]) { delete self.activeThread; } delete self._client._threadClients[self._actor]; if (aOnResponse) { aOnResponse(aResponse); } }); }, /** * Request to set a breakpoint in the specified location. * * @param aLocation object * The source location object where the breakpoint * will be set. * @param aOnResponse integer * Called with the thread's response. */ setBreakpoint: function TC_setBreakpoint(aLocation, aOnResponse) { this._assertPaused("setBreakpoint"); let self = this; let packet = { to: this._actor, type: DebugProtocolTypes.setBreakpoint, location: aLocation }; this._client.request(packet, function (aResponse) { if (aOnResponse) { let bpClient = new BreakpointClient(self._client, aResponse.actor); aOnResponse(aResponse, bpClient); } }); }, /** * Request the loaded scripts for the current thread. * * @param aOnResponse integer * Called with the thread's response. */ getScripts: function TC_getScripts(aOnResponse) { let packet = { to: this._actor, type: DebugProtocolTypes.scripts }; this._client.request(packet, aOnResponse); }, /** * A cache of source scripts. Clients can observe the scriptsadded and * scriptscleared event to keep up to date on changes to this cache, * and can fill it using the fillScripts method. */ get cachedScripts() { return this._scriptCache; }, /** * Ensure that source scripts have been loaded in the * ThreadClient's source script cache. A scriptsadded event will be * sent when the source script cache is updated. * * @returns true if a scriptsadded notification should be expected. */ fillScripts: function TC_fillScripts() { let self = this; this.getScripts(function(aResponse) { for each (let script in aResponse.scripts) { self._scriptCache[script.url] = script; } // If the cache was modified, notify listeners. if (aResponse.scripts && aResponse.scripts.length) { self.notify("scriptsadded"); } }); return true; }, /** * Clear the thread's source script cache. A scriptscleared event * will be sent. */ _clearScripts: function TC_clearScripts() { if (Object.keys(this._scriptCache).length > 0) { this._scriptCache = {} this.notify("scriptscleared"); } }, /** * Request frames from the callstack for the current thread. * * @param aStart integer * The number of the youngest stack frame to return (the youngest * frame is 0). * @param aCount integer * The maximum number of frames to return, or null to return all * frames. * @param aOnResponse function * Called with the thread's response. */ getFrames: function TC_getFrames(aStart, aCount, aOnResponse) { this._assertPaused("frames"); let packet = { to: this._actor, type: DebugProtocolTypes.frames, start: aStart, count: aCount ? aCount : undefined }; this._client.request(packet, aOnResponse); }, /** * An array of cached frames. Clients can observe the framesadded and * framescleared event to keep up to date on changes to this cache, * and can fill it using the fillFrames method. */ get cachedFrames() { return this._frameCache; }, /** * true if there are more stack frames available on the server. */ get moreFrames() { return this.paused && (!this._frameCache || this._frameCache.length == 0 || !this._frameCache[this._frameCache.length - 1].oldest); }, /** * Ensure that at least aTotal stack frames have been loaded in the * ThreadClient's stack frame cache. A framesadded event will be * sent when the stack frame cache is updated. * * @param aTotal number * The minimum number of stack frames to be included. * * @returns true if a framesadded notification should be expected. */ fillFrames: function TC_fillFrames(aTotal) { this._assertPaused("fillFrames"); if (this._frameCache.length >= aTotal) { return false; } let numFrames = this._frameCache.length; let self = this; this.getFrames(numFrames, aTotal - numFrames, function(aResponse) { for each (let frame in aResponse.frames) { self._frameCache[frame.depth] = frame; } // If we got as many frames as we asked for, there might be more // frames available. self.notify("framesadded"); }); return true; }, /** * Clear the thread's stack frame cache. A framescleared event * will be sent. */ _clearFrames: function TC_clearFrames() { if (this._frameCache.length > 0) { this._frameCache = []; this.notify("framescleared"); } }, /** * Return a GripClient object for the given object grip. * * @param aGrip object * A pause-lifetime object grip returned by the protocol. */ pauseGrip: function TC_pauseGrip(aGrip) { if (!this._pauseGrips) { this._pauseGrips = {}; } if (aGrip.actor in this._pauseGrips) { return this._pauseGrips[aGrip.actor]; } let client = new GripClient(this._client, aGrip); this._pauseGrips[aGrip.actor] = client; return client; }, /** * Invalidate pause-lifetime grip clients and clear the list of * current grip clients. */ _clearPauseGrips: function TC_clearPauseGrips(aPacket) { for each (let grip in this._pauseGrips) { grip.valid = false; } this._pauseGrips = null; }, /** * Handle thread state change by doing necessary cleanup and notifying all * registered listeners. */ _onThreadState: function TC_onThreadState(aPacket) { this._state = ThreadStateTypes[aPacket.type]; this._clearFrames(); this._clearPauseGrips(); this._client._eventsEnabled && this.notify(aPacket.type, aPacket); }, }; eventSource(ThreadClient.prototype); /** * Grip clients are used to retrieve information about the relevant object. * * @param aClient DebuggerClient * The debugger client parent. * @param aGrip object * A pause-lifetime object grip returned by the protocol. */ function GripClient(aClient, aGrip) { this._grip = aGrip; this._client = aClient; } GripClient.prototype = { get actor() { return this._grip.actor }, _valid: true, get valid() { return this._valid; }, set valid(aValid) { this._valid = !!aValid; }, /** * Request the name of the function and its formal parameters. * * @param aOnResponse function * Called with the request's response. */ getSignature: function GC_getSignature(aOnResponse) { if (this._grip["class"] !== "Function") { throw "getSignature is only valid for function grips."; } let packet = { to: this.actor, type: DebugProtocolTypes.nameAndParameters }; this._client.request(packet, function (aResponse) { if (aOnResponse) { aOnResponse(aResponse); } }); }, /** * Request the names of the properties defined on the object and not its * prototype. * * @param aOnResponse function Called with the request's response. */ getOwnPropertyNames: function GC_getOwnPropertyNames(aOnResponse) { let packet = { to: this.actor, type: DebugProtocolTypes.ownPropertyNames }; this._client.request(packet, function (aResponse) { if (aOnResponse) { aOnResponse(aResponse); } }); }, /** * Request the prototype and own properties of the object. * * @param aOnResponse function Called with the request's response. */ getPrototypeAndProperties: function GC_getPrototypeAndProperties(aOnResponse) { let packet = { to: this.actor, type: DebugProtocolTypes.prototypeAndProperties }; this._client.request(packet, function (aResponse) { if (aOnResponse) { aOnResponse(aResponse); } }); }, /** * Request the property descriptor of the object's specified property. * * @param aName string The name of the requested property. * @param aOnResponse function Called with the request's response. */ getProperty: function GC_getProperty(aName, aOnResponse) { let packet = { to: this.actor, type: DebugProtocolTypes.property, name: aName }; this._client.request(packet, function (aResponse) { if (aOnResponse) { aOnResponse(aResponse); } }); }, /** * Request the prototype of the object. * * @param aOnResponse function Called with the request's response. */ getPrototype: function GC_getPrototype(aOnResponse) { let packet = { to: this.actor, type: DebugProtocolTypes.prototype }; this._client.request(packet, function (aResponse) { if (aOnResponse) { aOnResponse(aResponse); } }); } }; /** * Breakpoint clients are used to remove breakpoints that are no longer used. * * @param aClient DebuggerClient * The debugger client parent. * @param aActor string * The actor ID for this breakpoint. */ function BreakpointClient(aClient, aActor) { this._client = aClient; this._actor = aActor; } BreakpointClient.prototype = { _actor: null, get actor() { return this._actor; }, /** * Remove the breakpoint from the server. */ remove: function BC_remove(aOnResponse) { let packet = { to: this._actor, type: DebugProtocolTypes["delete"] }; this._client.request(packet, function(aResponse) { if (aOnResponse) { aOnResponse(aResponse); } }); } }; eventSource(BreakpointClient.prototype); /** * Connects to a debugger server socket and returns a DebuggerTransport. * * @param aHost string * The host name or IP address of the debugger server. * @param aPort number * The port number of the debugger server. */ function debuggerSocketConnect(aHost, aPort) { let s = socketTransportService.createTransport(null, 0, aHost, aPort, null); let transport = new DebuggerTransport(s.openInputStream(0, 0, 0), s.openOutputStream(0, 0, 0)); return transport; }