|
1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 "use strict"; |
|
8 var Ci = Components.interfaces; |
|
9 var Cc = Components.classes; |
|
10 var Cu = Components.utils; |
|
11 var Cr = Components.results; |
|
12 // On B2G scope object misbehaves and we have to bind globals to `this` |
|
13 // in order to ensure theses variable to be visible in transport.js |
|
14 this.Ci = Ci; |
|
15 this.Cc = Cc; |
|
16 this.Cu = Cu; |
|
17 this.Cr = Cr; |
|
18 |
|
19 this.EXPORTED_SYMBOLS = ["DebuggerTransport", |
|
20 "DebuggerClient", |
|
21 "RootClient", |
|
22 "debuggerSocketConnect", |
|
23 "LongStringClient", |
|
24 "EnvironmentClient", |
|
25 "ObjectClient"]; |
|
26 |
|
27 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
28 Cu.import("resource://gre/modules/NetUtil.jsm"); |
|
29 Cu.import("resource://gre/modules/Services.jsm"); |
|
30 Cu.import("resource://gre/modules/Timer.jsm"); |
|
31 |
|
32 let promise = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js").Promise; |
|
33 const { defer, resolve, reject } = promise; |
|
34 |
|
35 XPCOMUtils.defineLazyServiceGetter(this, "socketTransportService", |
|
36 "@mozilla.org/network/socket-transport-service;1", |
|
37 "nsISocketTransportService"); |
|
38 |
|
39 XPCOMUtils.defineLazyModuleGetter(this, "console", |
|
40 "resource://gre/modules/devtools/Console.jsm"); |
|
41 |
|
42 XPCOMUtils.defineLazyModuleGetter(this, "devtools", |
|
43 "resource://gre/modules/devtools/Loader.jsm"); |
|
44 |
|
45 Object.defineProperty(this, "WebConsoleClient", { |
|
46 get: function () { |
|
47 return devtools.require("devtools/toolkit/webconsole/client").WebConsoleClient; |
|
48 }, |
|
49 configurable: true, |
|
50 enumerable: true |
|
51 }); |
|
52 |
|
53 Components.utils.import("resource://gre/modules/devtools/DevToolsUtils.jsm"); |
|
54 this.makeInfallible = DevToolsUtils.makeInfallible; |
|
55 |
|
56 let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log"); |
|
57 |
|
58 function dumpn(str) |
|
59 { |
|
60 if (wantLogging) { |
|
61 dump("DBG-CLIENT: " + str + "\n"); |
|
62 } |
|
63 } |
|
64 |
|
65 let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] |
|
66 .getService(Ci.mozIJSSubScriptLoader); |
|
67 loader.loadSubScript("resource://gre/modules/devtools/server/transport.js", this); |
|
68 |
|
69 /** |
|
70 * Add simple event notification to a prototype object. Any object that has |
|
71 * some use for event notifications or the observer pattern in general can be |
|
72 * augmented with the necessary facilities by passing its prototype to this |
|
73 * function. |
|
74 * |
|
75 * @param aProto object |
|
76 * The prototype object that will be modified. |
|
77 */ |
|
78 function eventSource(aProto) { |
|
79 /** |
|
80 * Add a listener to the event source for a given event. |
|
81 * |
|
82 * @param aName string |
|
83 * The event to listen for. |
|
84 * @param aListener function |
|
85 * Called when the event is fired. If the same listener |
|
86 * is added more than once, it will be called once per |
|
87 * addListener call. |
|
88 */ |
|
89 aProto.addListener = function (aName, aListener) { |
|
90 if (typeof aListener != "function") { |
|
91 throw TypeError("Listeners must be functions."); |
|
92 } |
|
93 |
|
94 if (!this._listeners) { |
|
95 this._listeners = {}; |
|
96 } |
|
97 |
|
98 this._getListeners(aName).push(aListener); |
|
99 }; |
|
100 |
|
101 /** |
|
102 * Add a listener to the event source for a given event. The |
|
103 * listener will be removed after it is called for the first time. |
|
104 * |
|
105 * @param aName string |
|
106 * The event to listen for. |
|
107 * @param aListener function |
|
108 * Called when the event is fired. |
|
109 */ |
|
110 aProto.addOneTimeListener = function (aName, aListener) { |
|
111 let l = (...args) => { |
|
112 this.removeListener(aName, l); |
|
113 aListener.apply(null, args); |
|
114 }; |
|
115 this.addListener(aName, l); |
|
116 }; |
|
117 |
|
118 /** |
|
119 * Remove a listener from the event source previously added with |
|
120 * addListener(). |
|
121 * |
|
122 * @param aName string |
|
123 * The event name used during addListener to add the listener. |
|
124 * @param aListener function |
|
125 * The callback to remove. If addListener was called multiple |
|
126 * times, all instances will be removed. |
|
127 */ |
|
128 aProto.removeListener = function (aName, aListener) { |
|
129 if (!this._listeners || !this._listeners[aName]) { |
|
130 return; |
|
131 } |
|
132 this._listeners[aName] = |
|
133 this._listeners[aName].filter(function (l) { return l != aListener }); |
|
134 }; |
|
135 |
|
136 /** |
|
137 * Returns the listeners for the specified event name. If none are defined it |
|
138 * initializes an empty list and returns that. |
|
139 * |
|
140 * @param aName string |
|
141 * The event name. |
|
142 */ |
|
143 aProto._getListeners = function (aName) { |
|
144 if (aName in this._listeners) { |
|
145 return this._listeners[aName]; |
|
146 } |
|
147 this._listeners[aName] = []; |
|
148 return this._listeners[aName]; |
|
149 }; |
|
150 |
|
151 /** |
|
152 * Notify listeners of an event. |
|
153 * |
|
154 * @param aName string |
|
155 * The event to fire. |
|
156 * @param arguments |
|
157 * All arguments will be passed along to the listeners, |
|
158 * including the name argument. |
|
159 */ |
|
160 aProto.notify = function () { |
|
161 if (!this._listeners) { |
|
162 return; |
|
163 } |
|
164 |
|
165 let name = arguments[0]; |
|
166 let listeners = this._getListeners(name).slice(0); |
|
167 |
|
168 for each (let listener in listeners) { |
|
169 try { |
|
170 listener.apply(null, arguments); |
|
171 } catch (e) { |
|
172 // Prevent a bad listener from interfering with the others. |
|
173 DevToolsUtils.reportException("notify event '" + name + "'", e); |
|
174 } |
|
175 } |
|
176 } |
|
177 } |
|
178 |
|
179 /** |
|
180 * Set of protocol messages that affect thread state, and the |
|
181 * state the actor is in after each message. |
|
182 */ |
|
183 const ThreadStateTypes = { |
|
184 "paused": "paused", |
|
185 "resumed": "attached", |
|
186 "detached": "detached" |
|
187 }; |
|
188 |
|
189 /** |
|
190 * Set of protocol messages that are sent by the server without a prior request |
|
191 * by the client. |
|
192 */ |
|
193 const UnsolicitedNotifications = { |
|
194 "consoleAPICall": "consoleAPICall", |
|
195 "eventNotification": "eventNotification", |
|
196 "fileActivity": "fileActivity", |
|
197 "lastPrivateContextExited": "lastPrivateContextExited", |
|
198 "logMessage": "logMessage", |
|
199 "networkEvent": "networkEvent", |
|
200 "networkEventUpdate": "networkEventUpdate", |
|
201 "newGlobal": "newGlobal", |
|
202 "newScript": "newScript", |
|
203 "newSource": "newSource", |
|
204 "tabDetached": "tabDetached", |
|
205 "tabListChanged": "tabListChanged", |
|
206 "reflowActivity": "reflowActivity", |
|
207 "addonListChanged": "addonListChanged", |
|
208 "tabNavigated": "tabNavigated", |
|
209 "pageError": "pageError", |
|
210 "documentLoad": "documentLoad", |
|
211 "enteredFrame": "enteredFrame", |
|
212 "exitedFrame": "exitedFrame", |
|
213 "appOpen": "appOpen", |
|
214 "appClose": "appClose", |
|
215 "appInstall": "appInstall", |
|
216 "appUninstall": "appUninstall" |
|
217 }; |
|
218 |
|
219 /** |
|
220 * Set of pause types that are sent by the server and not as an immediate |
|
221 * response to a client request. |
|
222 */ |
|
223 const UnsolicitedPauses = { |
|
224 "resumeLimit": "resumeLimit", |
|
225 "debuggerStatement": "debuggerStatement", |
|
226 "breakpoint": "breakpoint", |
|
227 "DOMEvent": "DOMEvent", |
|
228 "watchpoint": "watchpoint", |
|
229 "exception": "exception" |
|
230 }; |
|
231 |
|
232 /** |
|
233 * Creates a client for the remote debugging protocol server. This client |
|
234 * provides the means to communicate with the server and exchange the messages |
|
235 * required by the protocol in a traditional JavaScript API. |
|
236 */ |
|
237 this.DebuggerClient = function (aTransport) |
|
238 { |
|
239 this._transport = aTransport; |
|
240 this._transport.hooks = this; |
|
241 |
|
242 // Map actor ID to client instance for each actor type. |
|
243 this._threadClients = new Map; |
|
244 this._addonClients = new Map; |
|
245 this._tabClients = new Map; |
|
246 this._tracerClients = new Map; |
|
247 this._consoleClients = new Map; |
|
248 |
|
249 this._pendingRequests = []; |
|
250 this._activeRequests = new Map; |
|
251 this._eventsEnabled = true; |
|
252 |
|
253 this.compat = new ProtocolCompatibility(this, []); |
|
254 this.traits = {}; |
|
255 |
|
256 this.request = this.request.bind(this); |
|
257 this.localTransport = this._transport.onOutputStreamReady === undefined; |
|
258 |
|
259 /* |
|
260 * As the first thing on the connection, expect a greeting packet from |
|
261 * the connection's root actor. |
|
262 */ |
|
263 this.mainRoot = null; |
|
264 this.expectReply("root", (aPacket) => { |
|
265 this.mainRoot = new RootClient(this, aPacket); |
|
266 this.notify("connected", aPacket.applicationType, aPacket.traits); |
|
267 }); |
|
268 } |
|
269 |
|
270 /** |
|
271 * A declarative helper for defining methods that send requests to the server. |
|
272 * |
|
273 * @param aPacketSkeleton |
|
274 * The form of the packet to send. Can specify fields to be filled from |
|
275 * the parameters by using the |args| function. |
|
276 * @param telemetry |
|
277 * The unique suffix of the telemetry histogram id. |
|
278 * @param before |
|
279 * The function to call before sending the packet. Is passed the packet, |
|
280 * and the return value is used as the new packet. The |this| context is |
|
281 * the instance of the client object we are defining a method for. |
|
282 * @param after |
|
283 * The function to call after the response is received. It is passed the |
|
284 * response, and the return value is considered the new response that |
|
285 * will be passed to the callback. The |this| context is the instance of |
|
286 * the client object we are defining a method for. |
|
287 */ |
|
288 DebuggerClient.requester = function (aPacketSkeleton, |
|
289 { telemetry, before, after }) { |
|
290 return DevToolsUtils.makeInfallible(function (...args) { |
|
291 let histogram, startTime; |
|
292 if (telemetry) { |
|
293 let transportType = this._transport.onOutputStreamReady === undefined |
|
294 ? "LOCAL_" |
|
295 : "REMOTE_"; |
|
296 let histogramId = "DEVTOOLS_DEBUGGER_RDP_" |
|
297 + transportType + telemetry + "_MS"; |
|
298 histogram = Services.telemetry.getHistogramById(histogramId); |
|
299 startTime = +new Date; |
|
300 } |
|
301 let outgoingPacket = { |
|
302 to: aPacketSkeleton.to || this.actor |
|
303 }; |
|
304 |
|
305 let maxPosition = -1; |
|
306 for (let k of Object.keys(aPacketSkeleton)) { |
|
307 if (aPacketSkeleton[k] instanceof DebuggerClient.Argument) { |
|
308 let { position } = aPacketSkeleton[k]; |
|
309 outgoingPacket[k] = aPacketSkeleton[k].getArgument(args); |
|
310 maxPosition = Math.max(position, maxPosition); |
|
311 } else { |
|
312 outgoingPacket[k] = aPacketSkeleton[k]; |
|
313 } |
|
314 } |
|
315 |
|
316 if (before) { |
|
317 outgoingPacket = before.call(this, outgoingPacket); |
|
318 } |
|
319 |
|
320 this.request(outgoingPacket, DevToolsUtils.makeInfallible(function (aResponse) { |
|
321 if (after) { |
|
322 let { from } = aResponse; |
|
323 aResponse = after.call(this, aResponse); |
|
324 if (!aResponse.from) { |
|
325 aResponse.from = from; |
|
326 } |
|
327 } |
|
328 |
|
329 // The callback is always the last parameter. |
|
330 let thisCallback = args[maxPosition + 1]; |
|
331 if (thisCallback) { |
|
332 thisCallback(aResponse); |
|
333 } |
|
334 |
|
335 if (histogram) { |
|
336 histogram.add(+new Date - startTime); |
|
337 } |
|
338 }.bind(this), "DebuggerClient.requester request callback")); |
|
339 |
|
340 }, "DebuggerClient.requester"); |
|
341 }; |
|
342 |
|
343 function args(aPos) { |
|
344 return new DebuggerClient.Argument(aPos); |
|
345 } |
|
346 |
|
347 DebuggerClient.Argument = function (aPosition) { |
|
348 this.position = aPosition; |
|
349 }; |
|
350 |
|
351 DebuggerClient.Argument.prototype.getArgument = function (aParams) { |
|
352 if (!(this.position in aParams)) { |
|
353 throw new Error("Bad index into params: " + this.position); |
|
354 } |
|
355 return aParams[this.position]; |
|
356 }; |
|
357 |
|
358 DebuggerClient.prototype = { |
|
359 /** |
|
360 * Connect to the server and start exchanging protocol messages. |
|
361 * |
|
362 * @param aOnConnected function |
|
363 * If specified, will be called when the greeting packet is |
|
364 * received from the debugging server. |
|
365 */ |
|
366 connect: function (aOnConnected) { |
|
367 this.addOneTimeListener("connected", (aName, aApplicationType, aTraits) => { |
|
368 this.traits = aTraits; |
|
369 if (aOnConnected) { |
|
370 aOnConnected(aApplicationType, aTraits); |
|
371 } |
|
372 }); |
|
373 |
|
374 this._transport.ready(); |
|
375 }, |
|
376 |
|
377 /** |
|
378 * Shut down communication with the debugging server. |
|
379 * |
|
380 * @param aOnClosed function |
|
381 * If specified, will be called when the debugging connection |
|
382 * has been closed. |
|
383 */ |
|
384 close: function (aOnClosed) { |
|
385 // Disable detach event notifications, because event handlers will be in a |
|
386 // cleared scope by the time they run. |
|
387 this._eventsEnabled = false; |
|
388 |
|
389 if (aOnClosed) { |
|
390 this.addOneTimeListener('closed', function (aEvent) { |
|
391 aOnClosed(); |
|
392 }); |
|
393 } |
|
394 |
|
395 const detachClients = (clientMap, next) => { |
|
396 const clients = clientMap.values(); |
|
397 const total = clientMap.size; |
|
398 let numFinished = 0; |
|
399 |
|
400 if (total == 0) { |
|
401 next(); |
|
402 return; |
|
403 } |
|
404 |
|
405 for (let client of clients) { |
|
406 let method = client instanceof WebConsoleClient ? "close" : "detach"; |
|
407 client[method](() => { |
|
408 if (++numFinished === total) { |
|
409 clientMap.clear(); |
|
410 next(); |
|
411 } |
|
412 }); |
|
413 } |
|
414 }; |
|
415 |
|
416 detachClients(this._consoleClients, () => { |
|
417 detachClients(this._threadClients, () => { |
|
418 detachClients(this._tabClients, () => { |
|
419 detachClients(this._addonClients, () => { |
|
420 this._transport.close(); |
|
421 this._transport = null; |
|
422 }); |
|
423 }); |
|
424 }); |
|
425 }); |
|
426 }, |
|
427 |
|
428 /* |
|
429 * This function exists only to preserve DebuggerClient's interface; |
|
430 * new code should say 'client.mainRoot.listTabs()'. |
|
431 */ |
|
432 listTabs: function (aOnResponse) { return this.mainRoot.listTabs(aOnResponse); }, |
|
433 |
|
434 /* |
|
435 * This function exists only to preserve DebuggerClient's interface; |
|
436 * new code should say 'client.mainRoot.listAddons()'. |
|
437 */ |
|
438 listAddons: function (aOnResponse) { return this.mainRoot.listAddons(aOnResponse); }, |
|
439 |
|
440 /** |
|
441 * Attach to a tab actor. |
|
442 * |
|
443 * @param string aTabActor |
|
444 * The actor ID for the tab to attach. |
|
445 * @param function aOnResponse |
|
446 * Called with the response packet and a TabClient |
|
447 * (which will be undefined on error). |
|
448 */ |
|
449 attachTab: function (aTabActor, aOnResponse) { |
|
450 if (this._tabClients.has(aTabActor)) { |
|
451 let cachedTab = this._tabClients.get(aTabActor); |
|
452 let cachedResponse = { |
|
453 cacheEnabled: cachedTab.cacheEnabled, |
|
454 javascriptEnabled: cachedTab.javascriptEnabled, |
|
455 traits: cachedTab.traits, |
|
456 }; |
|
457 setTimeout(() => aOnResponse(cachedResponse, cachedTab), 0); |
|
458 return; |
|
459 } |
|
460 |
|
461 let packet = { |
|
462 to: aTabActor, |
|
463 type: "attach" |
|
464 }; |
|
465 this.request(packet, (aResponse) => { |
|
466 let tabClient; |
|
467 if (!aResponse.error) { |
|
468 tabClient = new TabClient(this, aResponse); |
|
469 this._tabClients.set(aTabActor, tabClient); |
|
470 } |
|
471 aOnResponse(aResponse, tabClient); |
|
472 }); |
|
473 }, |
|
474 |
|
475 /** |
|
476 * Attach to an addon actor. |
|
477 * |
|
478 * @param string aAddonActor |
|
479 * The actor ID for the addon to attach. |
|
480 * @param function aOnResponse |
|
481 * Called with the response packet and a AddonClient |
|
482 * (which will be undefined on error). |
|
483 */ |
|
484 attachAddon: function DC_attachAddon(aAddonActor, aOnResponse) { |
|
485 let packet = { |
|
486 to: aAddonActor, |
|
487 type: "attach" |
|
488 }; |
|
489 this.request(packet, aResponse => { |
|
490 let addonClient; |
|
491 if (!aResponse.error) { |
|
492 addonClient = new AddonClient(this, aAddonActor); |
|
493 this._addonClients[aAddonActor] = addonClient; |
|
494 this.activeAddon = addonClient; |
|
495 } |
|
496 aOnResponse(aResponse, addonClient); |
|
497 }); |
|
498 }, |
|
499 |
|
500 /** |
|
501 * Attach to a Web Console actor. |
|
502 * |
|
503 * @param string aConsoleActor |
|
504 * The ID for the console actor to attach to. |
|
505 * @param array aListeners |
|
506 * The console listeners you want to start. |
|
507 * @param function aOnResponse |
|
508 * Called with the response packet and a WebConsoleClient |
|
509 * instance (which will be undefined on error). |
|
510 */ |
|
511 attachConsole: |
|
512 function (aConsoleActor, aListeners, aOnResponse) { |
|
513 let packet = { |
|
514 to: aConsoleActor, |
|
515 type: "startListeners", |
|
516 listeners: aListeners, |
|
517 }; |
|
518 |
|
519 this.request(packet, (aResponse) => { |
|
520 let consoleClient; |
|
521 if (!aResponse.error) { |
|
522 if (this._consoleClients.has(aConsoleActor)) { |
|
523 consoleClient = this._consoleClients.get(aConsoleActor); |
|
524 } else { |
|
525 consoleClient = new WebConsoleClient(this, aResponse); |
|
526 this._consoleClients.set(aConsoleActor, consoleClient); |
|
527 } |
|
528 } |
|
529 aOnResponse(aResponse, consoleClient); |
|
530 }); |
|
531 }, |
|
532 |
|
533 /** |
|
534 * Attach to a global-scoped thread actor for chrome debugging. |
|
535 * |
|
536 * @param string aThreadActor |
|
537 * The actor ID for the thread to attach. |
|
538 * @param function aOnResponse |
|
539 * Called with the response packet and a ThreadClient |
|
540 * (which will be undefined on error). |
|
541 * @param object aOptions |
|
542 * Configuration options. |
|
543 * - useSourceMaps: whether to use source maps or not. |
|
544 */ |
|
545 attachThread: function (aThreadActor, aOnResponse, aOptions={}) { |
|
546 if (this._threadClients.has(aThreadActor)) { |
|
547 setTimeout(() => aOnResponse({}, this._threadClients.get(aThreadActor)), 0); |
|
548 return; |
|
549 } |
|
550 |
|
551 let packet = { |
|
552 to: aThreadActor, |
|
553 type: "attach", |
|
554 options: aOptions |
|
555 }; |
|
556 this.request(packet, (aResponse) => { |
|
557 if (!aResponse.error) { |
|
558 var threadClient = new ThreadClient(this, aThreadActor); |
|
559 this._threadClients.set(aThreadActor, threadClient); |
|
560 } |
|
561 aOnResponse(aResponse, threadClient); |
|
562 }); |
|
563 }, |
|
564 |
|
565 /** |
|
566 * Attach to a trace actor. |
|
567 * |
|
568 * @param string aTraceActor |
|
569 * The actor ID for the tracer to attach. |
|
570 * @param function aOnResponse |
|
571 * Called with the response packet and a TraceClient |
|
572 * (which will be undefined on error). |
|
573 */ |
|
574 attachTracer: function (aTraceActor, aOnResponse) { |
|
575 if (this._tracerClients.has(aTraceActor)) { |
|
576 setTimeout(() => aOnResponse({}, this._tracerClients.get(aTraceActor)), 0); |
|
577 return; |
|
578 } |
|
579 |
|
580 let packet = { |
|
581 to: aTraceActor, |
|
582 type: "attach" |
|
583 }; |
|
584 this.request(packet, (aResponse) => { |
|
585 if (!aResponse.error) { |
|
586 var traceClient = new TraceClient(this, aTraceActor); |
|
587 this._tracerClients.set(aTraceActor, traceClient); |
|
588 } |
|
589 aOnResponse(aResponse, traceClient); |
|
590 }); |
|
591 }, |
|
592 |
|
593 /** |
|
594 * Release an object actor. |
|
595 * |
|
596 * @param string aActor |
|
597 * The actor ID to send the request to. |
|
598 * @param aOnResponse function |
|
599 * If specified, will be called with the response packet when |
|
600 * debugging server responds. |
|
601 */ |
|
602 release: DebuggerClient.requester({ |
|
603 to: args(0), |
|
604 type: "release" |
|
605 }, { |
|
606 telemetry: "RELEASE" |
|
607 }), |
|
608 |
|
609 /** |
|
610 * Send a request to the debugging server. |
|
611 * |
|
612 * @param aRequest object |
|
613 * A JSON packet to send to the debugging server. |
|
614 * @param aOnResponse function |
|
615 * If specified, will be called with the response packet when |
|
616 * debugging server responds. |
|
617 */ |
|
618 request: function (aRequest, aOnResponse) { |
|
619 if (!this.mainRoot) { |
|
620 throw Error("Have not yet received a hello packet from the server."); |
|
621 } |
|
622 if (!aRequest.to) { |
|
623 let type = aRequest.type || ""; |
|
624 throw Error("'" + type + "' request packet has no destination."); |
|
625 } |
|
626 |
|
627 this._pendingRequests.push({ to: aRequest.to, |
|
628 request: aRequest, |
|
629 onResponse: aOnResponse }); |
|
630 this._sendRequests(); |
|
631 }, |
|
632 |
|
633 /** |
|
634 * Send pending requests to any actors that don't already have an |
|
635 * active request. |
|
636 */ |
|
637 _sendRequests: function () { |
|
638 this._pendingRequests = this._pendingRequests.filter((request) => { |
|
639 if (this._activeRequests.has(request.to)) { |
|
640 return true; |
|
641 } |
|
642 |
|
643 this.expectReply(request.to, request.onResponse); |
|
644 this._transport.send(request.request); |
|
645 |
|
646 return false; |
|
647 }); |
|
648 }, |
|
649 |
|
650 /** |
|
651 * Arrange to hand the next reply from |aActor| to |aHandler|. |
|
652 * |
|
653 * DebuggerClient.prototype.request usually takes care of establishing |
|
654 * the handler for a given request, but in rare cases (well, greetings |
|
655 * from new root actors, is the only case at the moment) we must be |
|
656 * prepared for a "reply" that doesn't correspond to any request we sent. |
|
657 */ |
|
658 expectReply: function (aActor, aHandler) { |
|
659 if (this._activeRequests.has(aActor)) { |
|
660 throw Error("clashing handlers for next reply from " + uneval(aActor)); |
|
661 } |
|
662 this._activeRequests.set(aActor, aHandler); |
|
663 }, |
|
664 |
|
665 // Transport hooks. |
|
666 |
|
667 /** |
|
668 * Called by DebuggerTransport to dispatch incoming packets as appropriate. |
|
669 * |
|
670 * @param aPacket object |
|
671 * The incoming packet. |
|
672 * @param aIgnoreCompatibility boolean |
|
673 * Set true to not pass the packet through the compatibility layer. |
|
674 */ |
|
675 onPacket: function (aPacket, aIgnoreCompatibility=false) { |
|
676 let packet = aIgnoreCompatibility |
|
677 ? aPacket |
|
678 : this.compat.onPacket(aPacket); |
|
679 |
|
680 resolve(packet).then(aPacket => { |
|
681 if (!aPacket.from) { |
|
682 DevToolsUtils.reportException( |
|
683 "onPacket", |
|
684 new Error("Server did not specify an actor, dropping packet: " + |
|
685 JSON.stringify(aPacket))); |
|
686 return; |
|
687 } |
|
688 |
|
689 // If we have a registered Front for this actor, let it handle the packet |
|
690 // and skip all the rest of this unpleasantness. |
|
691 let front = this.getActor(aPacket.from); |
|
692 if (front) { |
|
693 front.onPacket(aPacket); |
|
694 return; |
|
695 } |
|
696 |
|
697 let onResponse; |
|
698 // See if we have a handler function waiting for a reply from this |
|
699 // actor. (Don't count unsolicited notifications or pauses as |
|
700 // replies.) |
|
701 if (this._activeRequests.has(aPacket.from) && |
|
702 !(aPacket.type in UnsolicitedNotifications) && |
|
703 !(aPacket.type == ThreadStateTypes.paused && |
|
704 aPacket.why.type in UnsolicitedPauses)) { |
|
705 onResponse = this._activeRequests.get(aPacket.from); |
|
706 this._activeRequests.delete(aPacket.from); |
|
707 } |
|
708 |
|
709 // Packets that indicate thread state changes get special treatment. |
|
710 if (aPacket.type in ThreadStateTypes && |
|
711 this._threadClients.has(aPacket.from)) { |
|
712 this._threadClients.get(aPacket.from)._onThreadState(aPacket); |
|
713 } |
|
714 // On navigation the server resumes, so the client must resume as well. |
|
715 // We achieve that by generating a fake resumption packet that triggers |
|
716 // the client's thread state change listeners. |
|
717 if (aPacket.type == UnsolicitedNotifications.tabNavigated && |
|
718 this._tabClients.has(aPacket.from) && |
|
719 this._tabClients.get(aPacket.from).thread) { |
|
720 let thread = this._tabClients.get(aPacket.from).thread; |
|
721 let resumption = { from: thread._actor, type: "resumed" }; |
|
722 thread._onThreadState(resumption); |
|
723 } |
|
724 // Only try to notify listeners on events, not responses to requests |
|
725 // that lack a packet type. |
|
726 if (aPacket.type) { |
|
727 this.notify(aPacket.type, aPacket); |
|
728 } |
|
729 |
|
730 if (onResponse) { |
|
731 onResponse(aPacket); |
|
732 } |
|
733 |
|
734 this._sendRequests(); |
|
735 }, ex => DevToolsUtils.reportException("onPacket handler", ex)); |
|
736 }, |
|
737 |
|
738 /** |
|
739 * Called by DebuggerTransport when the underlying stream is closed. |
|
740 * |
|
741 * @param aStatus nsresult |
|
742 * The status code that corresponds to the reason for closing |
|
743 * the stream. |
|
744 */ |
|
745 onClosed: function (aStatus) { |
|
746 this.notify("closed"); |
|
747 }, |
|
748 |
|
749 /** |
|
750 * Actor lifetime management, echos the server's actor pools. |
|
751 */ |
|
752 __pools: null, |
|
753 get _pools() { |
|
754 if (this.__pools) { |
|
755 return this.__pools; |
|
756 } |
|
757 this.__pools = new Set(); |
|
758 return this.__pools; |
|
759 }, |
|
760 |
|
761 addActorPool: function (pool) { |
|
762 this._pools.add(pool); |
|
763 }, |
|
764 removeActorPool: function (pool) { |
|
765 this._pools.delete(pool); |
|
766 }, |
|
767 getActor: function (actorID) { |
|
768 let pool = this.poolFor(actorID); |
|
769 return pool ? pool.get(actorID) : null; |
|
770 }, |
|
771 |
|
772 poolFor: function (actorID) { |
|
773 for (let pool of this._pools) { |
|
774 if (pool.has(actorID)) return pool; |
|
775 } |
|
776 return null; |
|
777 }, |
|
778 |
|
779 /** |
|
780 * Currently attached addon. |
|
781 */ |
|
782 activeAddon: null |
|
783 } |
|
784 |
|
785 eventSource(DebuggerClient.prototype); |
|
786 |
|
787 // Constants returned by `FeatureCompatibilityShim.onPacketTest`. |
|
788 const SUPPORTED = 1; |
|
789 const NOT_SUPPORTED = 2; |
|
790 const SKIP = 3; |
|
791 |
|
792 /** |
|
793 * This object provides an abstraction layer over all of our backwards |
|
794 * compatibility, feature detection, and shimming with regards to the remote |
|
795 * debugging prototcol. |
|
796 * |
|
797 * @param aFeatures Array |
|
798 * An array of FeatureCompatibilityShim objects |
|
799 */ |
|
800 function ProtocolCompatibility(aClient, aFeatures) { |
|
801 this._client = aClient; |
|
802 this._featuresWithUnknownSupport = new Set(aFeatures); |
|
803 this._featuresWithoutSupport = new Set(); |
|
804 |
|
805 this._featureDeferreds = Object.create(null) |
|
806 for (let f of aFeatures) { |
|
807 this._featureDeferreds[f.name] = defer(); |
|
808 } |
|
809 } |
|
810 |
|
811 ProtocolCompatibility.prototype = { |
|
812 /** |
|
813 * Returns a promise that resolves to true if the RDP supports the feature, |
|
814 * and is rejected otherwise. |
|
815 * |
|
816 * @param aFeatureName String |
|
817 * The name of the feature we are testing. |
|
818 */ |
|
819 supportsFeature: function (aFeatureName) { |
|
820 return this._featureDeferreds[aFeatureName].promise; |
|
821 }, |
|
822 |
|
823 /** |
|
824 * Force a feature to be considered unsupported. |
|
825 * |
|
826 * @param aFeatureName String |
|
827 * The name of the feature we are testing. |
|
828 */ |
|
829 rejectFeature: function (aFeatureName) { |
|
830 this._featureDeferreds[aFeatureName].reject(false); |
|
831 }, |
|
832 |
|
833 /** |
|
834 * Called for each packet received over the RDP from the server. Tests for |
|
835 * protocol features and shims packets to support needed features. |
|
836 * |
|
837 * @param aPacket Object |
|
838 * Packet received over the RDP from the server. |
|
839 */ |
|
840 onPacket: function (aPacket) { |
|
841 this._detectFeatures(aPacket); |
|
842 return this._shimPacket(aPacket); |
|
843 }, |
|
844 |
|
845 /** |
|
846 * For each of the features we don't know whether the server supports or not, |
|
847 * attempt to detect support based on the packet we just received. |
|
848 */ |
|
849 _detectFeatures: function (aPacket) { |
|
850 for (let feature of this._featuresWithUnknownSupport) { |
|
851 try { |
|
852 switch (feature.onPacketTest(aPacket)) { |
|
853 case SKIP: |
|
854 break; |
|
855 case SUPPORTED: |
|
856 this._featuresWithUnknownSupport.delete(feature); |
|
857 this._featureDeferreds[feature.name].resolve(true); |
|
858 break; |
|
859 case NOT_SUPPORTED: |
|
860 this._featuresWithUnknownSupport.delete(feature); |
|
861 this._featuresWithoutSupport.add(feature); |
|
862 this.rejectFeature(feature.name); |
|
863 break; |
|
864 default: |
|
865 DevToolsUtils.reportException( |
|
866 "PC__detectFeatures", |
|
867 new Error("Bad return value from `onPacketTest` for feature '" |
|
868 + feature.name + "'")); |
|
869 } |
|
870 } catch (ex) { |
|
871 DevToolsUtils.reportException("PC__detectFeatures", ex); |
|
872 } |
|
873 } |
|
874 }, |
|
875 |
|
876 /** |
|
877 * Go through each of the features that we know are unsupported by the current |
|
878 * server and attempt to shim support. |
|
879 */ |
|
880 _shimPacket: function (aPacket) { |
|
881 let extraPackets = []; |
|
882 |
|
883 let loop = function (aFeatures, aPacket) { |
|
884 if (aFeatures.length === 0) { |
|
885 for (let packet of extraPackets) { |
|
886 this._client.onPacket(packet, true); |
|
887 } |
|
888 return aPacket; |
|
889 } else { |
|
890 let replacePacket = function (aNewPacket) { |
|
891 return aNewPacket; |
|
892 }; |
|
893 let extraPacket = function (aExtraPacket) { |
|
894 extraPackets.push(aExtraPacket); |
|
895 return aPacket; |
|
896 }; |
|
897 let keepPacket = function () { |
|
898 return aPacket; |
|
899 }; |
|
900 let newPacket = aFeatures[0].translatePacket(aPacket, |
|
901 replacePacket, |
|
902 extraPacket, |
|
903 keepPacket); |
|
904 return resolve(newPacket).then(loop.bind(null, aFeatures.slice(1))); |
|
905 } |
|
906 }.bind(this); |
|
907 |
|
908 return loop([f for (f of this._featuresWithoutSupport)], |
|
909 aPacket); |
|
910 } |
|
911 }; |
|
912 |
|
913 /** |
|
914 * Interface defining what methods a feature compatibility shim should have. |
|
915 */ |
|
916 const FeatureCompatibilityShim = { |
|
917 // The name of the feature |
|
918 name: null, |
|
919 |
|
920 /** |
|
921 * Takes a packet and returns boolean (or promise of boolean) indicating |
|
922 * whether the server supports the RDP feature we are possibly shimming. |
|
923 */ |
|
924 onPacketTest: function (aPacket) { |
|
925 throw new Error("Not yet implemented"); |
|
926 }, |
|
927 |
|
928 /** |
|
929 * Takes a packet actually sent from the server and decides whether to replace |
|
930 * it with a new packet, create an extra packet, or keep it. |
|
931 */ |
|
932 translatePacket: function (aPacket, aReplacePacket, aExtraPacket, aKeepPacket) { |
|
933 throw new Error("Not yet implemented"); |
|
934 } |
|
935 }; |
|
936 |
|
937 /** |
|
938 * Creates a tab client for the remote debugging protocol server. This client |
|
939 * is a front to the tab actor created in the server side, hiding the protocol |
|
940 * details in a traditional JavaScript API. |
|
941 * |
|
942 * @param aClient DebuggerClient |
|
943 * The debugger client parent. |
|
944 * @param aForm object |
|
945 * The protocol form for this tab. |
|
946 */ |
|
947 function TabClient(aClient, aForm) { |
|
948 this.client = aClient; |
|
949 this._actor = aForm.from; |
|
950 this._threadActor = aForm.threadActor; |
|
951 this.javascriptEnabled = aForm.javascriptEnabled; |
|
952 this.cacheEnabled = aForm.cacheEnabled; |
|
953 this.thread = null; |
|
954 this.request = this.client.request; |
|
955 this.traits = aForm.traits || {}; |
|
956 } |
|
957 |
|
958 TabClient.prototype = { |
|
959 get actor() { return this._actor }, |
|
960 get _transport() { return this.client._transport; }, |
|
961 |
|
962 /** |
|
963 * Attach to a thread actor. |
|
964 * |
|
965 * @param object aOptions |
|
966 * Configuration options. |
|
967 * - useSourceMaps: whether to use source maps or not. |
|
968 * @param function aOnResponse |
|
969 * Called with the response packet and a ThreadClient |
|
970 * (which will be undefined on error). |
|
971 */ |
|
972 attachThread: function(aOptions={}, aOnResponse) { |
|
973 if (this.thread) { |
|
974 setTimeout(() => aOnResponse({}, this.thread), 0); |
|
975 return; |
|
976 } |
|
977 |
|
978 let packet = { |
|
979 to: this._threadActor, |
|
980 type: "attach", |
|
981 options: aOptions |
|
982 }; |
|
983 this.request(packet, (aResponse) => { |
|
984 if (!aResponse.error) { |
|
985 this.thread = new ThreadClient(this, this._threadActor); |
|
986 this.client._threadClients.set(this._threadActor, this.thread); |
|
987 } |
|
988 aOnResponse(aResponse, this.thread); |
|
989 }); |
|
990 }, |
|
991 |
|
992 /** |
|
993 * Detach the client from the tab actor. |
|
994 * |
|
995 * @param function aOnResponse |
|
996 * Called with the response packet. |
|
997 */ |
|
998 detach: DebuggerClient.requester({ |
|
999 type: "detach" |
|
1000 }, { |
|
1001 before: function (aPacket) { |
|
1002 if (this.thread) { |
|
1003 this.thread.detach(); |
|
1004 } |
|
1005 return aPacket; |
|
1006 }, |
|
1007 after: function (aResponse) { |
|
1008 this.client._tabClients.delete(this.actor); |
|
1009 return aResponse; |
|
1010 }, |
|
1011 telemetry: "TABDETACH" |
|
1012 }), |
|
1013 |
|
1014 /** |
|
1015 * Reload the page in this tab. |
|
1016 */ |
|
1017 reload: DebuggerClient.requester({ |
|
1018 type: "reload" |
|
1019 }, { |
|
1020 telemetry: "RELOAD" |
|
1021 }), |
|
1022 |
|
1023 /** |
|
1024 * Navigate to another URL. |
|
1025 * |
|
1026 * @param string url |
|
1027 * The URL to navigate to. |
|
1028 */ |
|
1029 navigateTo: DebuggerClient.requester({ |
|
1030 type: "navigateTo", |
|
1031 url: args(0) |
|
1032 }, { |
|
1033 telemetry: "NAVIGATETO" |
|
1034 }), |
|
1035 |
|
1036 /** |
|
1037 * Reconfigure the tab actor. |
|
1038 * |
|
1039 * @param object aOptions |
|
1040 * A dictionary object of the new options to use in the tab actor. |
|
1041 * @param function aOnResponse |
|
1042 * Called with the response packet. |
|
1043 */ |
|
1044 reconfigure: DebuggerClient.requester({ |
|
1045 type: "reconfigure", |
|
1046 options: args(0) |
|
1047 }, { |
|
1048 telemetry: "RECONFIGURETAB" |
|
1049 }), |
|
1050 }; |
|
1051 |
|
1052 eventSource(TabClient.prototype); |
|
1053 |
|
1054 function AddonClient(aClient, aActor) { |
|
1055 this._client = aClient; |
|
1056 this._actor = aActor; |
|
1057 this.request = this._client.request; |
|
1058 } |
|
1059 |
|
1060 AddonClient.prototype = { |
|
1061 get actor() { return this._actor; }, |
|
1062 get _transport() { return this._client._transport; }, |
|
1063 |
|
1064 /** |
|
1065 * Detach the client from the addon actor. |
|
1066 * |
|
1067 * @param function aOnResponse |
|
1068 * Called with the response packet. |
|
1069 */ |
|
1070 detach: DebuggerClient.requester({ |
|
1071 type: "detach" |
|
1072 }, { |
|
1073 after: function(aResponse) { |
|
1074 if (this._client.activeAddon === this._client._addonClients[this.actor]) { |
|
1075 this._client.activeAddon = null |
|
1076 } |
|
1077 delete this._client._addonClients[this.actor]; |
|
1078 return aResponse; |
|
1079 }, |
|
1080 telemetry: "ADDONDETACH" |
|
1081 }) |
|
1082 }; |
|
1083 |
|
1084 /** |
|
1085 * A RootClient object represents a root actor on the server. Each |
|
1086 * DebuggerClient keeps a RootClient instance representing the root actor |
|
1087 * for the initial connection; DebuggerClient's 'listTabs' and |
|
1088 * 'listChildProcesses' methods forward to that root actor. |
|
1089 * |
|
1090 * @param aClient object |
|
1091 * The client connection to which this actor belongs. |
|
1092 * @param aGreeting string |
|
1093 * The greeting packet from the root actor we're to represent. |
|
1094 * |
|
1095 * Properties of a RootClient instance: |
|
1096 * |
|
1097 * @property actor string |
|
1098 * The name of this child's root actor. |
|
1099 * @property applicationType string |
|
1100 * The application type, as given in the root actor's greeting packet. |
|
1101 * @property traits object |
|
1102 * The traits object, as given in the root actor's greeting packet. |
|
1103 */ |
|
1104 function RootClient(aClient, aGreeting) { |
|
1105 this._client = aClient; |
|
1106 this.actor = aGreeting.from; |
|
1107 this.applicationType = aGreeting.applicationType; |
|
1108 this.traits = aGreeting.traits; |
|
1109 } |
|
1110 |
|
1111 RootClient.prototype = { |
|
1112 constructor: RootClient, |
|
1113 |
|
1114 /** |
|
1115 * List the open tabs. |
|
1116 * |
|
1117 * @param function aOnResponse |
|
1118 * Called with the response packet. |
|
1119 */ |
|
1120 listTabs: DebuggerClient.requester({ type: "listTabs" }, |
|
1121 { telemetry: "LISTTABS" }), |
|
1122 |
|
1123 /** |
|
1124 * List the installed addons. |
|
1125 * |
|
1126 * @param function aOnResponse |
|
1127 * Called with the response packet. |
|
1128 */ |
|
1129 listAddons: DebuggerClient.requester({ type: "listAddons" }, |
|
1130 { telemetry: "LISTADDONS" }), |
|
1131 |
|
1132 /* |
|
1133 * Methods constructed by DebuggerClient.requester require these forwards |
|
1134 * on their 'this'. |
|
1135 */ |
|
1136 get _transport() { return this._client._transport; }, |
|
1137 get request() { return this._client.request; } |
|
1138 }; |
|
1139 |
|
1140 /** |
|
1141 * Creates a thread client for the remote debugging protocol server. This client |
|
1142 * is a front to the thread actor created in the server side, hiding the |
|
1143 * protocol details in a traditional JavaScript API. |
|
1144 * |
|
1145 * @param aClient DebuggerClient|TabClient |
|
1146 * The parent of the thread (tab for tab-scoped debuggers, DebuggerClient |
|
1147 * for chrome debuggers). |
|
1148 * @param aActor string |
|
1149 * The actor ID for this thread. |
|
1150 */ |
|
1151 function ThreadClient(aClient, aActor) { |
|
1152 this._parent = aClient; |
|
1153 this.client = aClient instanceof DebuggerClient ? aClient : aClient.client; |
|
1154 this._actor = aActor; |
|
1155 this._frameCache = []; |
|
1156 this._scriptCache = {}; |
|
1157 this._pauseGrips = {}; |
|
1158 this._threadGrips = {}; |
|
1159 this.request = this.client.request; |
|
1160 } |
|
1161 |
|
1162 ThreadClient.prototype = { |
|
1163 _state: "paused", |
|
1164 get state() { return this._state; }, |
|
1165 get paused() { return this._state === "paused"; }, |
|
1166 |
|
1167 _pauseOnExceptions: false, |
|
1168 _ignoreCaughtExceptions: false, |
|
1169 _pauseOnDOMEvents: null, |
|
1170 |
|
1171 _actor: null, |
|
1172 get actor() { return this._actor; }, |
|
1173 |
|
1174 get compat() { return this.client.compat; }, |
|
1175 get _transport() { return this.client._transport; }, |
|
1176 |
|
1177 _assertPaused: function (aCommand) { |
|
1178 if (!this.paused) { |
|
1179 throw Error(aCommand + " command sent while not paused. Currently " + this._state); |
|
1180 } |
|
1181 }, |
|
1182 |
|
1183 /** |
|
1184 * Resume a paused thread. If the optional aLimit parameter is present, then |
|
1185 * the thread will also pause when that limit is reached. |
|
1186 * |
|
1187 * @param [optional] object aLimit |
|
1188 * An object with a type property set to the appropriate limit (next, |
|
1189 * step, or finish) per the remote debugging protocol specification. |
|
1190 * Use null to specify no limit. |
|
1191 * @param function aOnResponse |
|
1192 * Called with the response packet. |
|
1193 */ |
|
1194 _doResume: DebuggerClient.requester({ |
|
1195 type: "resume", |
|
1196 resumeLimit: args(0) |
|
1197 }, { |
|
1198 before: function (aPacket) { |
|
1199 this._assertPaused("resume"); |
|
1200 |
|
1201 // Put the client in a tentative "resuming" state so we can prevent |
|
1202 // further requests that should only be sent in the paused state. |
|
1203 this._state = "resuming"; |
|
1204 |
|
1205 if (this._pauseOnExceptions) { |
|
1206 aPacket.pauseOnExceptions = this._pauseOnExceptions; |
|
1207 } |
|
1208 if (this._ignoreCaughtExceptions) { |
|
1209 aPacket.ignoreCaughtExceptions = this._ignoreCaughtExceptions; |
|
1210 } |
|
1211 if (this._pauseOnDOMEvents) { |
|
1212 aPacket.pauseOnDOMEvents = this._pauseOnDOMEvents; |
|
1213 } |
|
1214 return aPacket; |
|
1215 }, |
|
1216 after: function (aResponse) { |
|
1217 if (aResponse.error) { |
|
1218 // There was an error resuming, back to paused state. |
|
1219 this._state = "paused"; |
|
1220 } |
|
1221 return aResponse; |
|
1222 }, |
|
1223 telemetry: "RESUME" |
|
1224 }), |
|
1225 |
|
1226 /** |
|
1227 * Reconfigure the thread actor. |
|
1228 * |
|
1229 * @param object aOptions |
|
1230 * A dictionary object of the new options to use in the thread actor. |
|
1231 * @param function aOnResponse |
|
1232 * Called with the response packet. |
|
1233 */ |
|
1234 reconfigure: DebuggerClient.requester({ |
|
1235 type: "reconfigure", |
|
1236 options: args(0) |
|
1237 }, { |
|
1238 telemetry: "RECONFIGURETHREAD" |
|
1239 }), |
|
1240 |
|
1241 /** |
|
1242 * Resume a paused thread. |
|
1243 */ |
|
1244 resume: function (aOnResponse) { |
|
1245 this._doResume(null, aOnResponse); |
|
1246 }, |
|
1247 |
|
1248 /** |
|
1249 * Step over a function call. |
|
1250 * |
|
1251 * @param function aOnResponse |
|
1252 * Called with the response packet. |
|
1253 */ |
|
1254 stepOver: function (aOnResponse) { |
|
1255 this._doResume({ type: "next" }, aOnResponse); |
|
1256 }, |
|
1257 |
|
1258 /** |
|
1259 * Step into a function call. |
|
1260 * |
|
1261 * @param function aOnResponse |
|
1262 * Called with the response packet. |
|
1263 */ |
|
1264 stepIn: function (aOnResponse) { |
|
1265 this._doResume({ type: "step" }, aOnResponse); |
|
1266 }, |
|
1267 |
|
1268 /** |
|
1269 * Step out of a function call. |
|
1270 * |
|
1271 * @param function aOnResponse |
|
1272 * Called with the response packet. |
|
1273 */ |
|
1274 stepOut: function (aOnResponse) { |
|
1275 this._doResume({ type: "finish" }, aOnResponse); |
|
1276 }, |
|
1277 |
|
1278 /** |
|
1279 * Interrupt a running thread. |
|
1280 * |
|
1281 * @param function aOnResponse |
|
1282 * Called with the response packet. |
|
1283 */ |
|
1284 interrupt: DebuggerClient.requester({ |
|
1285 type: "interrupt" |
|
1286 }, { |
|
1287 telemetry: "INTERRUPT" |
|
1288 }), |
|
1289 |
|
1290 /** |
|
1291 * Enable or disable pausing when an exception is thrown. |
|
1292 * |
|
1293 * @param boolean aFlag |
|
1294 * Enables pausing if true, disables otherwise. |
|
1295 * @param function aOnResponse |
|
1296 * Called with the response packet. |
|
1297 */ |
|
1298 pauseOnExceptions: function (aPauseOnExceptions, |
|
1299 aIgnoreCaughtExceptions, |
|
1300 aOnResponse) { |
|
1301 this._pauseOnExceptions = aPauseOnExceptions; |
|
1302 this._ignoreCaughtExceptions = aIgnoreCaughtExceptions; |
|
1303 |
|
1304 // If the debuggee is paused, we have to send the flag via a reconfigure |
|
1305 // request. |
|
1306 if (this.paused) { |
|
1307 this.reconfigure({ |
|
1308 pauseOnExceptions: aPauseOnExceptions, |
|
1309 ignoreCaughtExceptions: aIgnoreCaughtExceptions |
|
1310 }, aOnResponse); |
|
1311 return; |
|
1312 } |
|
1313 // Otherwise send the flag using a standard resume request. |
|
1314 this.interrupt(aResponse => { |
|
1315 if (aResponse.error) { |
|
1316 // Can't continue if pausing failed. |
|
1317 aOnResponse(aResponse); |
|
1318 return; |
|
1319 } |
|
1320 this.resume(aOnResponse); |
|
1321 }); |
|
1322 }, |
|
1323 |
|
1324 /** |
|
1325 * Enable pausing when the specified DOM events are triggered. Disabling |
|
1326 * pausing on an event can be realized by calling this method with the updated |
|
1327 * array of events that doesn't contain it. |
|
1328 * |
|
1329 * @param array|string events |
|
1330 * An array of strings, representing the DOM event types to pause on, |
|
1331 * or "*" to pause on all DOM events. Pass an empty array to |
|
1332 * completely disable pausing on DOM events. |
|
1333 * @param function onResponse |
|
1334 * Called with the response packet in a future turn of the event loop. |
|
1335 */ |
|
1336 pauseOnDOMEvents: function (events, onResponse) { |
|
1337 this._pauseOnDOMEvents = events; |
|
1338 // If the debuggee is paused, the value of the array will be communicated in |
|
1339 // the next resumption. Otherwise we have to force a pause in order to send |
|
1340 // the array. |
|
1341 if (this.paused) { |
|
1342 setTimeout(() => onResponse({}), 0); |
|
1343 return; |
|
1344 } |
|
1345 this.interrupt(response => { |
|
1346 // Can't continue if pausing failed. |
|
1347 if (response.error) { |
|
1348 onResponse(response); |
|
1349 return; |
|
1350 } |
|
1351 this.resume(onResponse); |
|
1352 }); |
|
1353 }, |
|
1354 |
|
1355 /** |
|
1356 * Send a clientEvaluate packet to the debuggee. Response |
|
1357 * will be a resume packet. |
|
1358 * |
|
1359 * @param string aFrame |
|
1360 * The actor ID of the frame where the evaluation should take place. |
|
1361 * @param string aExpression |
|
1362 * The expression that will be evaluated in the scope of the frame |
|
1363 * above. |
|
1364 * @param function aOnResponse |
|
1365 * Called with the response packet. |
|
1366 */ |
|
1367 eval: DebuggerClient.requester({ |
|
1368 type: "clientEvaluate", |
|
1369 frame: args(0), |
|
1370 expression: args(1) |
|
1371 }, { |
|
1372 before: function (aPacket) { |
|
1373 this._assertPaused("eval"); |
|
1374 // Put the client in a tentative "resuming" state so we can prevent |
|
1375 // further requests that should only be sent in the paused state. |
|
1376 this._state = "resuming"; |
|
1377 return aPacket; |
|
1378 }, |
|
1379 after: function (aResponse) { |
|
1380 if (aResponse.error) { |
|
1381 // There was an error resuming, back to paused state. |
|
1382 this._state = "paused"; |
|
1383 } |
|
1384 return aResponse; |
|
1385 }, |
|
1386 telemetry: "CLIENTEVALUATE" |
|
1387 }), |
|
1388 |
|
1389 /** |
|
1390 * Detach from the thread actor. |
|
1391 * |
|
1392 * @param function aOnResponse |
|
1393 * Called with the response packet. |
|
1394 */ |
|
1395 detach: DebuggerClient.requester({ |
|
1396 type: "detach" |
|
1397 }, { |
|
1398 after: function (aResponse) { |
|
1399 this.client._threadClients.delete(this.actor); |
|
1400 this._parent.thread = null; |
|
1401 return aResponse; |
|
1402 }, |
|
1403 telemetry: "THREADDETACH" |
|
1404 }), |
|
1405 |
|
1406 /** |
|
1407 * Request to set a breakpoint in the specified location. |
|
1408 * |
|
1409 * @param object aLocation |
|
1410 * The source location object where the breakpoint will be set. |
|
1411 * @param function aOnResponse |
|
1412 * Called with the thread's response. |
|
1413 */ |
|
1414 setBreakpoint: function ({ url, line, column, condition }, aOnResponse) { |
|
1415 // A helper function that sets the breakpoint. |
|
1416 let doSetBreakpoint = function (aCallback) { |
|
1417 const location = { |
|
1418 url: url, |
|
1419 line: line, |
|
1420 column: column |
|
1421 }; |
|
1422 |
|
1423 let packet = { |
|
1424 to: this._actor, |
|
1425 type: "setBreakpoint", |
|
1426 location: location, |
|
1427 condition: condition |
|
1428 }; |
|
1429 this.client.request(packet, function (aResponse) { |
|
1430 // Ignoring errors, since the user may be setting a breakpoint in a |
|
1431 // dead script that will reappear on a page reload. |
|
1432 if (aOnResponse) { |
|
1433 let root = this.client.mainRoot; |
|
1434 let bpClient = new BreakpointClient( |
|
1435 this.client, |
|
1436 aResponse.actor, |
|
1437 location, |
|
1438 root.traits.conditionalBreakpoints ? condition : undefined |
|
1439 ); |
|
1440 aOnResponse(aResponse, bpClient); |
|
1441 } |
|
1442 if (aCallback) { |
|
1443 aCallback(); |
|
1444 } |
|
1445 }.bind(this)); |
|
1446 }.bind(this); |
|
1447 |
|
1448 // If the debuggee is paused, just set the breakpoint. |
|
1449 if (this.paused) { |
|
1450 doSetBreakpoint(); |
|
1451 return; |
|
1452 } |
|
1453 // Otherwise, force a pause in order to set the breakpoint. |
|
1454 this.interrupt(function (aResponse) { |
|
1455 if (aResponse.error) { |
|
1456 // Can't set the breakpoint if pausing failed. |
|
1457 aOnResponse(aResponse); |
|
1458 return; |
|
1459 } |
|
1460 doSetBreakpoint(this.resume.bind(this)); |
|
1461 }.bind(this)); |
|
1462 }, |
|
1463 |
|
1464 /** |
|
1465 * Release multiple thread-lifetime object actors. If any pause-lifetime |
|
1466 * actors are included in the request, a |notReleasable| error will return, |
|
1467 * but all the thread-lifetime ones will have been released. |
|
1468 * |
|
1469 * @param array actors |
|
1470 * An array with actor IDs to release. |
|
1471 */ |
|
1472 releaseMany: DebuggerClient.requester({ |
|
1473 type: "releaseMany", |
|
1474 actors: args(0), |
|
1475 }, { |
|
1476 telemetry: "RELEASEMANY" |
|
1477 }), |
|
1478 |
|
1479 /** |
|
1480 * Promote multiple pause-lifetime object actors to thread-lifetime ones. |
|
1481 * |
|
1482 * @param array actors |
|
1483 * An array with actor IDs to promote. |
|
1484 */ |
|
1485 threadGrips: DebuggerClient.requester({ |
|
1486 type: "threadGrips", |
|
1487 actors: args(0) |
|
1488 }, { |
|
1489 telemetry: "THREADGRIPS" |
|
1490 }), |
|
1491 |
|
1492 /** |
|
1493 * Return the event listeners defined on the page. |
|
1494 * |
|
1495 * @param aOnResponse Function |
|
1496 * Called with the thread's response. |
|
1497 */ |
|
1498 eventListeners: DebuggerClient.requester({ |
|
1499 type: "eventListeners" |
|
1500 }, { |
|
1501 telemetry: "EVENTLISTENERS" |
|
1502 }), |
|
1503 |
|
1504 /** |
|
1505 * Request the loaded sources for the current thread. |
|
1506 * |
|
1507 * @param aOnResponse Function |
|
1508 * Called with the thread's response. |
|
1509 */ |
|
1510 getSources: DebuggerClient.requester({ |
|
1511 type: "sources" |
|
1512 }, { |
|
1513 telemetry: "SOURCES" |
|
1514 }), |
|
1515 |
|
1516 _doInterrupted: function (aAction, aError) { |
|
1517 if (this.paused) { |
|
1518 aAction(); |
|
1519 return; |
|
1520 } |
|
1521 this.interrupt(function (aResponse) { |
|
1522 if (aResponse) { |
|
1523 aError(aResponse); |
|
1524 return; |
|
1525 } |
|
1526 aAction(); |
|
1527 this.resume(); |
|
1528 }.bind(this)); |
|
1529 }, |
|
1530 |
|
1531 /** |
|
1532 * Clear the thread's source script cache. A scriptscleared event |
|
1533 * will be sent. |
|
1534 */ |
|
1535 _clearScripts: function () { |
|
1536 if (Object.keys(this._scriptCache).length > 0) { |
|
1537 this._scriptCache = {} |
|
1538 this.notify("scriptscleared"); |
|
1539 } |
|
1540 }, |
|
1541 |
|
1542 /** |
|
1543 * Request frames from the callstack for the current thread. |
|
1544 * |
|
1545 * @param aStart integer |
|
1546 * The number of the youngest stack frame to return (the youngest |
|
1547 * frame is 0). |
|
1548 * @param aCount integer |
|
1549 * The maximum number of frames to return, or null to return all |
|
1550 * frames. |
|
1551 * @param aOnResponse function |
|
1552 * Called with the thread's response. |
|
1553 */ |
|
1554 getFrames: DebuggerClient.requester({ |
|
1555 type: "frames", |
|
1556 start: args(0), |
|
1557 count: args(1) |
|
1558 }, { |
|
1559 telemetry: "FRAMES" |
|
1560 }), |
|
1561 |
|
1562 /** |
|
1563 * An array of cached frames. Clients can observe the framesadded and |
|
1564 * framescleared event to keep up to date on changes to this cache, |
|
1565 * and can fill it using the fillFrames method. |
|
1566 */ |
|
1567 get cachedFrames() { return this._frameCache; }, |
|
1568 |
|
1569 /** |
|
1570 * true if there are more stack frames available on the server. |
|
1571 */ |
|
1572 get moreFrames() { |
|
1573 return this.paused && (!this._frameCache || this._frameCache.length == 0 |
|
1574 || !this._frameCache[this._frameCache.length - 1].oldest); |
|
1575 }, |
|
1576 |
|
1577 /** |
|
1578 * Ensure that at least aTotal stack frames have been loaded in the |
|
1579 * ThreadClient's stack frame cache. A framesadded event will be |
|
1580 * sent when the stack frame cache is updated. |
|
1581 * |
|
1582 * @param aTotal number |
|
1583 * The minimum number of stack frames to be included. |
|
1584 * @param aCallback function |
|
1585 * Optional callback function called when frames have been loaded |
|
1586 * @returns true if a framesadded notification should be expected. |
|
1587 */ |
|
1588 fillFrames: function (aTotal, aCallback=noop) { |
|
1589 this._assertPaused("fillFrames"); |
|
1590 if (this._frameCache.length >= aTotal) { |
|
1591 return false; |
|
1592 } |
|
1593 |
|
1594 let numFrames = this._frameCache.length; |
|
1595 |
|
1596 this.getFrames(numFrames, aTotal - numFrames, (aResponse) => { |
|
1597 if (aResponse.error) { |
|
1598 aCallback(aResponse); |
|
1599 return; |
|
1600 } |
|
1601 |
|
1602 for each (let frame in aResponse.frames) { |
|
1603 this._frameCache[frame.depth] = frame; |
|
1604 } |
|
1605 |
|
1606 // If we got as many frames as we asked for, there might be more |
|
1607 // frames available. |
|
1608 this.notify("framesadded"); |
|
1609 |
|
1610 aCallback(aResponse); |
|
1611 }); |
|
1612 |
|
1613 return true; |
|
1614 }, |
|
1615 |
|
1616 /** |
|
1617 * Clear the thread's stack frame cache. A framescleared event |
|
1618 * will be sent. |
|
1619 */ |
|
1620 _clearFrames: function () { |
|
1621 if (this._frameCache.length > 0) { |
|
1622 this._frameCache = []; |
|
1623 this.notify("framescleared"); |
|
1624 } |
|
1625 }, |
|
1626 |
|
1627 /** |
|
1628 * Return a ObjectClient object for the given object grip. |
|
1629 * |
|
1630 * @param aGrip object |
|
1631 * A pause-lifetime object grip returned by the protocol. |
|
1632 */ |
|
1633 pauseGrip: function (aGrip) { |
|
1634 if (aGrip.actor in this._pauseGrips) { |
|
1635 return this._pauseGrips[aGrip.actor]; |
|
1636 } |
|
1637 |
|
1638 let client = new ObjectClient(this.client, aGrip); |
|
1639 this._pauseGrips[aGrip.actor] = client; |
|
1640 return client; |
|
1641 }, |
|
1642 |
|
1643 /** |
|
1644 * Get or create a long string client, checking the grip client cache if it |
|
1645 * already exists. |
|
1646 * |
|
1647 * @param aGrip Object |
|
1648 * The long string grip returned by the protocol. |
|
1649 * @param aGripCacheName String |
|
1650 * The property name of the grip client cache to check for existing |
|
1651 * clients in. |
|
1652 */ |
|
1653 _longString: function (aGrip, aGripCacheName) { |
|
1654 if (aGrip.actor in this[aGripCacheName]) { |
|
1655 return this[aGripCacheName][aGrip.actor]; |
|
1656 } |
|
1657 |
|
1658 let client = new LongStringClient(this.client, aGrip); |
|
1659 this[aGripCacheName][aGrip.actor] = client; |
|
1660 return client; |
|
1661 }, |
|
1662 |
|
1663 /** |
|
1664 * Return an instance of LongStringClient for the given long string grip that |
|
1665 * is scoped to the current pause. |
|
1666 * |
|
1667 * @param aGrip Object |
|
1668 * The long string grip returned by the protocol. |
|
1669 */ |
|
1670 pauseLongString: function (aGrip) { |
|
1671 return this._longString(aGrip, "_pauseGrips"); |
|
1672 }, |
|
1673 |
|
1674 /** |
|
1675 * Return an instance of LongStringClient for the given long string grip that |
|
1676 * is scoped to the thread lifetime. |
|
1677 * |
|
1678 * @param aGrip Object |
|
1679 * The long string grip returned by the protocol. |
|
1680 */ |
|
1681 threadLongString: function (aGrip) { |
|
1682 return this._longString(aGrip, "_threadGrips"); |
|
1683 }, |
|
1684 |
|
1685 /** |
|
1686 * Clear and invalidate all the grip clients from the given cache. |
|
1687 * |
|
1688 * @param aGripCacheName |
|
1689 * The property name of the grip cache we want to clear. |
|
1690 */ |
|
1691 _clearObjectClients: function (aGripCacheName) { |
|
1692 for each (let grip in this[aGripCacheName]) { |
|
1693 grip.valid = false; |
|
1694 } |
|
1695 this[aGripCacheName] = {}; |
|
1696 }, |
|
1697 |
|
1698 /** |
|
1699 * Invalidate pause-lifetime grip clients and clear the list of current grip |
|
1700 * clients. |
|
1701 */ |
|
1702 _clearPauseGrips: function () { |
|
1703 this._clearObjectClients("_pauseGrips"); |
|
1704 }, |
|
1705 |
|
1706 /** |
|
1707 * Invalidate thread-lifetime grip clients and clear the list of current grip |
|
1708 * clients. |
|
1709 */ |
|
1710 _clearThreadGrips: function () { |
|
1711 this._clearObjectClients("_threadGrips"); |
|
1712 }, |
|
1713 |
|
1714 /** |
|
1715 * Handle thread state change by doing necessary cleanup and notifying all |
|
1716 * registered listeners. |
|
1717 */ |
|
1718 _onThreadState: function (aPacket) { |
|
1719 this._state = ThreadStateTypes[aPacket.type]; |
|
1720 this._clearFrames(); |
|
1721 this._clearPauseGrips(); |
|
1722 aPacket.type === ThreadStateTypes.detached && this._clearThreadGrips(); |
|
1723 this.client._eventsEnabled && this.notify(aPacket.type, aPacket); |
|
1724 }, |
|
1725 |
|
1726 /** |
|
1727 * Return an EnvironmentClient instance for the given environment actor form. |
|
1728 */ |
|
1729 environment: function (aForm) { |
|
1730 return new EnvironmentClient(this.client, aForm); |
|
1731 }, |
|
1732 |
|
1733 /** |
|
1734 * Return an instance of SourceClient for the given source actor form. |
|
1735 */ |
|
1736 source: function (aForm) { |
|
1737 if (aForm.actor in this._threadGrips) { |
|
1738 return this._threadGrips[aForm.actor]; |
|
1739 } |
|
1740 |
|
1741 return this._threadGrips[aForm.actor] = new SourceClient(this, aForm); |
|
1742 }, |
|
1743 |
|
1744 /** |
|
1745 * Request the prototype and own properties of mutlipleObjects. |
|
1746 * |
|
1747 * @param aOnResponse function |
|
1748 * Called with the request's response. |
|
1749 * @param actors [string] |
|
1750 * List of actor ID of the queried objects. |
|
1751 */ |
|
1752 getPrototypesAndProperties: DebuggerClient.requester({ |
|
1753 type: "prototypesAndProperties", |
|
1754 actors: args(0) |
|
1755 }, { |
|
1756 telemetry: "PROTOTYPESANDPROPERTIES" |
|
1757 }) |
|
1758 }; |
|
1759 |
|
1760 eventSource(ThreadClient.prototype); |
|
1761 |
|
1762 /** |
|
1763 * Creates a tracing profiler client for the remote debugging protocol |
|
1764 * server. This client is a front to the trace actor created on the |
|
1765 * server side, hiding the protocol details in a traditional |
|
1766 * JavaScript API. |
|
1767 * |
|
1768 * @param aClient DebuggerClient |
|
1769 * The debugger client parent. |
|
1770 * @param aActor string |
|
1771 * The actor ID for this thread. |
|
1772 */ |
|
1773 function TraceClient(aClient, aActor) { |
|
1774 this._client = aClient; |
|
1775 this._actor = aActor; |
|
1776 this._activeTraces = new Set(); |
|
1777 this._waitingPackets = new Map(); |
|
1778 this._expectedPacket = 0; |
|
1779 this.request = this._client.request; |
|
1780 } |
|
1781 |
|
1782 TraceClient.prototype = { |
|
1783 get actor() { return this._actor; }, |
|
1784 get tracing() { return this._activeTraces.size > 0; }, |
|
1785 |
|
1786 get _transport() { return this._client._transport; }, |
|
1787 |
|
1788 /** |
|
1789 * Detach from the trace actor. |
|
1790 */ |
|
1791 detach: DebuggerClient.requester({ |
|
1792 type: "detach" |
|
1793 }, { |
|
1794 after: function (aResponse) { |
|
1795 this._client._tracerClients.delete(this.actor); |
|
1796 return aResponse; |
|
1797 }, |
|
1798 telemetry: "TRACERDETACH" |
|
1799 }), |
|
1800 |
|
1801 /** |
|
1802 * Start a new trace. |
|
1803 * |
|
1804 * @param aTrace [string] |
|
1805 * An array of trace types to be recorded by the new trace. |
|
1806 * |
|
1807 * @param aName string |
|
1808 * The name of the new trace. |
|
1809 * |
|
1810 * @param aOnResponse function |
|
1811 * Called with the request's response. |
|
1812 */ |
|
1813 startTrace: DebuggerClient.requester({ |
|
1814 type: "startTrace", |
|
1815 name: args(1), |
|
1816 trace: args(0) |
|
1817 }, { |
|
1818 after: function (aResponse) { |
|
1819 if (aResponse.error) { |
|
1820 return aResponse; |
|
1821 } |
|
1822 |
|
1823 if (!this.tracing) { |
|
1824 this._waitingPackets.clear(); |
|
1825 this._expectedPacket = 0; |
|
1826 } |
|
1827 this._activeTraces.add(aResponse.name); |
|
1828 |
|
1829 return aResponse; |
|
1830 }, |
|
1831 telemetry: "STARTTRACE" |
|
1832 }), |
|
1833 |
|
1834 /** |
|
1835 * End a trace. If a name is provided, stop the named |
|
1836 * trace. Otherwise, stop the most recently started trace. |
|
1837 * |
|
1838 * @param aName string |
|
1839 * The name of the trace to stop. |
|
1840 * |
|
1841 * @param aOnResponse function |
|
1842 * Called with the request's response. |
|
1843 */ |
|
1844 stopTrace: DebuggerClient.requester({ |
|
1845 type: "stopTrace", |
|
1846 name: args(0) |
|
1847 }, { |
|
1848 after: function (aResponse) { |
|
1849 if (aResponse.error) { |
|
1850 return aResponse; |
|
1851 } |
|
1852 |
|
1853 this._activeTraces.delete(aResponse.name); |
|
1854 |
|
1855 return aResponse; |
|
1856 }, |
|
1857 telemetry: "STOPTRACE" |
|
1858 }) |
|
1859 }; |
|
1860 |
|
1861 /** |
|
1862 * Grip clients are used to retrieve information about the relevant object. |
|
1863 * |
|
1864 * @param aClient DebuggerClient |
|
1865 * The debugger client parent. |
|
1866 * @param aGrip object |
|
1867 * A pause-lifetime object grip returned by the protocol. |
|
1868 */ |
|
1869 function ObjectClient(aClient, aGrip) |
|
1870 { |
|
1871 this._grip = aGrip; |
|
1872 this._client = aClient; |
|
1873 this.request = this._client.request; |
|
1874 } |
|
1875 |
|
1876 ObjectClient.prototype = { |
|
1877 get actor() { return this._grip.actor }, |
|
1878 get _transport() { return this._client._transport; }, |
|
1879 |
|
1880 valid: true, |
|
1881 |
|
1882 get isFrozen() this._grip.frozen, |
|
1883 get isSealed() this._grip.sealed, |
|
1884 get isExtensible() this._grip.extensible, |
|
1885 |
|
1886 getDefinitionSite: DebuggerClient.requester({ |
|
1887 type: "definitionSite" |
|
1888 }, { |
|
1889 before: function (aPacket) { |
|
1890 if (this._grip.class != "Function") { |
|
1891 throw new Error("getDefinitionSite is only valid for function grips."); |
|
1892 } |
|
1893 return aPacket; |
|
1894 } |
|
1895 }), |
|
1896 |
|
1897 /** |
|
1898 * Request the names of a function's formal parameters. |
|
1899 * |
|
1900 * @param aOnResponse function |
|
1901 * Called with an object of the form: |
|
1902 * { parameterNames:[<parameterName>, ...] } |
|
1903 * where each <parameterName> is the name of a parameter. |
|
1904 */ |
|
1905 getParameterNames: DebuggerClient.requester({ |
|
1906 type: "parameterNames" |
|
1907 }, { |
|
1908 before: function (aPacket) { |
|
1909 if (this._grip["class"] !== "Function") { |
|
1910 throw new Error("getParameterNames is only valid for function grips."); |
|
1911 } |
|
1912 return aPacket; |
|
1913 }, |
|
1914 telemetry: "PARAMETERNAMES" |
|
1915 }), |
|
1916 |
|
1917 /** |
|
1918 * Request the names of the properties defined on the object and not its |
|
1919 * prototype. |
|
1920 * |
|
1921 * @param aOnResponse function Called with the request's response. |
|
1922 */ |
|
1923 getOwnPropertyNames: DebuggerClient.requester({ |
|
1924 type: "ownPropertyNames" |
|
1925 }, { |
|
1926 telemetry: "OWNPROPERTYNAMES" |
|
1927 }), |
|
1928 |
|
1929 /** |
|
1930 * Request the prototype and own properties of the object. |
|
1931 * |
|
1932 * @param aOnResponse function Called with the request's response. |
|
1933 */ |
|
1934 getPrototypeAndProperties: DebuggerClient.requester({ |
|
1935 type: "prototypeAndProperties" |
|
1936 }, { |
|
1937 telemetry: "PROTOTYPEANDPROPERTIES" |
|
1938 }), |
|
1939 |
|
1940 /** |
|
1941 * Request the property descriptor of the object's specified property. |
|
1942 * |
|
1943 * @param aName string The name of the requested property. |
|
1944 * @param aOnResponse function Called with the request's response. |
|
1945 */ |
|
1946 getProperty: DebuggerClient.requester({ |
|
1947 type: "property", |
|
1948 name: args(0) |
|
1949 }, { |
|
1950 telemetry: "PROPERTY" |
|
1951 }), |
|
1952 |
|
1953 /** |
|
1954 * Request the prototype of the object. |
|
1955 * |
|
1956 * @param aOnResponse function Called with the request's response. |
|
1957 */ |
|
1958 getPrototype: DebuggerClient.requester({ |
|
1959 type: "prototype" |
|
1960 }, { |
|
1961 telemetry: "PROTOTYPE" |
|
1962 }), |
|
1963 |
|
1964 /** |
|
1965 * Request the display string of the object. |
|
1966 * |
|
1967 * @param aOnResponse function Called with the request's response. |
|
1968 */ |
|
1969 getDisplayString: DebuggerClient.requester({ |
|
1970 type: "displayString" |
|
1971 }, { |
|
1972 telemetry: "DISPLAYSTRING" |
|
1973 }), |
|
1974 |
|
1975 /** |
|
1976 * Request the scope of the object. |
|
1977 * |
|
1978 * @param aOnResponse function Called with the request's response. |
|
1979 */ |
|
1980 getScope: DebuggerClient.requester({ |
|
1981 type: "scope" |
|
1982 }, { |
|
1983 before: function (aPacket) { |
|
1984 if (this._grip.class !== "Function") { |
|
1985 throw new Error("scope is only valid for function grips."); |
|
1986 } |
|
1987 return aPacket; |
|
1988 }, |
|
1989 telemetry: "SCOPE" |
|
1990 }) |
|
1991 }; |
|
1992 |
|
1993 /** |
|
1994 * A LongStringClient provides a way to access "very long" strings from the |
|
1995 * debugger server. |
|
1996 * |
|
1997 * @param aClient DebuggerClient |
|
1998 * The debugger client parent. |
|
1999 * @param aGrip Object |
|
2000 * A pause-lifetime long string grip returned by the protocol. |
|
2001 */ |
|
2002 function LongStringClient(aClient, aGrip) { |
|
2003 this._grip = aGrip; |
|
2004 this._client = aClient; |
|
2005 this.request = this._client.request; |
|
2006 } |
|
2007 |
|
2008 LongStringClient.prototype = { |
|
2009 get actor() { return this._grip.actor; }, |
|
2010 get length() { return this._grip.length; }, |
|
2011 get initial() { return this._grip.initial; }, |
|
2012 get _transport() { return this._client._transport; }, |
|
2013 |
|
2014 valid: true, |
|
2015 |
|
2016 /** |
|
2017 * Get the substring of this LongString from aStart to aEnd. |
|
2018 * |
|
2019 * @param aStart Number |
|
2020 * The starting index. |
|
2021 * @param aEnd Number |
|
2022 * The ending index. |
|
2023 * @param aCallback Function |
|
2024 * The function called when we receive the substring. |
|
2025 */ |
|
2026 substring: DebuggerClient.requester({ |
|
2027 type: "substring", |
|
2028 start: args(0), |
|
2029 end: args(1) |
|
2030 }, { |
|
2031 telemetry: "SUBSTRING" |
|
2032 }), |
|
2033 }; |
|
2034 |
|
2035 /** |
|
2036 * A SourceClient provides a way to access the source text of a script. |
|
2037 * |
|
2038 * @param aClient ThreadClient |
|
2039 * The thread client parent. |
|
2040 * @param aForm Object |
|
2041 * The form sent across the remote debugging protocol. |
|
2042 */ |
|
2043 function SourceClient(aClient, aForm) { |
|
2044 this._form = aForm; |
|
2045 this._isBlackBoxed = aForm.isBlackBoxed; |
|
2046 this._isPrettyPrinted = aForm.isPrettyPrinted; |
|
2047 this._activeThread = aClient; |
|
2048 this._client = aClient.client; |
|
2049 } |
|
2050 |
|
2051 SourceClient.prototype = { |
|
2052 get _transport() this._client._transport, |
|
2053 get isBlackBoxed() this._isBlackBoxed, |
|
2054 get isPrettyPrinted() this._isPrettyPrinted, |
|
2055 get actor() this._form.actor, |
|
2056 get request() this._client.request, |
|
2057 get url() this._form.url, |
|
2058 |
|
2059 /** |
|
2060 * Black box this SourceClient's source. |
|
2061 * |
|
2062 * @param aCallback Function |
|
2063 * The callback function called when we receive the response from the server. |
|
2064 */ |
|
2065 blackBox: DebuggerClient.requester({ |
|
2066 type: "blackbox" |
|
2067 }, { |
|
2068 telemetry: "BLACKBOX", |
|
2069 after: function (aResponse) { |
|
2070 if (!aResponse.error) { |
|
2071 this._isBlackBoxed = true; |
|
2072 if (this._activeThread) { |
|
2073 this._activeThread.notify("blackboxchange", this); |
|
2074 } |
|
2075 } |
|
2076 return aResponse; |
|
2077 } |
|
2078 }), |
|
2079 |
|
2080 /** |
|
2081 * Un-black box this SourceClient's source. |
|
2082 * |
|
2083 * @param aCallback Function |
|
2084 * The callback function called when we receive the response from the server. |
|
2085 */ |
|
2086 unblackBox: DebuggerClient.requester({ |
|
2087 type: "unblackbox" |
|
2088 }, { |
|
2089 telemetry: "UNBLACKBOX", |
|
2090 after: function (aResponse) { |
|
2091 if (!aResponse.error) { |
|
2092 this._isBlackBoxed = false; |
|
2093 if (this._activeThread) { |
|
2094 this._activeThread.notify("blackboxchange", this); |
|
2095 } |
|
2096 } |
|
2097 return aResponse; |
|
2098 } |
|
2099 }), |
|
2100 |
|
2101 /** |
|
2102 * Get a long string grip for this SourceClient's source. |
|
2103 */ |
|
2104 source: function (aCallback) { |
|
2105 let packet = { |
|
2106 to: this._form.actor, |
|
2107 type: "source" |
|
2108 }; |
|
2109 this._client.request(packet, aResponse => { |
|
2110 this._onSourceResponse(aResponse, aCallback) |
|
2111 }); |
|
2112 }, |
|
2113 |
|
2114 /** |
|
2115 * Pretty print this source's text. |
|
2116 */ |
|
2117 prettyPrint: function (aIndent, aCallback) { |
|
2118 const packet = { |
|
2119 to: this._form.actor, |
|
2120 type: "prettyPrint", |
|
2121 indent: aIndent |
|
2122 }; |
|
2123 this._client.request(packet, aResponse => { |
|
2124 if (!aResponse.error) { |
|
2125 this._isPrettyPrinted = true; |
|
2126 this._activeThread._clearFrames(); |
|
2127 this._activeThread.notify("prettyprintchange", this); |
|
2128 } |
|
2129 this._onSourceResponse(aResponse, aCallback); |
|
2130 }); |
|
2131 }, |
|
2132 |
|
2133 /** |
|
2134 * Stop pretty printing this source's text. |
|
2135 */ |
|
2136 disablePrettyPrint: function (aCallback) { |
|
2137 const packet = { |
|
2138 to: this._form.actor, |
|
2139 type: "disablePrettyPrint" |
|
2140 }; |
|
2141 this._client.request(packet, aResponse => { |
|
2142 if (!aResponse.error) { |
|
2143 this._isPrettyPrinted = false; |
|
2144 this._activeThread._clearFrames(); |
|
2145 this._activeThread.notify("prettyprintchange", this); |
|
2146 } |
|
2147 this._onSourceResponse(aResponse, aCallback); |
|
2148 }); |
|
2149 }, |
|
2150 |
|
2151 _onSourceResponse: function (aResponse, aCallback) { |
|
2152 if (aResponse.error) { |
|
2153 aCallback(aResponse); |
|
2154 return; |
|
2155 } |
|
2156 |
|
2157 if (typeof aResponse.source === "string") { |
|
2158 aCallback(aResponse); |
|
2159 return; |
|
2160 } |
|
2161 |
|
2162 let { contentType, source } = aResponse; |
|
2163 let longString = this._activeThread.threadLongString(source); |
|
2164 longString.substring(0, longString.length, function (aResponse) { |
|
2165 if (aResponse.error) { |
|
2166 aCallback(aResponse); |
|
2167 return; |
|
2168 } |
|
2169 |
|
2170 aCallback({ |
|
2171 source: aResponse.substring, |
|
2172 contentType: contentType |
|
2173 }); |
|
2174 }); |
|
2175 } |
|
2176 }; |
|
2177 |
|
2178 /** |
|
2179 * Breakpoint clients are used to remove breakpoints that are no longer used. |
|
2180 * |
|
2181 * @param aClient DebuggerClient |
|
2182 * The debugger client parent. |
|
2183 * @param aActor string |
|
2184 * The actor ID for this breakpoint. |
|
2185 * @param aLocation object |
|
2186 * The location of the breakpoint. This is an object with two properties: |
|
2187 * url and line. |
|
2188 * @param aCondition string |
|
2189 * The conditional expression of the breakpoint |
|
2190 */ |
|
2191 function BreakpointClient(aClient, aActor, aLocation, aCondition) { |
|
2192 this._client = aClient; |
|
2193 this._actor = aActor; |
|
2194 this.location = aLocation; |
|
2195 this.request = this._client.request; |
|
2196 |
|
2197 // The condition property should only exist if it's a truthy value |
|
2198 if (aCondition) { |
|
2199 this.condition = aCondition; |
|
2200 } |
|
2201 } |
|
2202 |
|
2203 BreakpointClient.prototype = { |
|
2204 |
|
2205 _actor: null, |
|
2206 get actor() { return this._actor; }, |
|
2207 get _transport() { return this._client._transport; }, |
|
2208 |
|
2209 /** |
|
2210 * Remove the breakpoint from the server. |
|
2211 */ |
|
2212 remove: DebuggerClient.requester({ |
|
2213 type: "delete" |
|
2214 }, { |
|
2215 telemetry: "DELETE" |
|
2216 }), |
|
2217 |
|
2218 /** |
|
2219 * Determines if this breakpoint has a condition |
|
2220 */ |
|
2221 hasCondition: function() { |
|
2222 let root = this._client.mainRoot; |
|
2223 // XXX bug 990137: We will remove support for client-side handling of |
|
2224 // conditional breakpoints |
|
2225 if (root.traits.conditionalBreakpoints) { |
|
2226 return "condition" in this; |
|
2227 } else { |
|
2228 return "conditionalExpression" in this; |
|
2229 } |
|
2230 }, |
|
2231 |
|
2232 /** |
|
2233 * Get the condition of this breakpoint. Currently we have to |
|
2234 * support locally emulated conditional breakpoints until the |
|
2235 * debugger servers are updated (see bug 990137). We used a |
|
2236 * different property when moving it server-side to ensure that we |
|
2237 * are testing the right code. |
|
2238 */ |
|
2239 getCondition: function() { |
|
2240 let root = this._client.mainRoot; |
|
2241 if (root.traits.conditionalBreakpoints) { |
|
2242 return this.condition; |
|
2243 } else { |
|
2244 return this.conditionalExpression; |
|
2245 } |
|
2246 }, |
|
2247 |
|
2248 /** |
|
2249 * Set the condition of this breakpoint |
|
2250 */ |
|
2251 setCondition: function(gThreadClient, aCondition) { |
|
2252 let root = this._client.mainRoot; |
|
2253 let deferred = promise.defer(); |
|
2254 |
|
2255 if (root.traits.conditionalBreakpoints) { |
|
2256 let info = { |
|
2257 url: this.location.url, |
|
2258 line: this.location.line, |
|
2259 column: this.location.column, |
|
2260 condition: aCondition |
|
2261 }; |
|
2262 |
|
2263 // Remove the current breakpoint and add a new one with the |
|
2264 // condition. |
|
2265 this.remove(aResponse => { |
|
2266 if (aResponse && aResponse.error) { |
|
2267 deferred.reject(aResponse); |
|
2268 return; |
|
2269 } |
|
2270 |
|
2271 gThreadClient.setBreakpoint(info, (aResponse, aNewBreakpoint) => { |
|
2272 if (aResponse && aResponse.error) { |
|
2273 deferred.reject(aResponse); |
|
2274 } else { |
|
2275 deferred.resolve(aNewBreakpoint); |
|
2276 } |
|
2277 }); |
|
2278 }); |
|
2279 } else { |
|
2280 // The property shouldn't even exist if the condition is blank |
|
2281 if(aCondition === "") { |
|
2282 delete this.conditionalExpression; |
|
2283 } |
|
2284 else { |
|
2285 this.conditionalExpression = aCondition; |
|
2286 } |
|
2287 deferred.resolve(this); |
|
2288 } |
|
2289 |
|
2290 return deferred.promise; |
|
2291 } |
|
2292 }; |
|
2293 |
|
2294 eventSource(BreakpointClient.prototype); |
|
2295 |
|
2296 /** |
|
2297 * Environment clients are used to manipulate the lexical environment actors. |
|
2298 * |
|
2299 * @param aClient DebuggerClient |
|
2300 * The debugger client parent. |
|
2301 * @param aForm Object |
|
2302 * The form sent across the remote debugging protocol. |
|
2303 */ |
|
2304 function EnvironmentClient(aClient, aForm) { |
|
2305 this._client = aClient; |
|
2306 this._form = aForm; |
|
2307 this.request = this._client.request; |
|
2308 } |
|
2309 |
|
2310 EnvironmentClient.prototype = { |
|
2311 |
|
2312 get actor() this._form.actor, |
|
2313 get _transport() { return this._client._transport; }, |
|
2314 |
|
2315 /** |
|
2316 * Fetches the bindings introduced by this lexical environment. |
|
2317 */ |
|
2318 getBindings: DebuggerClient.requester({ |
|
2319 type: "bindings" |
|
2320 }, { |
|
2321 telemetry: "BINDINGS" |
|
2322 }), |
|
2323 |
|
2324 /** |
|
2325 * Changes the value of the identifier whose name is name (a string) to that |
|
2326 * represented by value (a grip). |
|
2327 */ |
|
2328 assign: DebuggerClient.requester({ |
|
2329 type: "assign", |
|
2330 name: args(0), |
|
2331 value: args(1) |
|
2332 }, { |
|
2333 telemetry: "ASSIGN" |
|
2334 }) |
|
2335 }; |
|
2336 |
|
2337 eventSource(EnvironmentClient.prototype); |
|
2338 |
|
2339 /** |
|
2340 * Connects to a debugger server socket and returns a DebuggerTransport. |
|
2341 * |
|
2342 * @param aHost string |
|
2343 * The host name or IP address of the debugger server. |
|
2344 * @param aPort number |
|
2345 * The port number of the debugger server. |
|
2346 */ |
|
2347 this.debuggerSocketConnect = function (aHost, aPort) |
|
2348 { |
|
2349 let s = socketTransportService.createTransport(null, 0, aHost, aPort, null); |
|
2350 // By default the CONNECT socket timeout is very long, 65535 seconds, |
|
2351 // so that if we race to be in CONNECT state while the server socket is still |
|
2352 // initializing, the connection is stuck in connecting state for 18.20 hours! |
|
2353 s.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, 2); |
|
2354 |
|
2355 // openOutputStream may throw NS_ERROR_NOT_INITIALIZED if we hit some race |
|
2356 // where the nsISocketTransport gets shutdown in between its instantiation and |
|
2357 // the call to this method. |
|
2358 let transport; |
|
2359 try { |
|
2360 transport = new DebuggerTransport(s.openInputStream(0, 0, 0), |
|
2361 s.openOutputStream(0, 0, 0)); |
|
2362 } catch(e) { |
|
2363 DevToolsUtils.reportException("debuggerSocketConnect", e); |
|
2364 throw e; |
|
2365 } |
|
2366 return transport; |
|
2367 } |
|
2368 |
|
2369 /** |
|
2370 * Takes a pair of items and returns them as an array. |
|
2371 */ |
|
2372 function pair(aItemOne, aItemTwo) { |
|
2373 return [aItemOne, aItemTwo]; |
|
2374 } |
|
2375 function noop() {} |