Files
palemoon27/toolkit/devtools/client/dbg-client.jsm
T
roytam1 a7bc0406ee import changes from `dev' branch of rmottola/Arctic-Fox:
- Bug 1155006: Fix unified build sensitivities in js/src/jit. r=shu (6e24e1af1)
- Bug 1162766 - Fix more bad implicit constructors in js. r=evilpie (39961b06d)
- Bug 1151606 - Stream atoms instead of raw pointers for native functions in tracked optimizations. (r=djvj) (7641ee9d6)
- pointer style (540728104)
- Bug 1154997 - Deal with self-hosted builtins when stringifying tracked optimization type info. (r=djvj) (92f9a54e6)
- pointer style (45742d820)
- Bug 1154115 - Rewrite the JSAPI profiling API to use a FrameHandle, as to avoid multiple lookups in JitcodeGlobalTable. (r=djvj) (4d202ba9e)
- Bug 1119023 - Timeline in new perf tool should filter out markers, r=jsantell (6fc1a8bbe)
- Bug 1132755 - Allocations tree has a bunch of columns that don't make sense, r=jsantell (1ae9ee7e2)
- Bug 1142744 - Fix tests broken by bug 1132755, r=me (cc495f72d)
- Bug 1133058 - OptionsView button, when clicked, should have an 'open' attribute. r=vp (65a78d896)
- Bug 1132765 - Pass through performance memory options for 'probability' and 'maxLogLength' from the front to the memory actor. r=vp (f9bbbe098)
- Bug 1141817 - Fix yield statement to correctly return memory actor state so that the performance tool can poll for allocations during recording. r=vp (2ddf7d528)
- Bug 1141817 - Followup to fix additional intermittents like bug 1132370, r=vp (eab962f01)
- Bug 1142748 - Use a single configuration for starting/stopping recordings, r=jsantell (0181b319a)
- bit of Bug 879008 - New UI for the sampling Profiler (32c4d0fe8)
- Bug 1123815 - Merge gum into fx-team to enable the Performance++ tool, r=me (84aabbd61)
- Bug 1143933 - Expose raw JIT optimization information in performance front end. r=vp,shu (f68a6df50)
- Bug 1143915 - Allow multiple calls to memory and timeline actor's start methods, to return the local start time from the actor. r=vp (028ac4187)
- Bug 978948 - Add animation generator support for setTimeout in the canvas debugger. r=vp (42d623452)
- Bug 985488 - Allow canvas debugger to time out and stop recording frames. Canvas debugger 'wait' style now matches other media styles. Update labels in canvas debugger to explicitly state that it's waiting for rAF cycles, rather than appearing as if something went wrong. r=vporof (b4670d843)
- Bug 1144163 - Add a rulers highlighter; added unit test. r=pbrosset (5811a67d0)
- Bug 1144163 - Add a rulers highlighter; added highlighter. r=pbrosset (779f88bdd)
- Bug 1144163 - Add a rulers highlighter; added gcli command and button. r=pbrosset (d0d13da51)
- Bug 1110550 - Enable performance overview graphs to rerender and change on devtools theme switch. r=vp (bd91ca7cf)
- Bug 1149630 - Performance graphs should inherit from a common graph and be similarly styled. r=vporof (481c841f1)
- Bug 1150733 - Correctly internationalize jit samples label. r=vporof, r=flod (b5612d1a6)
- Bug 1137518 - FlameGraph's destroy function should be async, r=jsantell (f103e4c15)
- Bug 1137503 - Avoid potential infinite loops in `findOptimalTickInterval` functions, r=jsantell (95df6c04a)
- Bug 1121194 - Support vertical panning for the flamegraph in the new performance tool, r=jsantell (06241b5b2)
- Bug 1121180 - Support dark theme in flamecharts for the performance tool. r=vp (c76abe237)
-  Bug 1059308 - Add Target.isTabActor to tell if the remote tab actor supports attach/detach requests. r=jryans (e03dcef93)
- Bug 1132370 - Wrong State: Expected 'attached', but current state is 'detached', r=jsantell (e884e8db9)
- No Bug - Fix documentation for _startMemory and _stopMemory in performance/modules/front.js, r=me DONTBUILD (d79090b31)
- Bug 1147656 - Remove duplicate profiler defaults from the front end and just use on the server. r=vp (35c015dd0)
- Bug 1046234 - Add more DevTools Telemetry measures (display size etc) r=pbrosset, r=gijs (a235681b4)
- actually package telemetry.js (e8f3a58a4)
- Bug 1077464 - Wire console.profile/profileEnd to the new performance tool. Move most of the recording-model logic from the front end into the PerformanceFront and PerformanceActorConnection so it can manage recordings without the front end being viewed. r=vp,jryans,pbrosset (eef8e18c3)
- Bug 1144363 - Fix this._telemetry is undefined in gDevTools. r=bgrins (ba7d02902)
- init telemetry, missing parts of Bug 866642 (1e70df975)
- do not use sysctl.h on Linux anymore, since it is not provided by recent glibc (b2467d7ce)
- clean up some telemetry issues of histogram, parts of  Bug 974171 (d30c8d0ad)
- move devtools to browser - part 1 (9a856f452)
- Bug 1291423: Explicitly qualify the destructor call that we invoke in Maybe::reset. r=Waldo (944904a7d)
- Bug 1148075 - Dynamically add XUL commands for the debugger frontend. r=vporof (60bc91f8f)
- Bug 1147945 - Let the profiler's buffer size and sample rate be configurable via prefs. r=vp (acebcbdd9)
- Bug 1124326 - Improve packageDir support for Cordova. r=ochameau (4b736580a)
- Bug 1124326 - Support Cordova w/o build file. r=ochameau (d4b50aeae)
- Bug 1134029 - Fix 'Component returned failure code: 0x80004005 (NS_ERROR_FAILURE) [nsIURI.host]' timeouts, r=jsantell (18d16a5d0)
- Bug 1147806 - Content frame filtering is confused when profiling FxOS, r=jsantell (b3c62c552)
- Bug 1108843 - Generalize platform data in call tree view when platform data is hidden. r=vporof (354553ed7)
- Bug 1138928 - Display only function name and file, instead of full url, in flame graphs. r=vp (4169689c1)
- Bug 1152605 - Should not show host names for chrome URIs. r=vporof (c6dcf9e78)
- Bug 1147604 - Inverted call trees should list (root) as leaves. r=jsantell (01768267f)
- Bug 1075450 - Disable some Awesomebar actions for private windows r=mak (21d5586e7)
- Bug 1120616 - Part 1: Implement filter styles in rule view r=bgrins (b66ee0282)
- Bug 1120616 - Part 2: Add unit tests for filter styles in rule view r=bgrins (2892503d8)
- Bug 1120616 - Part 3: Adjust the styles in the computed view's filter style search r=bgrins (41f8fae1b)
- Bug 1120616 - Part 4: Add textbox context menu for rule and computed view r=bgrins (ff3f868ad)
- Bug 1120616 - Part 5: Refactor style inspector tests to use synthesizeKeys r=bgrins (41db021d7)
- Bug 1102219 - Part 5: Replace more `String.prototype.contains` with `String.prototype.includes` in chrome code. r=till (86ed03588)
- Bug 1154018 - Check to see that nsIURI's host exists when parsing location for framenodes, and cache failures. r=vp (9494d52e7)
- Bug 1160691 - Optimize FrameUtils.isContent and FrameUtils.parseLocation. (r=jsantell) (09118fd5d)
- Bug 1154115 - Make the performance devtool handle the new profiler JSON format. (r=jsantell,vporof) (e3e5be7a4)
- Bug 1059308 - Make frame selection button to work in browser toolbox. r=jryans,past (30fe6e61e)
- Bug 1059308 - Fix tests to support chrome actor. r=jryans (01cf3926c)
- Bug 1147042 - Rename attachProcess to getProcess. r=ochameau (0393ffb80)
- Bug 1145824 - Profiler actor and performance tools now handle passing in a startTime to filter out SPS profiles on platform rather than client. r=vp,fitzgen (f225116ba)
- Bug 1157718 - Do not use Array.prototype.includes in production code that leaves nightly in performance tool. r=fitzgen (ff06d284e)
- Bug 1140728 - Rename 'Memory' to 'Allocations' in the new performance tool. r=jsantell (f584e720f)
- Bug 1137500 - Always wait for the overview to be rendered in tests after a recording finishes, unless otherwise specified, r=jsantell (59825e179)
- Bug 1137487 - AbstractCanvasGraph's destroy function should be async, r=jsantell (a17ae00b5)
- Bug 1132758 - Performance feature visibility now based on a per recording-basis, dependent on features enabled and server support. r=vp (0d080a7c2)
- Bug 1147035 - Make DeveloperToolbar.jsm use the gBrowser.contentDocumentAsCPOW shortcut. r=past. (251eff125)
- Bug 1151168 - Don't flush profiled threads that are pending deletion on JS shutdown and don't delete expired markers when resetting the profile buffer. (r=djvj) (90721313a)
2020-08-29 08:02:13 +08:00

2601 lines
77 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var Ci = Components.interfaces;
var Cc = Components.classes;
var Cu = Components.utils;
var Cr = Components.results;
var CC = Components.Constructor;
// On B2G scope object misbehaves and we have to bind globals to `this`
// in order to ensure theses variable to be visible in transport.js
this.Ci = Ci;
this.Cc = Cc;
this.Cu = Cu;
this.Cr = Cr;
this.CC = CC;
this.EXPORTED_SYMBOLS = ["DebuggerTransport",
"DebuggerClient",
"RootClient",
"LongStringClient",
"EnvironmentClient",
"ObjectClient"];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/Services.jsm");
let promise = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js").Promise;
const { defer, resolve, reject } = promise;
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/devtools/Console.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "devtools",
"resource://gre/modules/devtools/Loader.jsm");
XPCOMUtils.defineLazyGetter(this, "events", () => {
return devtools.require("sdk/event/core");
});
Object.defineProperty(this, "WebConsoleClient", {
get: function () {
return devtools.require("devtools/toolkit/webconsole/client").WebConsoleClient;
},
configurable: true,
enumerable: true
});
Components.utils.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
this.makeInfallible = DevToolsUtils.makeInfallible;
this.values = DevToolsUtils.values;
let LOG_PREF = "devtools.debugger.log";
let VERBOSE_PREF = "devtools.debugger.log.verbose";
let wantLogging = Services.prefs.getBoolPref(LOG_PREF);
let wantVerbose =
Services.prefs.getPrefType(VERBOSE_PREF) !== Services.prefs.PREF_INVALID &&
Services.prefs.getBoolPref(VERBOSE_PREF);
const noop = () => {};
function dumpn(str) {
if (wantLogging) {
dump("DBG-CLIENT: " + str + "\n");
}
}
function dumpv(msg) {
if (wantVerbose) {
dumpn(msg);
}
}
let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Ci.mozIJSSubScriptLoader);
loader.loadSubScript("resource://gre/modules/devtools/transport/transport.js", this);
DevToolsUtils.defineLazyGetter(this, "DebuggerSocket", () => {
let { DebuggerSocket } = devtools.require("devtools/toolkit/security/socket");
return DebuggerSocket;
});
DevToolsUtils.defineLazyGetter(this, "Authentication", () => {
return devtools.require("devtools/toolkit/security/auth");
});
/**
* TODO: Get rid of this API in favor of EventTarget (bug 1042642)
*
* 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.
* @param aListener function
* Called when the event is fired. If the same listener
* is added more than once, it will be called once per
* addListener call.
*/
aProto.addListener = function (aName, aListener) {
if (typeof aListener != "function") {
throw TypeError("Listeners must be functions.");
}
if (!this._listeners) {
this._listeners = {};
}
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.
* @param aListener function
* Called when the event is fired.
*/
aProto.addOneTimeListener = function (aName, aListener) {
let l = (...args) => {
this.removeListener(aName, l);
aListener.apply(null, args);
};
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 (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 (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.emit = function () {
if (!this._listeners) {
return;
}
let name = arguments[0];
let listeners = this._getListeners(name).slice(0);
for each (let listener in listeners) {
try {
listener.apply(null, arguments);
} catch (e) {
// Prevent a bad listener from interfering with the others.
DevToolsUtils.reportException("notify event '" + name + "'", 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 protocol messages that are sent by the server without a prior request
* by the client.
*/
const UnsolicitedNotifications = {
"consoleAPICall": "consoleAPICall",
"eventNotification": "eventNotification",
"fileActivity": "fileActivity",
"lastPrivateContextExited": "lastPrivateContextExited",
"logMessage": "logMessage",
"networkEvent": "networkEvent",
"networkEventUpdate": "networkEventUpdate",
"newGlobal": "newGlobal",
"newScript": "newScript",
"newSource": "newSource",
"tabDetached": "tabDetached",
"tabListChanged": "tabListChanged",
"reflowActivity": "reflowActivity",
"addonListChanged": "addonListChanged",
"tabNavigated": "tabNavigated",
"frameUpdate": "frameUpdate",
"pageError": "pageError",
"documentLoad": "documentLoad",
"enteredFrame": "enteredFrame",
"exitedFrame": "exitedFrame",
"appOpen": "appOpen",
"appClose": "appClose",
"appInstall": "appInstall",
"appUninstall": "appUninstall",
"evaluationResult": "evaluationResult",
};
/**
* Set of pause types that are sent by the server and not as an immediate
* response to a client request.
*/
const UnsolicitedPauses = {
"resumeLimit": "resumeLimit",
"debuggerStatement": "debuggerStatement",
"breakpoint": "breakpoint",
"DOMEvent": "DOMEvent",
"watchpoint": "watchpoint",
"exception": "exception"
};
/**
* 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.
*/
this.DebuggerClient = function (aTransport)
{
this._transport = aTransport;
this._transport.hooks = this;
// Map actor ID to client instance for each actor type.
this._clients = new Map();
this._pendingRequests = new Map();
this._activeRequests = new Map();
this._eventsEnabled = true;
this.traits = {};
this.request = this.request.bind(this);
this.localTransport = this._transport.onOutputStreamReady === undefined;
/*
* As the first thing on the connection, expect a greeting packet from
* the connection's root actor.
*/
this.mainRoot = null;
this.expectReply("root", (aPacket) => {
this.mainRoot = new RootClient(this, aPacket);
this.emit("connected", aPacket.applicationType, aPacket.traits);
});
}
/**
* A declarative helper for defining methods that send requests to the server.
*
* @param aPacketSkeleton
* The form of the packet to send. Can specify fields to be filled from
* the parameters by using the |args| function.
* @param before
* The function to call before sending the packet. Is passed the packet,
* and the return value is used as the new packet. The |this| context is
* the instance of the client object we are defining a method for.
* @param after
* The function to call after the response is received. It is passed the
* response, and the return value is considered the new response that
* will be passed to the callback. The |this| context is the instance of
* the client object we are defining a method for.
*/
DebuggerClient.requester = function (aPacketSkeleton,
{ before, after }) {
return DevToolsUtils.makeInfallible(function (...args) {
let outgoingPacket = {
to: aPacketSkeleton.to || this.actor
};
let maxPosition = -1;
for (let k of Object.keys(aPacketSkeleton)) {
if (aPacketSkeleton[k] instanceof DebuggerClient.Argument) {
let { position } = aPacketSkeleton[k];
outgoingPacket[k] = aPacketSkeleton[k].getArgument(args);
maxPosition = Math.max(position, maxPosition);
} else {
outgoingPacket[k] = aPacketSkeleton[k];
}
}
if (before) {
outgoingPacket = before.call(this, outgoingPacket);
}
this.request(outgoingPacket, DevToolsUtils.makeInfallible((aResponse) => {
if (after) {
let { from } = aResponse;
aResponse = after.call(this, aResponse);
if (!aResponse.from) {
aResponse.from = from;
}
}
// The callback is always the last parameter.
let thisCallback = args[maxPosition + 1];
if (thisCallback) {
thisCallback(aResponse);
}
}, "DebuggerClient.requester request callback"));
}, "DebuggerClient.requester");
};
function args(aPos) {
return new DebuggerClient.Argument(aPos);
}
DebuggerClient.Argument = function (aPosition) {
this.position = aPosition;
};
DebuggerClient.Argument.prototype.getArgument = function (aParams) {
if (!(this.position in aParams)) {
throw new Error("Bad index into params: " + this.position);
}
return aParams[this.position];
};
// Expose these to save callers the trouble of importing DebuggerSocket
DebuggerClient.socketConnect = function(options) {
// Defined here instead of just copying the function to allow lazy-load
return DebuggerSocket.connect(options);
};
DevToolsUtils.defineLazyGetter(DebuggerClient, "Authenticators", () => {
return Authentication.Authenticators;
});
DevToolsUtils.defineLazyGetter(DebuggerClient, "AuthenticationResult", () => {
return Authentication.AuthenticationResult;
});
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 (aOnConnected) {
this.emit("connect");
// Also emit the event on the |DebuggerServer| object (not on
// the instance), so it's possible to track all instances.
events.emit(DebuggerClient, "connect", this);
this.addOneTimeListener("connected", (aName, aApplicationType, aTraits) => {
this.traits = aTraits;
if (aOnConnected) {
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 (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();
});
}
// Call each client's `detach` method by calling
// lastly registered ones first to give a chance
// to detach child clients first.
let clients = [...this._clients.values()];
this._clients.clear();
const detachClients = () => {
let client = clients.pop();
if (!client) {
// All clients detached.
this._transport.close();
this._transport = null;
this._activeRequests.clear();
this._activeRequests = null;
this._pendingRequests.clear();
this._pendingRequests = null;
return;
}
if (client.detach) {
client.detach(detachClients);
return;
}
detachClients();
};
detachClients();
},
/*
* This function exists only to preserve DebuggerClient's interface;
* new code should say 'client.mainRoot.listTabs()'.
*/
listTabs: function (aOnResponse) { return this.mainRoot.listTabs(aOnResponse); },
/*
* This function exists only to preserve DebuggerClient's interface;
* new code should say 'client.mainRoot.listAddons()'.
*/
listAddons: function (aOnResponse) { return this.mainRoot.listAddons(aOnResponse); },
/**
* 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 (aTabActor, aOnResponse = noop) {
if (this._clients.has(aTabActor)) {
let cachedTab = this._clients.get(aTabActor);
let cachedResponse = {
cacheDisabled: cachedTab.cacheDisabled,
javascriptEnabled: cachedTab.javascriptEnabled,
traits: cachedTab.traits,
};
DevToolsUtils.executeSoon(() => aOnResponse(cachedResponse, cachedTab));
return;
}
let packet = {
to: aTabActor,
type: "attach"
};
this.request(packet, (aResponse) => {
let tabClient;
if (!aResponse.error) {
tabClient = new TabClient(this, aResponse);
this.registerClient(tabClient);
}
aOnResponse(aResponse, tabClient);
});
},
/**
* Attach to an addon actor.
*
* @param string aAddonActor
* The actor ID for the addon to attach.
* @param function aOnResponse
* Called with the response packet and a AddonClient
* (which will be undefined on error).
*/
attachAddon: function DC_attachAddon(aAddonActor, aOnResponse = noop) {
let packet = {
to: aAddonActor,
type: "attach"
};
this.request(packet, aResponse => {
let addonClient;
if (!aResponse.error) {
addonClient = new AddonClient(this, aAddonActor);
this.registerClient(addonClient);
this.activeAddon = addonClient;
}
aOnResponse(aResponse, addonClient);
});
},
/**
* Attach to a Web Console actor.
*
* @param string aConsoleActor
* The ID for the console actor to attach to.
* @param array aListeners
* The console listeners you want to start.
* @param function aOnResponse
* Called with the response packet and a WebConsoleClient
* instance (which will be undefined on error).
*/
attachConsole:
function (aConsoleActor, aListeners, aOnResponse = noop) {
let packet = {
to: aConsoleActor,
type: "startListeners",
listeners: aListeners,
};
this.request(packet, (aResponse) => {
let consoleClient;
if (!aResponse.error) {
if (this._clients.has(aConsoleActor)) {
consoleClient = this._clients.get(aConsoleActor);
} else {
consoleClient = new WebConsoleClient(this, aResponse);
this.registerClient(consoleClient);
}
}
aOnResponse(aResponse, consoleClient);
});
},
/**
* Attach to a global-scoped thread actor for chrome debugging.
*
* @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).
* @param object aOptions
* Configuration options.
* - useSourceMaps: whether to use source maps or not.
*/
attachThread: function (aThreadActor, aOnResponse = noop, aOptions={}) {
if (this._clients.has(aThreadActor)) {
DevToolsUtils.executeSoon(() => aOnResponse({}, this._clients.get(aThreadActor)));
return;
}
let packet = {
to: aThreadActor,
type: "attach",
options: aOptions
};
this.request(packet, (aResponse) => {
if (!aResponse.error) {
var threadClient = new ThreadClient(this, aThreadActor);
this.registerClient(threadClient);
}
aOnResponse(aResponse, threadClient);
});
},
/**
* Attach to a trace actor.
*
* @param string aTraceActor
* The actor ID for the tracer to attach.
* @param function aOnResponse
* Called with the response packet and a TraceClient
* (which will be undefined on error).
*/
attachTracer: function (aTraceActor, aOnResponse = noop) {
if (this._clients.has(aTraceActor)) {
DevToolsUtils.executeSoon(() => aOnResponse({}, this._clients.get(aTraceActor)));
return;
}
let packet = {
to: aTraceActor,
type: "attach"
};
this.request(packet, (aResponse) => {
if (!aResponse.error) {
var traceClient = new TraceClient(this, aTraceActor);
this.registerClient(traceClient);
}
aOnResponse(aResponse, traceClient);
});
},
/**
* Fetch the ChromeActor for the main process or ChildProcessActor for a
* a given child process ID.
*
* @param number aId
* The ID for the process to attach (returned by `listProcesses`).
* Connected to the main process if omitted, or is 0.
*/
getProcess: function (aId) {
let packet = {
to: "root",
type: "getProcess"
}
if (typeof(aId) == "number") {
packet.id = aId;
}
return this.request(packet);
},
/**
* Release an object actor.
*
* @param string aActor
* The actor ID to send the request to.
* @param aOnResponse function
* If specified, will be called with the response packet when
* debugging server responds.
*/
release: DebuggerClient.requester({
to: args(0),
type: "release"
}, { }),
/**
* 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 JSON response packet when
* debugging server responds.
* @return Request
* This object emits a number of events to allow you to respond to
* different parts of the request lifecycle.
* It is also a Promise object, with a `then` method, that is resolved
* whenever a JSON or a Bulk response is received; and is rejected
* if the response is an error.
* Note: This return value can be ignored if you are using JSON alone,
* because the callback provided in |aOnResponse| will be bound to the
* "json-reply" event automatically.
*
* Events emitted:
* * json-reply: The server replied with a JSON packet, which is
* passed as event data.
* * bulk-reply: The server replied with bulk data, which you can read
* using the event data object containing:
* * actor: Name of actor that received the packet
* * type: Name of actor's method that was called on receipt
* * length: Size of the data to be read
* * stream: This input stream should only be used directly if you
* can ensure that you will read exactly |length| bytes
* and will not close the stream when reading is complete
* * done: If you use the stream directly (instead of |copyTo|
* below), you must signal completion by resolving /
* rejecting this deferred. If it's rejected, the
* transport will be closed. If an Error is supplied as a
* rejection value, it will be logged via |dumpn|. If you
* do use |copyTo|, resolving is taken care of for you
* when copying completes.
* * copyTo: A helper function for getting your data out of the
* stream that meets the stream handling requirements
* above, and has the following signature:
* @param output nsIAsyncOutputStream
* The stream to copy to.
* @return Promise
* The promise is resolved when copying completes or
* rejected if any (unexpected) errors occur.
* This object also emits "progress" events for each chunk
* that is copied. See stream-utils.js.
*/
request: function (aRequest, aOnResponse) {
if (!this.mainRoot) {
throw Error("Have not yet received a hello packet from the server.");
}
if (!aRequest.to) {
let type = aRequest.type || "";
throw Error("'" + type + "' request packet has no destination.");
}
let request = new Request(aRequest);
request.format = "json";
if (aOnResponse) {
request.on("json-reply", aOnResponse);
}
this._sendOrQueueRequest(request);
// Implement a Promise like API on the returned object
// that resolves/rejects on request response
let deferred = promise.defer();
function listenerJson(resp) {
request.off("json-reply", listenerJson);
request.off("bulk-reply", listenerBulk);
if (resp.error) {
deferred.reject(resp);
} else {
deferred.resolve(resp);
}
}
function listenerBulk(resp) {
request.off("json-reply", listenerJson);
request.off("bulk-reply", listenerBulk);
deferred.resolve(resp);
}
request.on("json-reply", listenerJson);
request.on("bulk-reply", listenerBulk);
request.then = deferred.promise.then.bind(deferred.promise);
return request;
},
/**
* Transmit streaming data via a bulk request.
*
* This method initiates the bulk send process by queuing up the header data.
* The caller receives eventual access to a stream for writing.
*
* Since this opens up more options for how the server might respond (it could
* send back either JSON or bulk data), and the returned Request object emits
* events for different stages of the request process that you may want to
* react to.
*
* @param request Object
* This is modeled after the format of JSON packets above, but does not
* actually contain the data, but is instead just a routing header:
* * actor: Name of actor that will receive the packet
* * type: Name of actor's method that should be called on receipt
* * length: Size of the data to be sent
* @return Request
* This object emits a number of events to allow you to respond to
* different parts of the request lifecycle.
*
* Events emitted:
* * bulk-send-ready: Ready to send bulk data to the server, using the
* event data object containing:
* * stream: This output stream should only be used directly if
* you can ensure that you will write exactly |length|
* bytes and will not close the stream when writing is
* complete
* * done: If you use the stream directly (instead of |copyFrom|
* below), you must signal completion by resolving /
* rejecting this deferred. If it's rejected, the
* transport will be closed. If an Error is supplied as
* a rejection value, it will be logged via |dumpn|. If
* you do use |copyFrom|, resolving is taken care of for
* you when copying completes.
* * copyFrom: A helper function for getting your data onto the
* stream that meets the stream handling requirements
* above, and has the following signature:
* @param input nsIAsyncInputStream
* The stream to copy from.
* @return Promise
* The promise is resolved when copying completes or
* rejected if any (unexpected) errors occur.
* This object also emits "progress" events for each chunk
* that is copied. See stream-utils.js.
* * json-reply: The server replied with a JSON packet, which is
* passed as event data.
* * bulk-reply: The server replied with bulk data, which you can read
* using the event data object containing:
* * actor: Name of actor that received the packet
* * type: Name of actor's method that was called on receipt
* * length: Size of the data to be read
* * stream: This input stream should only be used directly if you
* can ensure that you will read exactly |length| bytes
* and will not close the stream when reading is complete
* * done: If you use the stream directly (instead of |copyTo|
* below), you must signal completion by resolving /
* rejecting this deferred. If it's rejected, the
* transport will be closed. If an Error is supplied as a
* rejection value, it will be logged via |dumpn|. If you
* do use |copyTo|, resolving is taken care of for you
* when copying completes.
* * copyTo: A helper function for getting your data out of the
* stream that meets the stream handling requirements
* above, and has the following signature:
* @param output nsIAsyncOutputStream
* The stream to copy to.
* @return Promise
* The promise is resolved when copying completes or
* rejected if any (unexpected) errors occur.
* This object also emits "progress" events for each chunk
* that is copied. See stream-utils.js.
*/
startBulkRequest: function(request) {
if (!this.traits.bulk) {
throw Error("Server doesn't support bulk transfers");
}
if (!this.mainRoot) {
throw Error("Have not yet received a hello packet from the server.");
}
if (!request.type) {
throw Error("Bulk packet is missing the required 'type' field.");
}
if (!request.actor) {
throw Error("'" + request.type + "' bulk packet has no destination.");
}
if (!request.length) {
throw Error("'" + request.type + "' bulk packet has no length.");
}
request = new Request(request);
request.format = "bulk";
this._sendOrQueueRequest(request);
return request;
},
/**
* If a new request can be sent immediately, do so. Otherwise, queue it.
*/
_sendOrQueueRequest(request) {
let actor = request.actor;
if (!this._activeRequests.has(actor)) {
this._sendRequest(request);
} else {
this._queueRequest(request);
}
},
/**
* Send a request.
* @throws Error if there is already an active request in flight for the same
* actor.
*/
_sendRequest(request) {
let actor = request.actor;
this.expectReply(actor, request);
if (request.format === "json") {
this._transport.send(request.request);
return false;
}
this._transport.startBulkSend(request.request).then((...args) => {
request.emit("bulk-send-ready", ...args);
});
},
/**
* Queue a request to be sent later. Queues are only drained when an in
* flight request to a given actor completes.
*/
_queueRequest(request) {
let actor = request.actor;
let queue = this._pendingRequests.get(actor) || [];
queue.push(request);
this._pendingRequests.set(actor, queue);
},
/**
* Attempt the next request to a given actor (if any).
*/
_attemptNextRequest(actor) {
if (this._activeRequests.has(actor)) {
return;
}
let queue = this._pendingRequests.get(actor);
if (!queue) {
return;
}
let request = queue.shift();
if (queue.length === 0) {
this._pendingRequests.delete(actor);
}
this._sendRequest(request);
},
/**
* Arrange to hand the next reply from |aActor| to the handler bound to
* |aRequest|.
*
* DebuggerClient.prototype.request / startBulkRequest usually takes care of
* establishing the handler for a given request, but in rare cases (well,
* greetings from new root actors, is the only case at the moment) we must be
* prepared for a "reply" that doesn't correspond to any request we sent.
*/
expectReply: function (aActor, aRequest) {
if (this._activeRequests.has(aActor)) {
throw Error("clashing handlers for next reply from " + uneval(aActor));
}
// If a handler is passed directly (as it is with the handler for the root
// actor greeting), create a dummy request to bind this to.
if (typeof aRequest === "function") {
let handler = aRequest;
aRequest = new Request();
aRequest.on("json-reply", handler);
}
this._activeRequests.set(aActor, aRequest);
},
// Transport hooks.
/**
* Called by DebuggerTransport to dispatch incoming packets as appropriate.
*
* @param aPacket object
* The incoming packet.
*/
onPacket: function (aPacket) {
if (!aPacket.from) {
DevToolsUtils.reportException(
"onPacket",
new Error("Server did not specify an actor, dropping packet: " +
JSON.stringify(aPacket)));
return;
}
// If we have a registered Front for this actor, let it handle the packet
// and skip all the rest of this unpleasantness.
let front = this.getActor(aPacket.from);
if (front) {
front.onPacket(aPacket);
return;
}
if (this._clients.has(aPacket.from) && aPacket.type) {
let client = this._clients.get(aPacket.from);
let type = aPacket.type;
if (client.events.indexOf(type) != -1) {
client.emit(type, aPacket);
// we ignore the rest, as the client is expected to handle this packet.
return;
}
}
let activeRequest;
// See if we have a handler function waiting for a reply from this
// actor. (Don't count unsolicited notifications or pauses as
// replies.)
if (this._activeRequests.has(aPacket.from) &&
!(aPacket.type in UnsolicitedNotifications) &&
!(aPacket.type == ThreadStateTypes.paused &&
aPacket.why.type in UnsolicitedPauses)) {
activeRequest = this._activeRequests.get(aPacket.from);
this._activeRequests.delete(aPacket.from);
}
// If there is a subsequent request for the same actor, hand it off to the
// transport. Delivery of packets on the other end is always async, even
// in the local transport case.
this._attemptNextRequest(aPacket.from);
// Packets that indicate thread state changes get special treatment.
if (aPacket.type in ThreadStateTypes &&
this._clients.has(aPacket.from) &&
typeof this._clients.get(aPacket.from)._onThreadState == "function") {
this._clients.get(aPacket.from)._onThreadState(aPacket);
}
// On navigation the server resumes, so the client must resume as well.
// We achieve that by generating a fake resumption packet that triggers
// the client's thread state change listeners.
if (aPacket.type == UnsolicitedNotifications.tabNavigated &&
this._clients.has(aPacket.from) &&
this._clients.get(aPacket.from).thread) {
let thread = this._clients.get(aPacket.from).thread;
let resumption = { from: thread._actor, type: "resumed" };
thread._onThreadState(resumption);
}
// Only try to notify listeners on events, not responses to requests
// that lack a packet type.
if (aPacket.type) {
this.emit(aPacket.type, aPacket);
}
if (activeRequest) {
activeRequest.emit("json-reply", aPacket);
}
},
/**
* Called by the DebuggerTransport to dispatch incoming bulk packets as
* appropriate.
*
* @param packet object
* The incoming packet, which contains:
* * actor: Name of actor that will receive the packet
* * type: Name of actor's method that should be called on receipt
* * length: Size of the data to be read
* * stream: This input stream should only be used directly if you can
* ensure that you will read exactly |length| bytes and will
* not close the stream when reading is complete
* * done: If you use the stream directly (instead of |copyTo|
* below), you must signal completion by resolving /
* rejecting this deferred. If it's rejected, the transport
* will be closed. If an Error is supplied as a rejection
* value, it will be logged via |dumpn|. If you do use
* |copyTo|, resolving is taken care of for you when copying
* completes.
* * copyTo: A helper function for getting your data out of the stream
* that meets the stream handling requirements above, and has
* the following signature:
* @param output nsIAsyncOutputStream
* The stream to copy to.
* @return Promise
* The promise is resolved when copying completes or rejected
* if any (unexpected) errors occur.
* This object also emits "progress" events for each chunk
* that is copied. See stream-utils.js.
*/
onBulkPacket: function(packet) {
let { actor, type, length } = packet;
if (!actor) {
DevToolsUtils.reportException(
"onBulkPacket",
new Error("Server did not specify an actor, dropping bulk packet: " +
JSON.stringify(packet)));
return;
}
// See if we have a handler function waiting for a reply from this
// actor.
if (!this._activeRequests.has(actor)) {
return;
}
let activeRequest = this._activeRequests.get(actor);
this._activeRequests.delete(actor);
// If there is a subsequent request for the same actor, hand it off to the
// transport. Delivery of packets on the other end is always async, even
// in the local transport case.
this._attemptNextRequest(actor);
activeRequest.emit("bulk-reply", packet);
},
/**
* 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 (aStatus) {
this.emit("closed");
// The |_pools| array on the client-side currently is used only by
// protocol.js to store active fronts, mirroring the actor pools found in
// the server. So, read all usages of "pool" as "protocol.js front".
//
// In the normal case where we shutdown cleanly, the toolbox tells each tool
// to close, and they each call |destroy| on any fronts they were using.
// When |destroy| or |cleanup| is called on a protocol.js front, it also
// removes itself from the |_pools| array. Once the toolbox has shutdown,
// the connection is closed, and we reach here. All fronts (should have
// been) |destroy|ed, so |_pools| should empty.
//
// If the connection instead aborts unexpectedly, we may end up here with
// all fronts used during the life of the connection. So, we call |cleanup|
// on them clear their state, reject pending requests, and remove themselves
// from |_pools|. This saves the toolbox from hanging indefinitely, in case
// it waits for some server response before shutdown that will now never
// arrive.
for (let pool of this._pools) {
pool.cleanup();
}
},
registerClient: function (client) {
let actorID = client.actor;
if (!actorID) {
throw new Error("DebuggerServer.registerClient expects " +
"a client instance with an `actor` attribute.");
}
if (!Array.isArray(client.events)) {
throw new Error("DebuggerServer.registerClient expects " +
"a client instance with an `events` attribute " +
"that is an array.");
}
if (client.events.length > 0 && typeof(client.emit) != "function") {
throw new Error("DebuggerServer.registerClient expects " +
"a client instance with non-empty `events` array to" +
"have an `emit` function.");
}
if (this._clients.has(actorID)) {
throw new Error("DebuggerServer.registerClient already registered " +
"a client for this actor.");
}
this._clients.set(actorID, client);
},
unregisterClient: function (client) {
let actorID = client.actor;
if (!actorID) {
throw new Error("DebuggerServer.unregisterClient expects " +
"a Client instance with a `actor` attribute.");
}
this._clients.delete(actorID);
},
/**
* Actor lifetime management, echos the server's actor pools.
*/
__pools: null,
get _pools() {
if (this.__pools) {
return this.__pools;
}
this.__pools = new Set();
return this.__pools;
},
addActorPool: function (pool) {
this._pools.add(pool);
},
removeActorPool: function (pool) {
this._pools.delete(pool);
},
getActor: function (actorID) {
let pool = this.poolFor(actorID);
return pool ? pool.get(actorID) : null;
},
poolFor: function (actorID) {
for (let pool of this._pools) {
if (pool.has(actorID)) return pool;
}
return null;
},
/**
* Currently attached addon.
*/
activeAddon: null
}
eventSource(DebuggerClient.prototype);
function Request(request) {
this.request = request;
}
Request.prototype = {
on: function(type, listener) {
events.on(this, type, listener);
},
off: function(type, listener) {
events.off(this, type, listener);
},
once: function(type, listener) {
events.once(this, type, listener);
},
emit: function(type, ...args) {
events.emit(this, type, ...args);
},
get actor() { return this.request.to || this.request.actor; }
};
/**
* 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 aForm object
* The protocol form for this tab.
*/
function TabClient(aClient, aForm) {
this.client = aClient;
this._actor = aForm.from;
this._threadActor = aForm.threadActor;
this.javascriptEnabled = aForm.javascriptEnabled;
this.cacheDisabled = aForm.cacheDisabled;
this.thread = null;
this.request = this.client.request;
this.traits = aForm.traits || {};
this.events = [];
}
TabClient.prototype = {
get actor() { return this._actor },
get _transport() { return this.client._transport; },
/**
* Attach to a thread actor.
*
* @param object aOptions
* Configuration options.
* - useSourceMaps: whether to use source maps or not.
* @param function aOnResponse
* Called with the response packet and a ThreadClient
* (which will be undefined on error).
*/
attachThread: function(aOptions={}, aOnResponse = noop) {
if (this.thread) {
DevToolsUtils.executeSoon(() => aOnResponse({}, this.thread));
return;
}
let packet = {
to: this._threadActor,
type: "attach",
options: aOptions
};
this.request(packet, (aResponse) => {
if (!aResponse.error) {
this.thread = new ThreadClient(this, this._threadActor);
this.client.registerClient(this.thread);
}
aOnResponse(aResponse, this.thread);
});
},
/**
* Detach the client from the tab actor.
*
* @param function aOnResponse
* Called with the response packet.
*/
detach: DebuggerClient.requester({
type: "detach"
}, {
before: function (aPacket) {
if (this.thread) {
this.thread.detach();
}
return aPacket;
},
after: function (aResponse) {
this.client.unregisterClient(this);
return aResponse;
}
}),
/**
* Reload the page in this tab.
*
* @param [optional] object options
* An object with a `force` property indicating whether or not
* this reload should skip the cache
*/
reload: function(options = { force: false }) {
return this._reload(options);
},
_reload: DebuggerClient.requester({
type: "reload",
options: args(0)
}, { }),
/**
* Navigate to another URL.
*
* @param string url
* The URL to navigate to.
*/
navigateTo: DebuggerClient.requester({
type: "navigateTo",
url: args(0)
}, { }),
/**
* Reconfigure the tab actor.
*
* @param object aOptions
* A dictionary object of the new options to use in the tab actor.
* @param function aOnResponse
* Called with the response packet.
*/
reconfigure: DebuggerClient.requester({
type: "reconfigure",
options: args(0)
}, { }),
};
eventSource(TabClient.prototype);
function AddonClient(aClient, aActor) {
this._client = aClient;
this._actor = aActor;
this.request = this._client.request;
this.events = [];
}
AddonClient.prototype = {
get actor() { return this._actor; },
get _transport() { return this._client._transport; },
/**
* Detach the client from the addon actor.
*
* @param function aOnResponse
* Called with the response packet.
*/
detach: DebuggerClient.requester({
type: "detach"
}, {
after: function(aResponse) {
if (this._client.activeAddon === this) {
this._client.activeAddon = null
}
this._client.unregisterClient(this);
return aResponse;
}
})
};
/**
* A RootClient object represents a root actor on the server. Each
* DebuggerClient keeps a RootClient instance representing the root actor
* for the initial connection; DebuggerClient's 'listTabs' and
* 'listChildProcesses' methods forward to that root actor.
*
* @param aClient object
* The client connection to which this actor belongs.
* @param aGreeting string
* The greeting packet from the root actor we're to represent.
*
* Properties of a RootClient instance:
*
* @property actor string
* The name of this child's root actor.
* @property applicationType string
* The application type, as given in the root actor's greeting packet.
* @property traits object
* The traits object, as given in the root actor's greeting packet.
*/
function RootClient(aClient, aGreeting) {
this._client = aClient;
this.actor = aGreeting.from;
this.applicationType = aGreeting.applicationType;
this.traits = aGreeting.traits;
}
RootClient.prototype = {
constructor: RootClient,
/**
* List the open tabs.
*
* @param function aOnResponse
* Called with the response packet.
*/
listTabs: DebuggerClient.requester({ type: "listTabs" },
{ }),
/**
* List the installed addons.
*
* @param function aOnResponse
* Called with the response packet.
*/
listAddons: DebuggerClient.requester({ type: "listAddons" },
{ }),
/**
* List the running processes.
*
* @param function aOnResponse
* Called with the response packet.
*/
listProcesses: DebuggerClient.requester({ type: "listProcesses" },
{ }),
/**
* Description of protocol's actors and methods.
*
* @param function aOnResponse
* Called with the response packet.
*/
protocolDescription: DebuggerClient.requester({ type: "protocolDescription" },
{ }),
/*
* Methods constructed by DebuggerClient.requester require these forwards
* on their 'this'.
*/
get _transport() { return this._client._transport; },
get request() { return this._client.request; }
};
/**
* 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|TabClient
* The parent of the thread (tab for tab-scoped debuggers, DebuggerClient
* for chrome debuggers).
* @param aActor string
* The actor ID for this thread.
*/
function ThreadClient(aClient, aActor) {
this._parent = aClient;
this.client = aClient instanceof DebuggerClient ? aClient : aClient.client;
this._actor = aActor;
this._frameCache = [];
this._scriptCache = {};
this._pauseGrips = {};
this._threadGrips = {};
this.request = this.client.request;
this.events = [];
}
ThreadClient.prototype = {
_state: "paused",
get state() { return this._state; },
get paused() { return this._state === "paused"; },
_pauseOnExceptions: false,
_ignoreCaughtExceptions: false,
_pauseOnDOMEvents: null,
_actor: null,
get actor() { return this._actor; },
get _transport() { return this.client._transport; },
_assertPaused: function (aCommand) {
if (!this.paused) {
throw Error(aCommand + " command sent while not paused. Currently " + this._state);
}
},
/**
* Resume a paused thread. If the optional aLimit parameter is present, then
* the thread will also pause when that limit is reached.
*
* @param [optional] object aLimit
* An object with a type property set to the appropriate limit (next,
* step, or finish) per the remote debugging protocol specification.
* Use null to specify no limit.
* @param function aOnResponse
* Called with the response packet.
*/
_doResume: DebuggerClient.requester({
type: "resume",
resumeLimit: args(0)
}, {
before: function (aPacket) {
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";
if (this._pauseOnExceptions) {
aPacket.pauseOnExceptions = this._pauseOnExceptions;
}
if (this._ignoreCaughtExceptions) {
aPacket.ignoreCaughtExceptions = this._ignoreCaughtExceptions;
}
if (this._pauseOnDOMEvents) {
aPacket.pauseOnDOMEvents = this._pauseOnDOMEvents;
}
return aPacket;
},
after: function (aResponse) {
if (aResponse.error) {
// There was an error resuming, back to paused state.
this._state = "paused";
}
return aResponse;
}
}),
/**
* Reconfigure the thread actor.
*
* @param object aOptions
* A dictionary object of the new options to use in the thread actor.
* @param function aOnResponse
* Called with the response packet.
*/
reconfigure: DebuggerClient.requester({
type: "reconfigure",
options: args(0)
}, { }),
/**
* Resume a paused thread.
*/
resume: function (aOnResponse) {
this._doResume(null, aOnResponse);
},
/**
* Resume then pause without stepping.
*
* @param function aOnResponse
* Called with the response packet.
*/
breakOnNext: function (aOnResponse) {
this._doResume({ type: "break" }, aOnResponse);
},
/**
* Step over a function call.
*
* @param function aOnResponse
* Called with the response packet.
*/
stepOver: function (aOnResponse) {
this._doResume({ type: "next" }, aOnResponse);
},
/**
* Step into a function call.
*
* @param function aOnResponse
* Called with the response packet.
*/
stepIn: function (aOnResponse) {
this._doResume({ type: "step" }, aOnResponse);
},
/**
* Step out of a function call.
*
* @param function aOnResponse
* Called with the response packet.
*/
stepOut: function (aOnResponse) {
this._doResume({ type: "finish" }, aOnResponse);
},
/**
* Interrupt a running thread.
*
* @param function aOnResponse
* Called with the response packet.
*/
interrupt: DebuggerClient.requester({
type: "interrupt"
}, { }),
/**
* Enable or disable pausing when an exception is thrown.
*
* @param boolean aFlag
* Enables pausing if true, disables otherwise.
* @param function aOnResponse
* Called with the response packet.
*/
pauseOnExceptions: function (aPauseOnExceptions,
aIgnoreCaughtExceptions,
aOnResponse = noop) {
this._pauseOnExceptions = aPauseOnExceptions;
this._ignoreCaughtExceptions = aIgnoreCaughtExceptions;
// If the debuggee is paused, we have to send the flag via a reconfigure
// request.
if (this.paused) {
this.reconfigure({
pauseOnExceptions: aPauseOnExceptions,
ignoreCaughtExceptions: aIgnoreCaughtExceptions
}, aOnResponse);
return;
}
// Otherwise send the flag using a standard resume request.
this.interrupt(aResponse => {
if (aResponse.error) {
// Can't continue if pausing failed.
aOnResponse(aResponse);
return;
}
this.resume(aOnResponse);
});
},
/**
* Enable pausing when the specified DOM events are triggered. Disabling
* pausing on an event can be realized by calling this method with the updated
* array of events that doesn't contain it.
*
* @param array|string events
* An array of strings, representing the DOM event types to pause on,
* or "*" to pause on all DOM events. Pass an empty array to
* completely disable pausing on DOM events.
* @param function onResponse
* Called with the response packet in a future turn of the event loop.
*/
pauseOnDOMEvents: function (events, onResponse = noop) {
this._pauseOnDOMEvents = events;
// If the debuggee is paused, the value of the array will be communicated in
// the next resumption. Otherwise we have to force a pause in order to send
// the array.
if (this.paused) {
DevToolsUtils.executeSoon(() => onResponse({}));
return;
}
this.interrupt(response => {
// Can't continue if pausing failed.
if (response.error) {
onResponse(response);
return;
}
this.resume(onResponse);
});
},
/**
* Send a clientEvaluate packet to the debuggee. Response
* will be a resume packet.
*
* @param string aFrame
* The actor ID of the frame where the evaluation should take place.
* @param string aExpression
* The expression that will be evaluated in the scope of the frame
* above.
* @param function aOnResponse
* Called with the response packet.
*/
eval: DebuggerClient.requester({
type: "clientEvaluate",
frame: args(0),
expression: args(1)
}, {
before: function (aPacket) {
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";
return aPacket;
},
after: function (aResponse) {
if (aResponse.error) {
// There was an error resuming, back to paused state.
this._state = "paused";
}
return aResponse;
}
}),
/**
* Detach from the thread actor.
*
* @param function aOnResponse
* Called with the response packet.
*/
detach: DebuggerClient.requester({
type: "detach"
}, {
after: function (aResponse) {
this.client.unregisterClient(this);
this._parent.thread = null;
return aResponse;
}
}),
/**
* Release multiple thread-lifetime object actors. If any pause-lifetime
* actors are included in the request, a |notReleasable| error will return,
* but all the thread-lifetime ones will have been released.
*
* @param array actors
* An array with actor IDs to release.
*/
releaseMany: DebuggerClient.requester({
type: "releaseMany",
actors: args(0),
}, { }),
/**
* Promote multiple pause-lifetime object actors to thread-lifetime ones.
*
* @param array actors
* An array with actor IDs to promote.
*/
threadGrips: DebuggerClient.requester({
type: "threadGrips",
actors: args(0)
}, { }),
/**
* Return the event listeners defined on the page.
*
* @param aOnResponse Function
* Called with the thread's response.
*/
eventListeners: DebuggerClient.requester({
type: "eventListeners"
}, { }),
/**
* Request the loaded sources for the current thread.
*
* @param aOnResponse Function
* Called with the thread's response.
*/
getSources: DebuggerClient.requester({
type: "sources"
}, { }),
/**
* Clear the thread's source script cache. A scriptscleared event
* will be sent.
*/
_clearScripts: function () {
if (Object.keys(this._scriptCache).length > 0) {
this._scriptCache = {}
this.emit("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: DebuggerClient.requester({
type: "frames",
start: args(0),
count: args(1)
}, { }),
/**
* 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.
* @param aCallback function
* Optional callback function called when frames have been loaded
* @returns true if a framesadded notification should be expected.
*/
fillFrames: function (aTotal, aCallback=noop) {
this._assertPaused("fillFrames");
if (this._frameCache.length >= aTotal) {
return false;
}
let numFrames = this._frameCache.length;
this.getFrames(numFrames, aTotal - numFrames, (aResponse) => {
if (aResponse.error) {
aCallback(aResponse);
return;
}
let threadGrips = values(this._threadGrips);
for (let i in aResponse.frames) {
let frame = aResponse.frames[i];
if (!frame.where.source) {
// Older servers use urls instead, so we need to resolve
// them to source actors
for (let grip of threadGrips) {
if (grip instanceof SourceClient && grip.url === frame.url) {
frame.where.source = grip._form;
}
}
}
this._frameCache[frame.depth] = frame;
}
// If we got as many frames as we asked for, there might be more
// frames available.
this.emit("framesadded");
aCallback(aResponse);
});
return true;
},
/**
* Clear the thread's stack frame cache. A framescleared event
* will be sent.
*/
_clearFrames: function () {
if (this._frameCache.length > 0) {
this._frameCache = [];
this.emit("framescleared");
}
},
/**
* Return a ObjectClient object for the given object grip.
*
* @param aGrip object
* A pause-lifetime object grip returned by the protocol.
*/
pauseGrip: function (aGrip) {
if (aGrip.actor in this._pauseGrips) {
return this._pauseGrips[aGrip.actor];
}
let client = new ObjectClient(this.client, aGrip);
this._pauseGrips[aGrip.actor] = client;
return client;
},
/**
* Get or create a long string client, checking the grip client cache if it
* already exists.
*
* @param aGrip Object
* The long string grip returned by the protocol.
* @param aGripCacheName String
* The property name of the grip client cache to check for existing
* clients in.
*/
_longString: function (aGrip, aGripCacheName) {
if (aGrip.actor in this[aGripCacheName]) {
return this[aGripCacheName][aGrip.actor];
}
let client = new LongStringClient(this.client, aGrip);
this[aGripCacheName][aGrip.actor] = client;
return client;
},
/**
* Return an instance of LongStringClient for the given long string grip that
* is scoped to the current pause.
*
* @param aGrip Object
* The long string grip returned by the protocol.
*/
pauseLongString: function (aGrip) {
return this._longString(aGrip, "_pauseGrips");
},
/**
* Return an instance of LongStringClient for the given long string grip that
* is scoped to the thread lifetime.
*
* @param aGrip Object
* The long string grip returned by the protocol.
*/
threadLongString: function (aGrip) {
return this._longString(aGrip, "_threadGrips");
},
/**
* Clear and invalidate all the grip clients from the given cache.
*
* @param aGripCacheName
* The property name of the grip cache we want to clear.
*/
_clearObjectClients: function (aGripCacheName) {
for each (let grip in this[aGripCacheName]) {
grip.valid = false;
}
this[aGripCacheName] = {};
},
/**
* Invalidate pause-lifetime grip clients and clear the list of current grip
* clients.
*/
_clearPauseGrips: function () {
this._clearObjectClients("_pauseGrips");
},
/**
* Invalidate thread-lifetime grip clients and clear the list of current grip
* clients.
*/
_clearThreadGrips: function () {
this._clearObjectClients("_threadGrips");
},
/**
* Handle thread state change by doing necessary cleanup and notifying all
* registered listeners.
*/
_onThreadState: function (aPacket) {
this._state = ThreadStateTypes[aPacket.type];
this._clearFrames();
this._clearPauseGrips();
aPacket.type === ThreadStateTypes.detached && this._clearThreadGrips();
this.client._eventsEnabled && this.emit(aPacket.type, aPacket);
},
/**
* Return an EnvironmentClient instance for the given environment actor form.
*/
environment: function (aForm) {
return new EnvironmentClient(this.client, aForm);
},
/**
* Return an instance of SourceClient for the given source actor form.
*/
source: function (aForm) {
if (aForm.actor in this._threadGrips) {
return this._threadGrips[aForm.actor];
}
return this._threadGrips[aForm.actor] = new SourceClient(this, aForm);
},
/**
* Request the prototype and own properties of mutlipleObjects.
*
* @param aOnResponse function
* Called with the request's response.
* @param actors [string]
* List of actor ID of the queried objects.
*/
getPrototypesAndProperties: DebuggerClient.requester({
type: "prototypesAndProperties",
actors: args(0)
}, { })
};
eventSource(ThreadClient.prototype);
/**
* Creates a tracing profiler client for the remote debugging protocol
* server. This client is a front to the trace actor created on 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 TraceClient(aClient, aActor) {
this._client = aClient;
this._actor = aActor;
this._activeTraces = new Set();
this._waitingPackets = new Map();
this._expectedPacket = 0;
this.request = this._client.request;
this.events = [];
}
TraceClient.prototype = {
get actor() { return this._actor; },
get tracing() { return this._activeTraces.size > 0; },
get _transport() { return this._client._transport; },
/**
* Detach from the trace actor.
*/
detach: DebuggerClient.requester({
type: "detach"
}, {
after: function (aResponse) {
this._client.unregisterClient(this);
return aResponse;
}
}),
/**
* Start a new trace.
*
* @param aTrace [string]
* An array of trace types to be recorded by the new trace.
*
* @param aName string
* The name of the new trace.
*
* @param aOnResponse function
* Called with the request's response.
*/
startTrace: DebuggerClient.requester({
type: "startTrace",
name: args(1),
trace: args(0)
}, {
after: function (aResponse) {
if (aResponse.error) {
return aResponse;
}
if (!this.tracing) {
this._waitingPackets.clear();
this._expectedPacket = 0;
}
this._activeTraces.add(aResponse.name);
return aResponse;
}
}),
/**
* End a trace. If a name is provided, stop the named
* trace. Otherwise, stop the most recently started trace.
*
* @param aName string
* The name of the trace to stop.
*
* @param aOnResponse function
* Called with the request's response.
*/
stopTrace: DebuggerClient.requester({
type: "stopTrace",
name: args(0)
}, {
after: function (aResponse) {
if (aResponse.error) {
return aResponse;
}
this._activeTraces.delete(aResponse.name);
return aResponse;
}
})
};
/**
* 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 ObjectClient(aClient, aGrip)
{
this._grip = aGrip;
this._client = aClient;
this.request = this._client.request;
}
ObjectClient.prototype = {
get actor() { return this._grip.actor },
get _transport() { return this._client._transport; },
valid: true,
get isFrozen() this._grip.frozen,
get isSealed() this._grip.sealed,
get isExtensible() this._grip.extensible,
getDefinitionSite: DebuggerClient.requester({
type: "definitionSite"
}, {
before: function (aPacket) {
if (this._grip.class != "Function") {
throw new Error("getDefinitionSite is only valid for function grips.");
}
return aPacket;
}
}),
/**
* Request the names of a function's formal parameters.
*
* @param aOnResponse function
* Called with an object of the form:
* { parameterNames:[<parameterName>, ...] }
* where each <parameterName> is the name of a parameter.
*/
getParameterNames: DebuggerClient.requester({
type: "parameterNames"
}, {
before: function (aPacket) {
if (this._grip["class"] !== "Function") {
throw new Error("getParameterNames is only valid for function grips.");
}
return aPacket;
}
}),
/**
* Request the names of the properties defined on the object and not its
* prototype.
*
* @param aOnResponse function Called with the request's response.
*/
getOwnPropertyNames: DebuggerClient.requester({
type: "ownPropertyNames"
}, { }),
/**
* Request the prototype and own properties of the object.
*
* @param aOnResponse function Called with the request's response.
*/
getPrototypeAndProperties: DebuggerClient.requester({
type: "prototypeAndProperties"
}, { }),
/**
* 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: DebuggerClient.requester({
type: "property",
name: args(0)
}, { }),
/**
* Request the prototype of the object.
*
* @param aOnResponse function Called with the request's response.
*/
getPrototype: DebuggerClient.requester({
type: "prototype"
}, { }),
/**
* Request the display string of the object.
*
* @param aOnResponse function Called with the request's response.
*/
getDisplayString: DebuggerClient.requester({
type: "displayString"
}, { }),
/**
* Request the scope of the object.
*
* @param aOnResponse function Called with the request's response.
*/
getScope: DebuggerClient.requester({
type: "scope"
}, {
before: function (aPacket) {
if (this._grip.class !== "Function") {
throw new Error("scope is only valid for function grips.");
}
return aPacket;
}
})
};
/**
* A LongStringClient provides a way to access "very long" strings from the
* debugger server.
*
* @param aClient DebuggerClient
* The debugger client parent.
* @param aGrip Object
* A pause-lifetime long string grip returned by the protocol.
*/
function LongStringClient(aClient, aGrip) {
this._grip = aGrip;
this._client = aClient;
this.request = this._client.request;
}
LongStringClient.prototype = {
get actor() { return this._grip.actor; },
get length() { return this._grip.length; },
get initial() { return this._grip.initial; },
get _transport() { return this._client._transport; },
valid: true,
/**
* Get the substring of this LongString from aStart to aEnd.
*
* @param aStart Number
* The starting index.
* @param aEnd Number
* The ending index.
* @param aCallback Function
* The function called when we receive the substring.
*/
substring: DebuggerClient.requester({
type: "substring",
start: args(0),
end: args(1)
}, { }),
};
/**
* A SourceClient provides a way to access the source text of a script.
*
* @param aClient ThreadClient
* The thread client parent.
* @param aForm Object
* The form sent across the remote debugging protocol.
*/
function SourceClient(aClient, aForm) {
this._form = aForm;
this._isBlackBoxed = aForm.isBlackBoxed;
this._isPrettyPrinted = aForm.isPrettyPrinted;
this._activeThread = aClient;
this._client = aClient.client;
}
SourceClient.prototype = {
get _transport() this._client._transport,
get isBlackBoxed() this._isBlackBoxed,
get isPrettyPrinted() this._isPrettyPrinted,
get actor() this._form.actor,
get request() this._client.request,
get url() this._form.url,
/**
* Black box this SourceClient's source.
*
* @param aCallback Function
* The callback function called when we receive the response from the server.
*/
blackBox: DebuggerClient.requester({
type: "blackbox"
}, {
after: function (aResponse) {
if (!aResponse.error) {
this._isBlackBoxed = true;
if (this._activeThread) {
this._activeThread.emit("blackboxchange", this);
}
}
return aResponse;
}
}),
/**
* Un-black box this SourceClient's source.
*
* @param aCallback Function
* The callback function called when we receive the response from the server.
*/
unblackBox: DebuggerClient.requester({
type: "unblackbox"
}, {
after: function (aResponse) {
if (!aResponse.error) {
this._isBlackBoxed = false;
if (this._activeThread) {
this._activeThread.emit("blackboxchange", this);
}
}
return aResponse;
}
}),
/**
* Get Executable Lines from a source
*
* @param aCallback Function
* The callback function called when we receive the response from the server.
*/
getExecutableLines: function(cb){
let packet = {
to: this._form.actor,
type: "getExecutableLines"
};
this._client.request(packet, res => {
cb(res.lines);
});
},
/**
* Get a long string grip for this SourceClient's source.
*/
source: function (aCallback) {
let packet = {
to: this._form.actor,
type: "source"
};
this._client.request(packet, aResponse => {
this._onSourceResponse(aResponse, aCallback)
});
},
/**
* Pretty print this source's text.
*/
prettyPrint: function (aIndent, aCallback) {
const packet = {
to: this._form.actor,
type: "prettyPrint",
indent: aIndent
};
this._client.request(packet, aResponse => {
if (!aResponse.error) {
this._isPrettyPrinted = true;
this._activeThread._clearFrames();
this._activeThread.emit("prettyprintchange", this);
}
this._onSourceResponse(aResponse, aCallback);
});
},
/**
* Stop pretty printing this source's text.
*/
disablePrettyPrint: function (aCallback) {
const packet = {
to: this._form.actor,
type: "disablePrettyPrint"
};
this._client.request(packet, aResponse => {
if (!aResponse.error) {
this._isPrettyPrinted = false;
this._activeThread._clearFrames();
this._activeThread.emit("prettyprintchange", this);
}
this._onSourceResponse(aResponse, aCallback);
});
},
_onSourceResponse: function (aResponse, aCallback) {
if (aResponse.error) {
aCallback(aResponse);
return;
}
if (typeof aResponse.source === "string") {
aCallback(aResponse);
return;
}
let { contentType, source } = aResponse;
let longString = this._activeThread.threadLongString(source);
longString.substring(0, longString.length, function (aResponse) {
if (aResponse.error) {
aCallback(aResponse);
return;
}
aCallback({
source: aResponse.substring,
contentType: contentType
});
});
},
/**
* Request to set a breakpoint in the specified location.
*
* @param object aLocation
* The location and condition of the breakpoint in
* the form of { line[, column, condition] }.
* @param function aOnResponse
* Called with the thread's response.
*/
setBreakpoint: function ({ line, column, condition }, aOnResponse = noop) {
// A helper function that sets the breakpoint.
let doSetBreakpoint = aCallback => {
let root = this._client.mainRoot;
let location = {
line: line,
column: column
};
let packet = {
to: this.actor,
type: "setBreakpoint",
location: location,
condition: condition
};
// Backwards compatibility: send the breakpoint request to the
// thread if the server doesn't support Debugger.Source actors.
if (!root.traits.debuggerSourceActors) {
packet.to = this._activeThread.actor;
packet.location.url = this.url;
}
this._client.request(packet, aResponse => {
// Ignoring errors, since the user may be setting a breakpoint in a
// dead script that will reappear on a page reload.
let bpClient;
if (aResponse.actor) {
bpClient = new BreakpointClient(
this._client,
this,
aResponse.actor,
location,
root.traits.conditionalBreakpoints ? condition : undefined
);
}
aOnResponse(aResponse, bpClient);
if (aCallback) {
aCallback();
}
});
};
// If the debuggee is paused, just set the breakpoint.
if (this._activeThread.paused) {
doSetBreakpoint();
return;
}
// Otherwise, force a pause in order to set the breakpoint.
this._activeThread.interrupt(aResponse => {
if (aResponse.error) {
// Can't set the breakpoint if pausing failed.
aOnResponse(aResponse);
return;
}
const { type, why } = aResponse;
const cleanUp = type == "paused" && why.type == "interrupted"
? () => this._activeThread.resume()
: noop;
doSetBreakpoint(cleanUp);
})
}
};
/**
* Breakpoint clients are used to remove breakpoints that are no longer used.
*
* @param aClient DebuggerClient
* The debugger client parent.
* @param aSourceClient SourceClient
* The source where this breakpoint exists
* @param aActor string
* The actor ID for this breakpoint.
* @param aLocation object
* The location of the breakpoint. This is an object with two properties:
* url and line.
* @param aCondition string
* The conditional expression of the breakpoint
*/
function BreakpointClient(aClient, aSourceClient, aActor, aLocation, aCondition) {
this._client = aClient;
this._actor = aActor;
this.location = aLocation;
this.location.actor = aSourceClient.actor;
this.location.url = aSourceClient.url;
this.source = aSourceClient;
this.request = this._client.request;
// The condition property should only exist if it's a truthy value
if (aCondition) {
this.condition = aCondition;
}
}
BreakpointClient.prototype = {
_actor: null,
get actor() { return this._actor; },
get _transport() { return this._client._transport; },
/**
* Remove the breakpoint from the server.
*/
remove: DebuggerClient.requester({
type: "delete"
}, { }),
/**
* Determines if this breakpoint has a condition
*/
hasCondition: function() {
let root = this._client.mainRoot;
// XXX bug 990137: We will remove support for client-side handling of
// conditional breakpoints
if (root.traits.conditionalBreakpoints) {
return "condition" in this;
} else {
return "conditionalExpression" in this;
}
},
/**
* Get the condition of this breakpoint. Currently we have to
* support locally emulated conditional breakpoints until the
* debugger servers are updated (see bug 990137). We used a
* different property when moving it server-side to ensure that we
* are testing the right code.
*/
getCondition: function() {
let root = this._client.mainRoot;
if (root.traits.conditionalBreakpoints) {
return this.condition;
} else {
return this.conditionalExpression;
}
},
/**
* Set the condition of this breakpoint
*/
setCondition: function(gThreadClient, aCondition) {
let root = this._client.mainRoot;
let deferred = promise.defer();
if (root.traits.conditionalBreakpoints) {
let info = {
line: this.location.line,
column: this.location.column,
condition: aCondition
};
// Remove the current breakpoint and add a new one with the
// condition.
this.remove(aResponse => {
if (aResponse && aResponse.error) {
deferred.reject(aResponse);
return;
}
this.source.setBreakpoint(info, (aResponse, aNewBreakpoint) => {
if (aResponse && aResponse.error) {
deferred.reject(aResponse);
} else {
deferred.resolve(aNewBreakpoint);
}
});
});
} else {
// The property shouldn't even exist if the condition is blank
if (aCondition === "") {
delete this.conditionalExpression;
}
else {
this.conditionalExpression = aCondition;
}
deferred.resolve(this);
}
return deferred.promise;
}
};
eventSource(BreakpointClient.prototype);
/**
* Environment clients are used to manipulate the lexical environment actors.
*
* @param aClient DebuggerClient
* The debugger client parent.
* @param aForm Object
* The form sent across the remote debugging protocol.
*/
function EnvironmentClient(aClient, aForm) {
this._client = aClient;
this._form = aForm;
this.request = this._client.request;
}
EnvironmentClient.prototype = {
get actor() this._form.actor,
get _transport() { return this._client._transport; },
/**
* Fetches the bindings introduced by this lexical environment.
*/
getBindings: DebuggerClient.requester({
type: "bindings"
}, { }),
/**
* Changes the value of the identifier whose name is name (a string) to that
* represented by value (a grip).
*/
assign: DebuggerClient.requester({
type: "assign",
name: args(0),
value: args(1)
}, { })
};
eventSource(EnvironmentClient.prototype);