|
1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; js-indent-level: 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 |
|
9 let B2G_ID = "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}"; |
|
10 |
|
11 let TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array", |
|
12 "Uint32Array", "Int8Array", "Int16Array", "Int32Array", "Float32Array", |
|
13 "Float64Array"]; |
|
14 |
|
15 // Number of items to preview in objects, arrays, maps, sets, lists, |
|
16 // collections, etc. |
|
17 let OBJECT_PREVIEW_MAX_ITEMS = 10; |
|
18 |
|
19 let addonManager = null; |
|
20 |
|
21 /** |
|
22 * This is a wrapper around amIAddonManager.mapURIToAddonID which always returns |
|
23 * false on B2G to avoid loading the add-on manager there and reports any |
|
24 * exceptions rather than throwing so that the caller doesn't have to worry |
|
25 * about them. |
|
26 */ |
|
27 function mapURIToAddonID(uri, id) { |
|
28 if (Services.appinfo.ID == B2G_ID) { |
|
29 return false; |
|
30 } |
|
31 |
|
32 if (!addonManager) { |
|
33 addonManager = Cc["@mozilla.org/addons/integration;1"]. |
|
34 getService(Ci.amIAddonManager); |
|
35 } |
|
36 |
|
37 try { |
|
38 return addonManager.mapURIToAddonID(uri, id); |
|
39 } |
|
40 catch (e) { |
|
41 DevtoolsUtils.reportException("mapURIToAddonID", e); |
|
42 return false; |
|
43 } |
|
44 } |
|
45 |
|
46 /** |
|
47 * BreakpointStore objects keep track of all breakpoints that get set so that we |
|
48 * can reset them when the same script is introduced to the thread again (such |
|
49 * as after a refresh). |
|
50 */ |
|
51 function BreakpointStore() { |
|
52 this._size = 0; |
|
53 |
|
54 // If we have a whole-line breakpoint set at LINE in URL, then |
|
55 // |
|
56 // this._wholeLineBreakpoints[URL][LINE] |
|
57 // |
|
58 // is an object |
|
59 // |
|
60 // { url, line[, actor] } |
|
61 // |
|
62 // where the `actor` property is optional. |
|
63 this._wholeLineBreakpoints = Object.create(null); |
|
64 |
|
65 // If we have a breakpoint set at LINE, COLUMN in URL, then |
|
66 // |
|
67 // this._breakpoints[URL][LINE][COLUMN] |
|
68 // |
|
69 // is an object |
|
70 // |
|
71 // { url, line, column[, actor] } |
|
72 // |
|
73 // where the `actor` property is optional. |
|
74 this._breakpoints = Object.create(null); |
|
75 } |
|
76 |
|
77 BreakpointStore.prototype = { |
|
78 _size: null, |
|
79 get size() { return this._size; }, |
|
80 |
|
81 /** |
|
82 * Add a breakpoint to the breakpoint store. |
|
83 * |
|
84 * @param Object aBreakpoint |
|
85 * The breakpoint to be added (not copied). It is an object with the |
|
86 * following properties: |
|
87 * - url |
|
88 * - line |
|
89 * - column (optional; omission implies that the breakpoint is for |
|
90 * the whole line) |
|
91 * - condition (optional) |
|
92 * - actor (optional) |
|
93 */ |
|
94 addBreakpoint: function (aBreakpoint) { |
|
95 let { url, line, column } = aBreakpoint; |
|
96 let updating = false; |
|
97 |
|
98 if (column != null) { |
|
99 if (!this._breakpoints[url]) { |
|
100 this._breakpoints[url] = []; |
|
101 } |
|
102 if (!this._breakpoints[url][line]) { |
|
103 this._breakpoints[url][line] = []; |
|
104 } |
|
105 this._breakpoints[url][line][column] = aBreakpoint; |
|
106 } else { |
|
107 // Add a breakpoint that breaks on the whole line. |
|
108 if (!this._wholeLineBreakpoints[url]) { |
|
109 this._wholeLineBreakpoints[url] = []; |
|
110 } |
|
111 this._wholeLineBreakpoints[url][line] = aBreakpoint; |
|
112 } |
|
113 |
|
114 this._size++; |
|
115 }, |
|
116 |
|
117 /** |
|
118 * Remove a breakpoint from the breakpoint store. |
|
119 * |
|
120 * @param Object aBreakpoint |
|
121 * The breakpoint to be removed. It is an object with the following |
|
122 * properties: |
|
123 * - url |
|
124 * - line |
|
125 * - column (optional) |
|
126 */ |
|
127 removeBreakpoint: function ({ url, line, column }) { |
|
128 if (column != null) { |
|
129 if (this._breakpoints[url]) { |
|
130 if (this._breakpoints[url][line]) { |
|
131 if (this._breakpoints[url][line][column]) { |
|
132 delete this._breakpoints[url][line][column]; |
|
133 this._size--; |
|
134 |
|
135 // If this was the last breakpoint on this line, delete the line from |
|
136 // `this._breakpoints[url]` as well. Otherwise `_iterLines` will yield |
|
137 // this line even though we no longer have breakpoints on |
|
138 // it. Furthermore, we use Object.keys() instead of just checking |
|
139 // `this._breakpoints[url].length` directly, because deleting |
|
140 // properties from sparse arrays doesn't update the `length` property |
|
141 // like adding them does. |
|
142 if (Object.keys(this._breakpoints[url][line]).length === 0) { |
|
143 delete this._breakpoints[url][line]; |
|
144 } |
|
145 } |
|
146 } |
|
147 } |
|
148 } else { |
|
149 if (this._wholeLineBreakpoints[url]) { |
|
150 if (this._wholeLineBreakpoints[url][line]) { |
|
151 delete this._wholeLineBreakpoints[url][line]; |
|
152 this._size--; |
|
153 } |
|
154 } |
|
155 } |
|
156 }, |
|
157 |
|
158 /** |
|
159 * Get a breakpoint from the breakpoint store. Will throw an error if the |
|
160 * breakpoint is not found. |
|
161 * |
|
162 * @param Object aLocation |
|
163 * The location of the breakpoint you are retrieving. It is an object |
|
164 * with the following properties: |
|
165 * - url |
|
166 * - line |
|
167 * - column (optional) |
|
168 */ |
|
169 getBreakpoint: function (aLocation) { |
|
170 let { url, line, column } = aLocation; |
|
171 dbg_assert(url != null); |
|
172 dbg_assert(line != null); |
|
173 |
|
174 var foundBreakpoint = this.hasBreakpoint(aLocation); |
|
175 if (foundBreakpoint == null) { |
|
176 throw new Error("No breakpoint at url = " + url |
|
177 + ", line = " + line |
|
178 + ", column = " + column); |
|
179 } |
|
180 |
|
181 return foundBreakpoint; |
|
182 }, |
|
183 |
|
184 /** |
|
185 * Checks if the breakpoint store has a requested breakpoint. |
|
186 * |
|
187 * @param Object aLocation |
|
188 * The location of the breakpoint you are retrieving. It is an object |
|
189 * with the following properties: |
|
190 * - url |
|
191 * - line |
|
192 * - column (optional) |
|
193 * @returns The stored breakpoint if it exists, null otherwise. |
|
194 */ |
|
195 hasBreakpoint: function (aLocation) { |
|
196 let { url, line, column } = aLocation; |
|
197 dbg_assert(url != null); |
|
198 dbg_assert(line != null); |
|
199 for (let bp of this.findBreakpoints(aLocation)) { |
|
200 // We will get whole line breakpoints before individual columns, so just |
|
201 // return the first one and if they didn't specify a column then they will |
|
202 // get the whole line breakpoint, and otherwise we will find the correct |
|
203 // one. |
|
204 return bp; |
|
205 } |
|
206 |
|
207 return null; |
|
208 }, |
|
209 |
|
210 /** |
|
211 * Iterate over the breakpoints in this breakpoint store. You can optionally |
|
212 * provide search parameters to filter the set of breakpoints down to those |
|
213 * that match your parameters. |
|
214 * |
|
215 * @param Object aSearchParams |
|
216 * Optional. An object with the following properties: |
|
217 * - url |
|
218 * - line (optional; requires the url property) |
|
219 * - column (optional; requires the line property) |
|
220 */ |
|
221 findBreakpoints: function* (aSearchParams={}) { |
|
222 if (aSearchParams.column != null) { |
|
223 dbg_assert(aSearchParams.line != null); |
|
224 } |
|
225 if (aSearchParams.line != null) { |
|
226 dbg_assert(aSearchParams.url != null); |
|
227 } |
|
228 |
|
229 for (let url of this._iterUrls(aSearchParams.url)) { |
|
230 for (let line of this._iterLines(url, aSearchParams.line)) { |
|
231 // Always yield whole line breakpoints first. See comment in |
|
232 // |BreakpointStore.prototype.hasBreakpoint|. |
|
233 if (aSearchParams.column == null |
|
234 && this._wholeLineBreakpoints[url] |
|
235 && this._wholeLineBreakpoints[url][line]) { |
|
236 yield this._wholeLineBreakpoints[url][line]; |
|
237 } |
|
238 for (let column of this._iterColumns(url, line, aSearchParams.column)) { |
|
239 yield this._breakpoints[url][line][column]; |
|
240 } |
|
241 } |
|
242 } |
|
243 }, |
|
244 |
|
245 _iterUrls: function* (aUrl) { |
|
246 if (aUrl) { |
|
247 if (this._breakpoints[aUrl] || this._wholeLineBreakpoints[aUrl]) { |
|
248 yield aUrl; |
|
249 } |
|
250 } else { |
|
251 for (let url of Object.keys(this._wholeLineBreakpoints)) { |
|
252 yield url; |
|
253 } |
|
254 for (let url of Object.keys(this._breakpoints)) { |
|
255 if (url in this._wholeLineBreakpoints) { |
|
256 continue; |
|
257 } |
|
258 yield url; |
|
259 } |
|
260 } |
|
261 }, |
|
262 |
|
263 _iterLines: function* (aUrl, aLine) { |
|
264 if (aLine != null) { |
|
265 if ((this._wholeLineBreakpoints[aUrl] |
|
266 && this._wholeLineBreakpoints[aUrl][aLine]) |
|
267 || (this._breakpoints[aUrl] && this._breakpoints[aUrl][aLine])) { |
|
268 yield aLine; |
|
269 } |
|
270 } else { |
|
271 const wholeLines = this._wholeLineBreakpoints[aUrl] |
|
272 ? Object.keys(this._wholeLineBreakpoints[aUrl]) |
|
273 : []; |
|
274 const columnLines = this._breakpoints[aUrl] |
|
275 ? Object.keys(this._breakpoints[aUrl]) |
|
276 : []; |
|
277 |
|
278 const lines = wholeLines.concat(columnLines).sort(); |
|
279 |
|
280 let lastLine; |
|
281 for (let line of lines) { |
|
282 if (line === lastLine) { |
|
283 continue; |
|
284 } |
|
285 yield line; |
|
286 lastLine = line; |
|
287 } |
|
288 } |
|
289 }, |
|
290 |
|
291 _iterColumns: function* (aUrl, aLine, aColumn) { |
|
292 if (!this._breakpoints[aUrl] || !this._breakpoints[aUrl][aLine]) { |
|
293 return; |
|
294 } |
|
295 |
|
296 if (aColumn != null) { |
|
297 if (this._breakpoints[aUrl][aLine][aColumn]) { |
|
298 yield aColumn; |
|
299 } |
|
300 } else { |
|
301 for (let column in this._breakpoints[aUrl][aLine]) { |
|
302 yield column; |
|
303 } |
|
304 } |
|
305 }, |
|
306 }; |
|
307 |
|
308 /** |
|
309 * Manages pushing event loops and automatically pops and exits them in the |
|
310 * correct order as they are resolved. |
|
311 * |
|
312 * @param nsIJSInspector inspector |
|
313 * The underlying JS inspector we use to enter and exit nested event |
|
314 * loops. |
|
315 * @param ThreadActor thread |
|
316 * The thread actor instance that owns this EventLoopStack. |
|
317 * @param DebuggerServerConnection connection |
|
318 * The remote protocol connection associated with this event loop stack. |
|
319 * @param Object hooks |
|
320 * An object with the following properties: |
|
321 * - url: The URL string of the debuggee we are spinning an event loop |
|
322 * for. |
|
323 * - preNest: function called before entering a nested event loop |
|
324 * - postNest: function called after exiting a nested event loop |
|
325 */ |
|
326 function EventLoopStack({ inspector, thread, connection, hooks }) { |
|
327 this._inspector = inspector; |
|
328 this._hooks = hooks; |
|
329 this._thread = thread; |
|
330 this._connection = connection; |
|
331 } |
|
332 |
|
333 EventLoopStack.prototype = { |
|
334 /** |
|
335 * The number of nested event loops on the stack. |
|
336 */ |
|
337 get size() { |
|
338 return this._inspector.eventLoopNestLevel; |
|
339 }, |
|
340 |
|
341 /** |
|
342 * The URL of the debuggee who pushed the event loop on top of the stack. |
|
343 */ |
|
344 get lastPausedUrl() { |
|
345 let url = null; |
|
346 if (this.size > 0) { |
|
347 try { |
|
348 url = this._inspector.lastNestRequestor.url |
|
349 } catch (e) { |
|
350 // The tab's URL getter may throw if the tab is destroyed by the time |
|
351 // this code runs, but we don't really care at this point. |
|
352 dumpn(e); |
|
353 } |
|
354 } |
|
355 return url; |
|
356 }, |
|
357 |
|
358 /** |
|
359 * The DebuggerServerConnection of the debugger who pushed the event loop on |
|
360 * top of the stack |
|
361 */ |
|
362 get lastConnection() { |
|
363 return this._inspector.lastNestRequestor._connection; |
|
364 }, |
|
365 |
|
366 /** |
|
367 * Push a new nested event loop onto the stack. |
|
368 * |
|
369 * @returns EventLoop |
|
370 */ |
|
371 push: function () { |
|
372 return new EventLoop({ |
|
373 inspector: this._inspector, |
|
374 thread: this._thread, |
|
375 connection: this._connection, |
|
376 hooks: this._hooks |
|
377 }); |
|
378 } |
|
379 }; |
|
380 |
|
381 /** |
|
382 * An object that represents a nested event loop. It is used as the nest |
|
383 * requestor with nsIJSInspector instances. |
|
384 * |
|
385 * @param nsIJSInspector inspector |
|
386 * The JS Inspector that runs nested event loops. |
|
387 * @param ThreadActor thread |
|
388 * The thread actor that is creating this nested event loop. |
|
389 * @param DebuggerServerConnection connection |
|
390 * The remote protocol connection associated with this event loop. |
|
391 * @param Object hooks |
|
392 * The same hooks object passed into EventLoopStack during its |
|
393 * initialization. |
|
394 */ |
|
395 function EventLoop({ inspector, thread, connection, hooks }) { |
|
396 this._inspector = inspector; |
|
397 this._thread = thread; |
|
398 this._hooks = hooks; |
|
399 this._connection = connection; |
|
400 |
|
401 this.enter = this.enter.bind(this); |
|
402 this.resolve = this.resolve.bind(this); |
|
403 } |
|
404 |
|
405 EventLoop.prototype = { |
|
406 entered: false, |
|
407 resolved: false, |
|
408 get url() { return this._hooks.url; }, |
|
409 |
|
410 /** |
|
411 * Enter this nested event loop. |
|
412 */ |
|
413 enter: function () { |
|
414 let nestData = this._hooks.preNest |
|
415 ? this._hooks.preNest() |
|
416 : null; |
|
417 |
|
418 this.entered = true; |
|
419 this._inspector.enterNestedEventLoop(this); |
|
420 |
|
421 // Keep exiting nested event loops while the last requestor is resolved. |
|
422 if (this._inspector.eventLoopNestLevel > 0) { |
|
423 const { resolved } = this._inspector.lastNestRequestor; |
|
424 if (resolved) { |
|
425 this._inspector.exitNestedEventLoop(); |
|
426 } |
|
427 } |
|
428 |
|
429 dbg_assert(this._thread.state === "running", |
|
430 "Should be in the running state"); |
|
431 |
|
432 if (this._hooks.postNest) { |
|
433 this._hooks.postNest(nestData); |
|
434 } |
|
435 }, |
|
436 |
|
437 /** |
|
438 * Resolve this nested event loop. |
|
439 * |
|
440 * @returns boolean |
|
441 * True if we exited this nested event loop because it was on top of |
|
442 * the stack, false if there is another nested event loop above this |
|
443 * one that hasn't resolved yet. |
|
444 */ |
|
445 resolve: function () { |
|
446 if (!this.entered) { |
|
447 throw new Error("Can't resolve an event loop before it has been entered!"); |
|
448 } |
|
449 if (this.resolved) { |
|
450 throw new Error("Already resolved this nested event loop!"); |
|
451 } |
|
452 this.resolved = true; |
|
453 if (this === this._inspector.lastNestRequestor) { |
|
454 this._inspector.exitNestedEventLoop(); |
|
455 return true; |
|
456 } |
|
457 return false; |
|
458 }, |
|
459 }; |
|
460 |
|
461 /** |
|
462 * JSD2 actors. |
|
463 */ |
|
464 /** |
|
465 * Creates a ThreadActor. |
|
466 * |
|
467 * ThreadActors manage a JSInspector object and manage execution/inspection |
|
468 * of debuggees. |
|
469 * |
|
470 * @param aHooks object |
|
471 * An object with preNest and postNest methods for calling when entering |
|
472 * and exiting a nested event loop. |
|
473 * @param aGlobal object [optional] |
|
474 * An optional (for content debugging only) reference to the content |
|
475 * window. |
|
476 */ |
|
477 function ThreadActor(aHooks, aGlobal) |
|
478 { |
|
479 this._state = "detached"; |
|
480 this._frameActors = []; |
|
481 this._hooks = aHooks; |
|
482 this.global = aGlobal; |
|
483 // A map of actorID -> actor for breakpoints created and managed by the server. |
|
484 this._hiddenBreakpoints = new Map(); |
|
485 |
|
486 this.findGlobals = this.globalManager.findGlobals.bind(this); |
|
487 this.onNewGlobal = this.globalManager.onNewGlobal.bind(this); |
|
488 this.onNewSource = this.onNewSource.bind(this); |
|
489 this._allEventsListener = this._allEventsListener.bind(this); |
|
490 |
|
491 this._options = { |
|
492 useSourceMaps: false |
|
493 }; |
|
494 |
|
495 this._gripDepth = 0; |
|
496 } |
|
497 |
|
498 /** |
|
499 * The breakpoint store must be shared across instances of ThreadActor so that |
|
500 * page reloads don't blow away all of our breakpoints. |
|
501 */ |
|
502 ThreadActor.breakpointStore = new BreakpointStore(); |
|
503 |
|
504 ThreadActor.prototype = { |
|
505 // Used by the ObjectActor to keep track of the depth of grip() calls. |
|
506 _gripDepth: null, |
|
507 |
|
508 actorPrefix: "context", |
|
509 |
|
510 get state() { return this._state; }, |
|
511 get attached() this.state == "attached" || |
|
512 this.state == "running" || |
|
513 this.state == "paused", |
|
514 |
|
515 get breakpointStore() { return ThreadActor.breakpointStore; }, |
|
516 |
|
517 get threadLifetimePool() { |
|
518 if (!this._threadLifetimePool) { |
|
519 this._threadLifetimePool = new ActorPool(this.conn); |
|
520 this.conn.addActorPool(this._threadLifetimePool); |
|
521 this._threadLifetimePool.objectActors = new WeakMap(); |
|
522 } |
|
523 return this._threadLifetimePool; |
|
524 }, |
|
525 |
|
526 get sources() { |
|
527 if (!this._sources) { |
|
528 this._sources = new ThreadSources(this, this._options.useSourceMaps, |
|
529 this._allowSource, this.onNewSource); |
|
530 } |
|
531 return this._sources; |
|
532 }, |
|
533 |
|
534 get youngestFrame() { |
|
535 if (this.state != "paused") { |
|
536 return null; |
|
537 } |
|
538 return this.dbg.getNewestFrame(); |
|
539 }, |
|
540 |
|
541 _prettyPrintWorker: null, |
|
542 get prettyPrintWorker() { |
|
543 if (!this._prettyPrintWorker) { |
|
544 this._prettyPrintWorker = new ChromeWorker( |
|
545 "resource://gre/modules/devtools/server/actors/pretty-print-worker.js"); |
|
546 |
|
547 this._prettyPrintWorker.addEventListener( |
|
548 "error", this._onPrettyPrintError, false); |
|
549 |
|
550 if (dumpn.wantLogging) { |
|
551 this._prettyPrintWorker.addEventListener("message", this._onPrettyPrintMsg, false); |
|
552 |
|
553 const postMsg = this._prettyPrintWorker.postMessage; |
|
554 this._prettyPrintWorker.postMessage = data => { |
|
555 dumpn("Sending message to prettyPrintWorker: " |
|
556 + JSON.stringify(data, null, 2) + "\n"); |
|
557 return postMsg.call(this._prettyPrintWorker, data); |
|
558 }; |
|
559 } |
|
560 } |
|
561 return this._prettyPrintWorker; |
|
562 }, |
|
563 |
|
564 _onPrettyPrintError: function ({ message, filename, lineno }) { |
|
565 reportError(new Error(message + " @ " + filename + ":" + lineno)); |
|
566 }, |
|
567 |
|
568 _onPrettyPrintMsg: function ({ data }) { |
|
569 dumpn("Received message from prettyPrintWorker: " |
|
570 + JSON.stringify(data, null, 2) + "\n"); |
|
571 }, |
|
572 |
|
573 /** |
|
574 * Keep track of all of the nested event loops we use to pause the debuggee |
|
575 * when we hit a breakpoint/debugger statement/etc in one place so we can |
|
576 * resolve them when we get resume packets. We have more than one (and keep |
|
577 * them in a stack) because we can pause within client evals. |
|
578 */ |
|
579 _threadPauseEventLoops: null, |
|
580 _pushThreadPause: function () { |
|
581 if (!this._threadPauseEventLoops) { |
|
582 this._threadPauseEventLoops = []; |
|
583 } |
|
584 const eventLoop = this._nestedEventLoops.push(); |
|
585 this._threadPauseEventLoops.push(eventLoop); |
|
586 eventLoop.enter(); |
|
587 }, |
|
588 _popThreadPause: function () { |
|
589 const eventLoop = this._threadPauseEventLoops.pop(); |
|
590 dbg_assert(eventLoop, "Should have an event loop."); |
|
591 eventLoop.resolve(); |
|
592 }, |
|
593 |
|
594 /** |
|
595 * Remove all debuggees and clear out the thread's sources. |
|
596 */ |
|
597 clearDebuggees: function () { |
|
598 if (this.dbg) { |
|
599 this.dbg.removeAllDebuggees(); |
|
600 } |
|
601 this._sources = null; |
|
602 }, |
|
603 |
|
604 /** |
|
605 * Add a debuggee global to the Debugger object. |
|
606 * |
|
607 * @returns the Debugger.Object that corresponds to the global. |
|
608 */ |
|
609 addDebuggee: function (aGlobal) { |
|
610 let globalDebugObject; |
|
611 try { |
|
612 globalDebugObject = this.dbg.addDebuggee(aGlobal); |
|
613 } catch (e) { |
|
614 // Ignore attempts to add the debugger's compartment as a debuggee. |
|
615 dumpn("Ignoring request to add the debugger's compartment as a debuggee"); |
|
616 } |
|
617 return globalDebugObject; |
|
618 }, |
|
619 |
|
620 /** |
|
621 * Initialize the Debugger. |
|
622 */ |
|
623 _initDebugger: function () { |
|
624 this.dbg = new Debugger(); |
|
625 this.dbg.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this); |
|
626 this.dbg.onDebuggerStatement = this.onDebuggerStatement.bind(this); |
|
627 this.dbg.onNewScript = this.onNewScript.bind(this); |
|
628 this.dbg.onNewGlobalObject = this.globalManager.onNewGlobal.bind(this); |
|
629 // Keep the debugger disabled until a client attaches. |
|
630 this.dbg.enabled = this._state != "detached"; |
|
631 }, |
|
632 |
|
633 /** |
|
634 * Remove a debuggee global from the JSInspector. |
|
635 */ |
|
636 removeDebugee: function (aGlobal) { |
|
637 try { |
|
638 this.dbg.removeDebuggee(aGlobal); |
|
639 } catch(ex) { |
|
640 // XXX: This debuggee has code currently executing on the stack, |
|
641 // we need to save this for later. |
|
642 } |
|
643 }, |
|
644 |
|
645 /** |
|
646 * Add the provided window and all windows in its frame tree as debuggees. |
|
647 * |
|
648 * @returns the Debugger.Object that corresponds to the window. |
|
649 */ |
|
650 _addDebuggees: function (aWindow) { |
|
651 let globalDebugObject = this.addDebuggee(aWindow); |
|
652 let frames = aWindow.frames; |
|
653 if (frames) { |
|
654 for (let i = 0; i < frames.length; i++) { |
|
655 this._addDebuggees(frames[i]); |
|
656 } |
|
657 } |
|
658 return globalDebugObject; |
|
659 }, |
|
660 |
|
661 /** |
|
662 * An object that will be used by ThreadActors to tailor their behavior |
|
663 * depending on the debugging context being required (chrome or content). |
|
664 */ |
|
665 globalManager: { |
|
666 findGlobals: function () { |
|
667 const { gDevToolsExtensions: { |
|
668 getContentGlobals |
|
669 } } = Cu.import("resource://gre/modules/devtools/DevToolsExtensions.jsm", {}); |
|
670 |
|
671 this.globalDebugObject = this._addDebuggees(this.global); |
|
672 |
|
673 // global may not be a window |
|
674 try { |
|
675 getContentGlobals({ |
|
676 'inner-window-id': getInnerId(this.global) |
|
677 }).forEach(this.addDebuggee.bind(this)); |
|
678 } |
|
679 catch(e) {} |
|
680 }, |
|
681 |
|
682 /** |
|
683 * A function that the engine calls when a new global object |
|
684 * (for example a sandbox) has been created. |
|
685 * |
|
686 * @param aGlobal Debugger.Object |
|
687 * The new global object that was created. |
|
688 */ |
|
689 onNewGlobal: function (aGlobal) { |
|
690 let useGlobal = (aGlobal.hostAnnotations && |
|
691 aGlobal.hostAnnotations.type == "document" && |
|
692 aGlobal.hostAnnotations.element === this.global); |
|
693 |
|
694 // check if the global is a sdk page-mod sandbox |
|
695 if (!useGlobal) { |
|
696 let metadata = {}; |
|
697 let id = ""; |
|
698 try { |
|
699 id = getInnerId(this.global); |
|
700 metadata = Cu.getSandboxMetadata(aGlobal.unsafeDereference()); |
|
701 } |
|
702 catch (e) {} |
|
703 |
|
704 useGlobal = (metadata['inner-window-id'] && metadata['inner-window-id'] == id); |
|
705 } |
|
706 |
|
707 // Content debugging only cares about new globals in the contant window, |
|
708 // like iframe children. |
|
709 if (useGlobal) { |
|
710 this.addDebuggee(aGlobal); |
|
711 // Notify the client. |
|
712 this.conn.send({ |
|
713 from: this.actorID, |
|
714 type: "newGlobal", |
|
715 // TODO: after bug 801084 lands see if we need to JSONify this. |
|
716 hostAnnotations: aGlobal.hostAnnotations |
|
717 }); |
|
718 } |
|
719 } |
|
720 }, |
|
721 |
|
722 disconnect: function () { |
|
723 dumpn("in ThreadActor.prototype.disconnect"); |
|
724 if (this._state == "paused") { |
|
725 this.onResume(); |
|
726 } |
|
727 |
|
728 this.clearDebuggees(); |
|
729 this.conn.removeActorPool(this._threadLifetimePool); |
|
730 this._threadLifetimePool = null; |
|
731 |
|
732 if (this._prettyPrintWorker) { |
|
733 this._prettyPrintWorker.removeEventListener( |
|
734 "error", this._onPrettyPrintError, false); |
|
735 this._prettyPrintWorker.removeEventListener( |
|
736 "message", this._onPrettyPrintMsg, false); |
|
737 this._prettyPrintWorker.terminate(); |
|
738 this._prettyPrintWorker = null; |
|
739 } |
|
740 |
|
741 if (!this.dbg) { |
|
742 return; |
|
743 } |
|
744 this.dbg.enabled = false; |
|
745 this.dbg = null; |
|
746 }, |
|
747 |
|
748 /** |
|
749 * Disconnect the debugger and put the actor in the exited state. |
|
750 */ |
|
751 exit: function () { |
|
752 this.disconnect(); |
|
753 this._state = "exited"; |
|
754 }, |
|
755 |
|
756 // Request handlers |
|
757 onAttach: function (aRequest) { |
|
758 if (this.state === "exited") { |
|
759 return { type: "exited" }; |
|
760 } |
|
761 |
|
762 if (this.state !== "detached") { |
|
763 return { error: "wrongState", |
|
764 message: "Current state is " + this.state }; |
|
765 } |
|
766 |
|
767 this._state = "attached"; |
|
768 |
|
769 update(this._options, aRequest.options || {}); |
|
770 |
|
771 // Initialize an event loop stack. This can't be done in the constructor, |
|
772 // because this.conn is not yet initialized by the actor pool at that time. |
|
773 this._nestedEventLoops = new EventLoopStack({ |
|
774 inspector: DebuggerServer.xpcInspector, |
|
775 hooks: this._hooks, |
|
776 connection: this.conn, |
|
777 thread: this |
|
778 }); |
|
779 |
|
780 if (!this.dbg) { |
|
781 this._initDebugger(); |
|
782 } |
|
783 this.findGlobals(); |
|
784 this.dbg.enabled = true; |
|
785 try { |
|
786 // Put ourselves in the paused state. |
|
787 let packet = this._paused(); |
|
788 if (!packet) { |
|
789 return { error: "notAttached" }; |
|
790 } |
|
791 packet.why = { type: "attached" }; |
|
792 |
|
793 this._restoreBreakpoints(); |
|
794 |
|
795 // Send the response to the attach request now (rather than |
|
796 // returning it), because we're going to start a nested event loop |
|
797 // here. |
|
798 this.conn.send(packet); |
|
799 |
|
800 // Start a nested event loop. |
|
801 this._pushThreadPause(); |
|
802 |
|
803 // We already sent a response to this request, don't send one |
|
804 // now. |
|
805 return null; |
|
806 } catch (e) { |
|
807 reportError(e); |
|
808 return { error: "notAttached", message: e.toString() }; |
|
809 } |
|
810 }, |
|
811 |
|
812 onDetach: function (aRequest) { |
|
813 this.disconnect(); |
|
814 this._state = "detached"; |
|
815 |
|
816 dumpn("ThreadActor.prototype.onDetach: returning 'detached' packet"); |
|
817 return { |
|
818 type: "detached" |
|
819 }; |
|
820 }, |
|
821 |
|
822 onReconfigure: function (aRequest) { |
|
823 if (this.state == "exited") { |
|
824 return { error: "wrongState" }; |
|
825 } |
|
826 |
|
827 update(this._options, aRequest.options || {}); |
|
828 // Clear existing sources, so they can be recreated on next access. |
|
829 this._sources = null; |
|
830 |
|
831 return {}; |
|
832 }, |
|
833 |
|
834 /** |
|
835 * Pause the debuggee, by entering a nested event loop, and return a 'paused' |
|
836 * packet to the client. |
|
837 * |
|
838 * @param Debugger.Frame aFrame |
|
839 * The newest debuggee frame in the stack. |
|
840 * @param object aReason |
|
841 * An object with a 'type' property containing the reason for the pause. |
|
842 * @param function onPacket |
|
843 * Hook to modify the packet before it is sent. Feel free to return a |
|
844 * promise. |
|
845 */ |
|
846 _pauseAndRespond: function (aFrame, aReason, onPacket=function (k) { return k; }) { |
|
847 try { |
|
848 let packet = this._paused(aFrame); |
|
849 if (!packet) { |
|
850 return undefined; |
|
851 } |
|
852 packet.why = aReason; |
|
853 |
|
854 this.sources.getOriginalLocation(packet.frame.where).then(aOrigPosition => { |
|
855 packet.frame.where = aOrigPosition; |
|
856 resolve(onPacket(packet)) |
|
857 .then(null, error => { |
|
858 reportError(error); |
|
859 return { |
|
860 error: "unknownError", |
|
861 message: error.message + "\n" + error.stack |
|
862 }; |
|
863 }) |
|
864 .then(packet => { |
|
865 this.conn.send(packet); |
|
866 }); |
|
867 }); |
|
868 |
|
869 this._pushThreadPause(); |
|
870 } catch(e) { |
|
871 reportError(e, "Got an exception during TA__pauseAndRespond: "); |
|
872 } |
|
873 |
|
874 return undefined; |
|
875 }, |
|
876 |
|
877 /** |
|
878 * Handle resume requests that include a forceCompletion request. |
|
879 * |
|
880 * @param Object aRequest |
|
881 * The request packet received over the RDP. |
|
882 * @returns A response packet. |
|
883 */ |
|
884 _forceCompletion: function (aRequest) { |
|
885 // TODO: remove this when Debugger.Frame.prototype.pop is implemented in |
|
886 // bug 736733. |
|
887 return { |
|
888 error: "notImplemented", |
|
889 message: "forced completion is not yet implemented." |
|
890 }; |
|
891 }, |
|
892 |
|
893 _makeOnEnterFrame: function ({ pauseAndRespond }) { |
|
894 return aFrame => { |
|
895 const generatedLocation = getFrameLocation(aFrame); |
|
896 let { url } = this.synchronize(this.sources.getOriginalLocation( |
|
897 generatedLocation)); |
|
898 |
|
899 return this.sources.isBlackBoxed(url) |
|
900 ? undefined |
|
901 : pauseAndRespond(aFrame); |
|
902 }; |
|
903 }, |
|
904 |
|
905 _makeOnPop: function ({ thread, pauseAndRespond, createValueGrip }) { |
|
906 return function (aCompletion) { |
|
907 // onPop is called with 'this' set to the current frame. |
|
908 |
|
909 const generatedLocation = getFrameLocation(this); |
|
910 const { url } = thread.synchronize(thread.sources.getOriginalLocation( |
|
911 generatedLocation)); |
|
912 |
|
913 if (thread.sources.isBlackBoxed(url)) { |
|
914 return undefined; |
|
915 } |
|
916 |
|
917 // Note that we're popping this frame; we need to watch for |
|
918 // subsequent step events on its caller. |
|
919 this.reportedPop = true; |
|
920 |
|
921 return pauseAndRespond(this, aPacket => { |
|
922 aPacket.why.frameFinished = {}; |
|
923 if (!aCompletion) { |
|
924 aPacket.why.frameFinished.terminated = true; |
|
925 } else if (aCompletion.hasOwnProperty("return")) { |
|
926 aPacket.why.frameFinished.return = createValueGrip(aCompletion.return); |
|
927 } else if (aCompletion.hasOwnProperty("yield")) { |
|
928 aPacket.why.frameFinished.return = createValueGrip(aCompletion.yield); |
|
929 } else { |
|
930 aPacket.why.frameFinished.throw = createValueGrip(aCompletion.throw); |
|
931 } |
|
932 return aPacket; |
|
933 }); |
|
934 }; |
|
935 }, |
|
936 |
|
937 _makeOnStep: function ({ thread, pauseAndRespond, startFrame, |
|
938 startLocation }) { |
|
939 return function () { |
|
940 // onStep is called with 'this' set to the current frame. |
|
941 |
|
942 const generatedLocation = getFrameLocation(this); |
|
943 const newLocation = thread.synchronize(thread.sources.getOriginalLocation( |
|
944 generatedLocation)); |
|
945 |
|
946 // Cases when we should pause because we have executed enough to consider |
|
947 // a "step" to have occured: |
|
948 // |
|
949 // 1.1. We change frames. |
|
950 // 1.2. We change URLs (can happen without changing frames thanks to |
|
951 // source mapping). |
|
952 // 1.3. We change lines. |
|
953 // |
|
954 // Cases when we should always continue execution, even if one of the |
|
955 // above cases is true: |
|
956 // |
|
957 // 2.1. We are in a source mapped region, but inside a null mapping |
|
958 // (doesn't correlate to any region of original source) |
|
959 // 2.2. The source we are in is black boxed. |
|
960 |
|
961 // Cases 2.1 and 2.2 |
|
962 if (newLocation.url == null |
|
963 || thread.sources.isBlackBoxed(newLocation.url)) { |
|
964 return undefined; |
|
965 } |
|
966 |
|
967 // Cases 1.1, 1.2 and 1.3 |
|
968 if (this !== startFrame |
|
969 || startLocation.url !== newLocation.url |
|
970 || startLocation.line !== newLocation.line) { |
|
971 return pauseAndRespond(this); |
|
972 } |
|
973 |
|
974 // Otherwise, let execution continue (we haven't executed enough code to |
|
975 // consider this a "step" yet). |
|
976 return undefined; |
|
977 }; |
|
978 }, |
|
979 |
|
980 /** |
|
981 * Define the JS hook functions for stepping. |
|
982 */ |
|
983 _makeSteppingHooks: function (aStartLocation) { |
|
984 // Bind these methods and state because some of the hooks are called |
|
985 // with 'this' set to the current frame. Rather than repeating the |
|
986 // binding in each _makeOnX method, just do it once here and pass it |
|
987 // in to each function. |
|
988 const steppingHookState = { |
|
989 pauseAndRespond: (aFrame, onPacket=(k)=>k) => { |
|
990 this._pauseAndRespond(aFrame, { type: "resumeLimit" }, onPacket); |
|
991 }, |
|
992 createValueGrip: this.createValueGrip.bind(this), |
|
993 thread: this, |
|
994 startFrame: this.youngestFrame, |
|
995 startLocation: aStartLocation |
|
996 }; |
|
997 |
|
998 return { |
|
999 onEnterFrame: this._makeOnEnterFrame(steppingHookState), |
|
1000 onPop: this._makeOnPop(steppingHookState), |
|
1001 onStep: this._makeOnStep(steppingHookState) |
|
1002 }; |
|
1003 }, |
|
1004 |
|
1005 /** |
|
1006 * Handle attaching the various stepping hooks we need to attach when we |
|
1007 * receive a resume request with a resumeLimit property. |
|
1008 * |
|
1009 * @param Object aRequest |
|
1010 * The request packet received over the RDP. |
|
1011 * @returns A promise that resolves to true once the hooks are attached, or is |
|
1012 * rejected with an error packet. |
|
1013 */ |
|
1014 _handleResumeLimit: function (aRequest) { |
|
1015 let steppingType = aRequest.resumeLimit.type; |
|
1016 if (["step", "next", "finish"].indexOf(steppingType) == -1) { |
|
1017 return reject({ error: "badParameterType", |
|
1018 message: "Unknown resumeLimit type" }); |
|
1019 } |
|
1020 |
|
1021 const generatedLocation = getFrameLocation(this.youngestFrame); |
|
1022 return this.sources.getOriginalLocation(generatedLocation) |
|
1023 .then(originalLocation => { |
|
1024 const { onEnterFrame, onPop, onStep } = this._makeSteppingHooks(originalLocation); |
|
1025 |
|
1026 // Make sure there is still a frame on the stack if we are to continue |
|
1027 // stepping. |
|
1028 let stepFrame = this._getNextStepFrame(this.youngestFrame); |
|
1029 if (stepFrame) { |
|
1030 switch (steppingType) { |
|
1031 case "step": |
|
1032 this.dbg.onEnterFrame = onEnterFrame; |
|
1033 // Fall through. |
|
1034 case "next": |
|
1035 if (stepFrame.script) { |
|
1036 stepFrame.onStep = onStep; |
|
1037 } |
|
1038 stepFrame.onPop = onPop; |
|
1039 break; |
|
1040 case "finish": |
|
1041 stepFrame.onPop = onPop; |
|
1042 } |
|
1043 } |
|
1044 |
|
1045 return true; |
|
1046 }); |
|
1047 }, |
|
1048 |
|
1049 /** |
|
1050 * Clear the onStep and onPop hooks from the given frame and all of the frames |
|
1051 * below it. |
|
1052 * |
|
1053 * @param Debugger.Frame aFrame |
|
1054 * The frame we want to clear the stepping hooks from. |
|
1055 */ |
|
1056 _clearSteppingHooks: function (aFrame) { |
|
1057 while (aFrame) { |
|
1058 aFrame.onStep = undefined; |
|
1059 aFrame.onPop = undefined; |
|
1060 aFrame = aFrame.older; |
|
1061 } |
|
1062 }, |
|
1063 |
|
1064 /** |
|
1065 * Listen to the debuggee's DOM events if we received a request to do so. |
|
1066 * |
|
1067 * @param Object aRequest |
|
1068 * The resume request packet received over the RDP. |
|
1069 */ |
|
1070 _maybeListenToEvents: function (aRequest) { |
|
1071 // Break-on-DOMEvents is only supported in content debugging. |
|
1072 let events = aRequest.pauseOnDOMEvents; |
|
1073 if (this.global && events && |
|
1074 (events == "*" || |
|
1075 (Array.isArray(events) && events.length))) { |
|
1076 this._pauseOnDOMEvents = events; |
|
1077 let els = Cc["@mozilla.org/eventlistenerservice;1"] |
|
1078 .getService(Ci.nsIEventListenerService); |
|
1079 els.addListenerForAllEvents(this.global, this._allEventsListener, true); |
|
1080 } |
|
1081 }, |
|
1082 |
|
1083 /** |
|
1084 * Handle a protocol request to resume execution of the debuggee. |
|
1085 */ |
|
1086 onResume: function (aRequest) { |
|
1087 if (this._state !== "paused") { |
|
1088 return { |
|
1089 error: "wrongState", |
|
1090 message: "Can't resume when debuggee isn't paused. Current state is '" |
|
1091 + this._state + "'" |
|
1092 }; |
|
1093 } |
|
1094 |
|
1095 // In case of multiple nested event loops (due to multiple debuggers open in |
|
1096 // different tabs or multiple debugger clients connected to the same tab) |
|
1097 // only allow resumption in a LIFO order. |
|
1098 if (this._nestedEventLoops.size && this._nestedEventLoops.lastPausedUrl |
|
1099 && (this._nestedEventLoops.lastPausedUrl !== this._hooks.url |
|
1100 || this._nestedEventLoops.lastConnection !== this.conn)) { |
|
1101 return { |
|
1102 error: "wrongOrder", |
|
1103 message: "trying to resume in the wrong order.", |
|
1104 lastPausedUrl: this._nestedEventLoops.lastPausedUrl |
|
1105 }; |
|
1106 } |
|
1107 |
|
1108 if (aRequest && aRequest.forceCompletion) { |
|
1109 return this._forceCompletion(aRequest); |
|
1110 } |
|
1111 |
|
1112 let resumeLimitHandled; |
|
1113 if (aRequest && aRequest.resumeLimit) { |
|
1114 resumeLimitHandled = this._handleResumeLimit(aRequest) |
|
1115 } else { |
|
1116 this._clearSteppingHooks(this.youngestFrame); |
|
1117 resumeLimitHandled = resolve(true); |
|
1118 } |
|
1119 |
|
1120 return resumeLimitHandled.then(() => { |
|
1121 if (aRequest) { |
|
1122 this._options.pauseOnExceptions = aRequest.pauseOnExceptions; |
|
1123 this._options.ignoreCaughtExceptions = aRequest.ignoreCaughtExceptions; |
|
1124 this.maybePauseOnExceptions(); |
|
1125 this._maybeListenToEvents(aRequest); |
|
1126 } |
|
1127 |
|
1128 let packet = this._resumed(); |
|
1129 this._popThreadPause(); |
|
1130 return packet; |
|
1131 }, error => { |
|
1132 return error instanceof Error |
|
1133 ? { error: "unknownError", |
|
1134 message: DevToolsUtils.safeErrorString(error) } |
|
1135 // It is a known error, and the promise was rejected with an error |
|
1136 // packet. |
|
1137 : error; |
|
1138 }); |
|
1139 }, |
|
1140 |
|
1141 /** |
|
1142 * Spin up a nested event loop so we can synchronously resolve a promise. |
|
1143 * |
|
1144 * @param aPromise |
|
1145 * The promise we want to resolve. |
|
1146 * @returns The promise's resolution. |
|
1147 */ |
|
1148 synchronize: function(aPromise) { |
|
1149 let needNest = true; |
|
1150 let eventLoop; |
|
1151 let returnVal; |
|
1152 |
|
1153 aPromise |
|
1154 .then((aResolvedVal) => { |
|
1155 needNest = false; |
|
1156 returnVal = aResolvedVal; |
|
1157 }) |
|
1158 .then(null, (aError) => { |
|
1159 reportError(aError, "Error inside synchronize:"); |
|
1160 }) |
|
1161 .then(() => { |
|
1162 if (eventLoop) { |
|
1163 eventLoop.resolve(); |
|
1164 } |
|
1165 }); |
|
1166 |
|
1167 if (needNest) { |
|
1168 eventLoop = this._nestedEventLoops.push(); |
|
1169 eventLoop.enter(); |
|
1170 } |
|
1171 |
|
1172 return returnVal; |
|
1173 }, |
|
1174 |
|
1175 /** |
|
1176 * Set the debugging hook to pause on exceptions if configured to do so. |
|
1177 */ |
|
1178 maybePauseOnExceptions: function() { |
|
1179 if (this._options.pauseOnExceptions) { |
|
1180 this.dbg.onExceptionUnwind = this.onExceptionUnwind.bind(this); |
|
1181 } |
|
1182 }, |
|
1183 |
|
1184 /** |
|
1185 * A listener that gets called for every event fired on the page, when a list |
|
1186 * of interesting events was provided with the pauseOnDOMEvents property. It |
|
1187 * is used to set server-managed breakpoints on any existing event listeners |
|
1188 * for those events. |
|
1189 * |
|
1190 * @param Event event |
|
1191 * The event that was fired. |
|
1192 */ |
|
1193 _allEventsListener: function(event) { |
|
1194 if (this._pauseOnDOMEvents == "*" || |
|
1195 this._pauseOnDOMEvents.indexOf(event.type) != -1) { |
|
1196 for (let listener of this._getAllEventListeners(event.target)) { |
|
1197 if (event.type == listener.type || this._pauseOnDOMEvents == "*") { |
|
1198 this._breakOnEnter(listener.script); |
|
1199 } |
|
1200 } |
|
1201 } |
|
1202 }, |
|
1203 |
|
1204 /** |
|
1205 * Return an array containing all the event listeners attached to the |
|
1206 * specified event target and its ancestors in the event target chain. |
|
1207 * |
|
1208 * @param EventTarget eventTarget |
|
1209 * The target the event was dispatched on. |
|
1210 * @returns Array |
|
1211 */ |
|
1212 _getAllEventListeners: function(eventTarget) { |
|
1213 let els = Cc["@mozilla.org/eventlistenerservice;1"] |
|
1214 .getService(Ci.nsIEventListenerService); |
|
1215 |
|
1216 let targets = els.getEventTargetChainFor(eventTarget); |
|
1217 let listeners = []; |
|
1218 |
|
1219 for (let target of targets) { |
|
1220 let handlers = els.getListenerInfoFor(target); |
|
1221 for (let handler of handlers) { |
|
1222 // Null is returned for all-events handlers, and native event listeners |
|
1223 // don't provide any listenerObject, which makes them not that useful to |
|
1224 // a JS debugger. |
|
1225 if (!handler || !handler.listenerObject || !handler.type) |
|
1226 continue; |
|
1227 // Create a listener-like object suitable for our purposes. |
|
1228 let l = Object.create(null); |
|
1229 l.type = handler.type; |
|
1230 let listener = handler.listenerObject; |
|
1231 l.script = this.globalDebugObject.makeDebuggeeValue(listener).script; |
|
1232 // Chrome listeners won't be converted to debuggee values, since their |
|
1233 // compartment is not added as a debuggee. |
|
1234 if (!l.script) |
|
1235 continue; |
|
1236 listeners.push(l); |
|
1237 } |
|
1238 } |
|
1239 return listeners; |
|
1240 }, |
|
1241 |
|
1242 /** |
|
1243 * Set a breakpoint on the first bytecode offset in the provided script. |
|
1244 */ |
|
1245 _breakOnEnter: function(script) { |
|
1246 let offsets = script.getAllOffsets(); |
|
1247 for (let line = 0, n = offsets.length; line < n; line++) { |
|
1248 if (offsets[line]) { |
|
1249 let location = { url: script.url, line: line }; |
|
1250 let resp = this._createAndStoreBreakpoint(location); |
|
1251 dbg_assert(!resp.actualLocation, "No actualLocation should be returned"); |
|
1252 if (resp.error) { |
|
1253 reportError(new Error("Unable to set breakpoint on event listener")); |
|
1254 return; |
|
1255 } |
|
1256 let bp = this.breakpointStore.getBreakpoint(location); |
|
1257 let bpActor = bp.actor; |
|
1258 dbg_assert(bp, "Breakpoint must exist"); |
|
1259 dbg_assert(bpActor, "Breakpoint actor must be created"); |
|
1260 this._hiddenBreakpoints.set(bpActor.actorID, bpActor); |
|
1261 break; |
|
1262 } |
|
1263 } |
|
1264 }, |
|
1265 |
|
1266 /** |
|
1267 * Helper method that returns the next frame when stepping. |
|
1268 */ |
|
1269 _getNextStepFrame: function (aFrame) { |
|
1270 let stepFrame = aFrame.reportedPop ? aFrame.older : aFrame; |
|
1271 if (!stepFrame || !stepFrame.script) { |
|
1272 stepFrame = null; |
|
1273 } |
|
1274 return stepFrame; |
|
1275 }, |
|
1276 |
|
1277 onClientEvaluate: function (aRequest) { |
|
1278 if (this.state !== "paused") { |
|
1279 return { error: "wrongState", |
|
1280 message: "Debuggee must be paused to evaluate code." }; |
|
1281 } |
|
1282 |
|
1283 let frame = this._requestFrame(aRequest.frame); |
|
1284 if (!frame) { |
|
1285 return { error: "unknownFrame", |
|
1286 message: "Evaluation frame not found" }; |
|
1287 } |
|
1288 |
|
1289 if (!frame.environment) { |
|
1290 return { error: "notDebuggee", |
|
1291 message: "cannot access the environment of this frame." }; |
|
1292 } |
|
1293 |
|
1294 let youngest = this.youngestFrame; |
|
1295 |
|
1296 // Put ourselves back in the running state and inform the client. |
|
1297 let resumedPacket = this._resumed(); |
|
1298 this.conn.send(resumedPacket); |
|
1299 |
|
1300 // Run the expression. |
|
1301 // XXX: test syntax errors |
|
1302 let completion = frame.eval(aRequest.expression); |
|
1303 |
|
1304 // Put ourselves back in the pause state. |
|
1305 let packet = this._paused(youngest); |
|
1306 packet.why = { type: "clientEvaluated", |
|
1307 frameFinished: this.createProtocolCompletionValue(completion) }; |
|
1308 |
|
1309 // Return back to our previous pause's event loop. |
|
1310 return packet; |
|
1311 }, |
|
1312 |
|
1313 onFrames: function (aRequest) { |
|
1314 if (this.state !== "paused") { |
|
1315 return { error: "wrongState", |
|
1316 message: "Stack frames are only available while the debuggee is paused."}; |
|
1317 } |
|
1318 |
|
1319 let start = aRequest.start ? aRequest.start : 0; |
|
1320 let count = aRequest.count; |
|
1321 |
|
1322 // Find the starting frame... |
|
1323 let frame = this.youngestFrame; |
|
1324 let i = 0; |
|
1325 while (frame && (i < start)) { |
|
1326 frame = frame.older; |
|
1327 i++; |
|
1328 } |
|
1329 |
|
1330 // Return request.count frames, or all remaining |
|
1331 // frames if count is not defined. |
|
1332 let frames = []; |
|
1333 let promises = []; |
|
1334 for (; frame && (!count || i < (start + count)); i++, frame=frame.older) { |
|
1335 let form = this._createFrameActor(frame).form(); |
|
1336 form.depth = i; |
|
1337 frames.push(form); |
|
1338 |
|
1339 let promise = this.sources.getOriginalLocation(form.where) |
|
1340 .then((aOrigLocation) => { |
|
1341 form.where = aOrigLocation; |
|
1342 let source = this.sources.source({ url: form.where.url }); |
|
1343 if (source) { |
|
1344 form.source = source.form(); |
|
1345 } |
|
1346 }); |
|
1347 promises.push(promise); |
|
1348 } |
|
1349 |
|
1350 return all(promises).then(function () { |
|
1351 return { frames: frames }; |
|
1352 }); |
|
1353 }, |
|
1354 |
|
1355 onReleaseMany: function (aRequest) { |
|
1356 if (!aRequest.actors) { |
|
1357 return { error: "missingParameter", |
|
1358 message: "no actors were specified" }; |
|
1359 } |
|
1360 |
|
1361 let res; |
|
1362 for each (let actorID in aRequest.actors) { |
|
1363 let actor = this.threadLifetimePool.get(actorID); |
|
1364 if (!actor) { |
|
1365 if (!res) { |
|
1366 res = { error: "notReleasable", |
|
1367 message: "Only thread-lifetime actors can be released." }; |
|
1368 } |
|
1369 continue; |
|
1370 } |
|
1371 actor.onRelease(); |
|
1372 } |
|
1373 return res ? res : {}; |
|
1374 }, |
|
1375 |
|
1376 /** |
|
1377 * Handle a protocol request to set a breakpoint. |
|
1378 */ |
|
1379 onSetBreakpoint: function (aRequest) { |
|
1380 if (this.state !== "paused") { |
|
1381 return { error: "wrongState", |
|
1382 message: "Breakpoints can only be set while the debuggee is paused."}; |
|
1383 } |
|
1384 |
|
1385 let { url: originalSource, |
|
1386 line: originalLine, |
|
1387 column: originalColumn } = aRequest.location; |
|
1388 |
|
1389 let locationPromise = this.sources.getGeneratedLocation(aRequest.location); |
|
1390 return locationPromise.then(({url, line, column}) => { |
|
1391 if (line == null || |
|
1392 line < 0 || |
|
1393 this.dbg.findScripts({ url: url }).length == 0) { |
|
1394 return { |
|
1395 error: "noScript", |
|
1396 message: "Requested setting a breakpoint on " |
|
1397 + url + ":" + line |
|
1398 + (column != null ? ":" + column : "") |
|
1399 + " but there is no Debugger.Script at that location" |
|
1400 }; |
|
1401 } |
|
1402 |
|
1403 let response = this._createAndStoreBreakpoint({ |
|
1404 url: url, |
|
1405 line: line, |
|
1406 column: column, |
|
1407 condition: aRequest.condition |
|
1408 }); |
|
1409 // If the original location of our generated location is different from |
|
1410 // the original location we attempted to set the breakpoint on, we will |
|
1411 // need to know so that we can set actualLocation on the response. |
|
1412 let originalLocation = this.sources.getOriginalLocation({ |
|
1413 url: url, |
|
1414 line: line, |
|
1415 column: column |
|
1416 }); |
|
1417 |
|
1418 return all([response, originalLocation]) |
|
1419 .then(([aResponse, {url, line}]) => { |
|
1420 if (aResponse.actualLocation) { |
|
1421 let actualOrigLocation = this.sources.getOriginalLocation(aResponse.actualLocation); |
|
1422 return actualOrigLocation.then(({ url, line, column }) => { |
|
1423 if (url !== originalSource |
|
1424 || line !== originalLine |
|
1425 || column !== originalColumn) { |
|
1426 aResponse.actualLocation = { |
|
1427 url: url, |
|
1428 line: line, |
|
1429 column: column |
|
1430 }; |
|
1431 } |
|
1432 return aResponse; |
|
1433 }); |
|
1434 } |
|
1435 |
|
1436 if (url !== originalSource || line !== originalLine) { |
|
1437 aResponse.actualLocation = { url: url, line: line }; |
|
1438 } |
|
1439 |
|
1440 return aResponse; |
|
1441 }); |
|
1442 }); |
|
1443 }, |
|
1444 |
|
1445 /** |
|
1446 * Create a breakpoint at the specified location and store it in the |
|
1447 * cache. Takes ownership of `aLocation`. |
|
1448 * |
|
1449 * @param Object aLocation |
|
1450 * An object of the form { url, line[, column] } |
|
1451 */ |
|
1452 _createAndStoreBreakpoint: function (aLocation) { |
|
1453 // Add the breakpoint to the store for later reuse, in case it belongs to a |
|
1454 // script that hasn't appeared yet. |
|
1455 this.breakpointStore.addBreakpoint(aLocation); |
|
1456 return this._setBreakpoint(aLocation); |
|
1457 }, |
|
1458 |
|
1459 /** |
|
1460 * Set a breakpoint using the jsdbg2 API. If the line on which the breakpoint |
|
1461 * is being set contains no code, then the breakpoint will slide down to the |
|
1462 * next line that has runnable code. In this case the server breakpoint cache |
|
1463 * will be updated, so callers that iterate over the breakpoint cache should |
|
1464 * take that into account. |
|
1465 * |
|
1466 * @param object aLocation |
|
1467 * The location of the breakpoint (in the generated source, if source |
|
1468 * mapping). |
|
1469 */ |
|
1470 _setBreakpoint: function (aLocation) { |
|
1471 let actor; |
|
1472 let storedBp = this.breakpointStore.getBreakpoint(aLocation); |
|
1473 if (storedBp.actor) { |
|
1474 actor = storedBp.actor; |
|
1475 actor.condition = aLocation.condition; |
|
1476 } else { |
|
1477 storedBp.actor = actor = new BreakpointActor(this, { |
|
1478 url: aLocation.url, |
|
1479 line: aLocation.line, |
|
1480 column: aLocation.column, |
|
1481 condition: aLocation.condition |
|
1482 }); |
|
1483 this.threadLifetimePool.addActor(actor); |
|
1484 } |
|
1485 |
|
1486 // Find all scripts matching the given location |
|
1487 let scripts = this.dbg.findScripts(aLocation); |
|
1488 if (scripts.length == 0) { |
|
1489 return { |
|
1490 error: "noScript", |
|
1491 message: "Requested setting a breakpoint on " |
|
1492 + aLocation.url + ":" + aLocation.line |
|
1493 + (aLocation.column != null ? ":" + aLocation.column : "") |
|
1494 + " but there is no Debugger.Script at that location", |
|
1495 actor: actor.actorID |
|
1496 }; |
|
1497 } |
|
1498 |
|
1499 /** |
|
1500 * For each script, if the given line has at least one entry point, set a |
|
1501 * breakpoint on the bytecode offets for each of them. |
|
1502 */ |
|
1503 |
|
1504 // Debugger.Script -> array of offset mappings |
|
1505 let scriptsAndOffsetMappings = new Map(); |
|
1506 |
|
1507 for (let script of scripts) { |
|
1508 this._findClosestOffsetMappings(aLocation, |
|
1509 script, |
|
1510 scriptsAndOffsetMappings); |
|
1511 } |
|
1512 |
|
1513 if (scriptsAndOffsetMappings.size > 0) { |
|
1514 for (let [script, mappings] of scriptsAndOffsetMappings) { |
|
1515 for (let offsetMapping of mappings) { |
|
1516 script.setBreakpoint(offsetMapping.offset, actor); |
|
1517 } |
|
1518 actor.addScript(script, this); |
|
1519 } |
|
1520 |
|
1521 return { |
|
1522 actor: actor.actorID |
|
1523 }; |
|
1524 } |
|
1525 |
|
1526 /** |
|
1527 * If we get here, no breakpoint was set. This is because the given line |
|
1528 * has no entry points, for example because it is empty. As a fallback |
|
1529 * strategy, we try to set the breakpoint on the smallest line greater |
|
1530 * than or equal to the given line that as at least one entry point. |
|
1531 */ |
|
1532 |
|
1533 // Find all innermost scripts matching the given location |
|
1534 let scripts = this.dbg.findScripts({ |
|
1535 url: aLocation.url, |
|
1536 line: aLocation.line, |
|
1537 innermost: true |
|
1538 }); |
|
1539 |
|
1540 /** |
|
1541 * For each innermost script, look for the smallest line greater than or |
|
1542 * equal to the given line that has one or more entry points. If found, set |
|
1543 * a breakpoint on the bytecode offset for each of its entry points. |
|
1544 */ |
|
1545 let actualLocation; |
|
1546 let found = false; |
|
1547 for (let script of scripts) { |
|
1548 let offsets = script.getAllOffsets(); |
|
1549 for (let line = aLocation.line; line < offsets.length; ++line) { |
|
1550 if (offsets[line]) { |
|
1551 for (let offset of offsets[line]) { |
|
1552 script.setBreakpoint(offset, actor); |
|
1553 } |
|
1554 actor.addScript(script, this); |
|
1555 if (!actualLocation) { |
|
1556 actualLocation = { |
|
1557 url: aLocation.url, |
|
1558 line: line |
|
1559 }; |
|
1560 } |
|
1561 found = true; |
|
1562 break; |
|
1563 } |
|
1564 } |
|
1565 } |
|
1566 if (found) { |
|
1567 let existingBp = this.breakpointStore.hasBreakpoint(actualLocation); |
|
1568 |
|
1569 if (existingBp && existingBp.actor) { |
|
1570 /** |
|
1571 * We already have a breakpoint actor for the actual location, so |
|
1572 * actor we created earlier is now redundant. Delete it, update the |
|
1573 * breakpoint store, and return the actor for the actual location. |
|
1574 */ |
|
1575 actor.onDelete(); |
|
1576 this.breakpointStore.removeBreakpoint(aLocation); |
|
1577 return { |
|
1578 actor: existingBp.actor.actorID, |
|
1579 actualLocation: actualLocation |
|
1580 }; |
|
1581 } else { |
|
1582 /** |
|
1583 * We don't have a breakpoint actor for the actual location yet. |
|
1584 * Instead or creating a new actor, reuse the actor we created earlier, |
|
1585 * and update the breakpoint store. |
|
1586 */ |
|
1587 actor.location = actualLocation; |
|
1588 this.breakpointStore.addBreakpoint({ |
|
1589 actor: actor, |
|
1590 url: actualLocation.url, |
|
1591 line: actualLocation.line, |
|
1592 column: actualLocation.column |
|
1593 }); |
|
1594 this.breakpointStore.removeBreakpoint(aLocation); |
|
1595 return { |
|
1596 actor: actor.actorID, |
|
1597 actualLocation: actualLocation |
|
1598 }; |
|
1599 } |
|
1600 } |
|
1601 |
|
1602 /** |
|
1603 * If we get here, no line matching the given line was found, so just |
|
1604 * fail epically. |
|
1605 */ |
|
1606 return { |
|
1607 error: "noCodeAtLineColumn", |
|
1608 actor: actor.actorID |
|
1609 }; |
|
1610 }, |
|
1611 |
|
1612 /** |
|
1613 * Find all of the offset mappings associated with `aScript` that are closest |
|
1614 * to `aTargetLocation`. If new offset mappings are found that are closer to |
|
1615 * `aTargetOffset` than the existing offset mappings inside |
|
1616 * `aScriptsAndOffsetMappings`, we empty that map and only consider the |
|
1617 * closest offset mappings. If there is no column in `aTargetLocation`, we add |
|
1618 * all offset mappings that are on the given line. |
|
1619 * |
|
1620 * @param Object aTargetLocation |
|
1621 * An object of the form { url, line[, column] }. |
|
1622 * @param Debugger.Script aScript |
|
1623 * The script in which we are searching for offsets. |
|
1624 * @param Map aScriptsAndOffsetMappings |
|
1625 * A Map object which maps Debugger.Script instances to arrays of |
|
1626 * offset mappings. This is an out param. |
|
1627 */ |
|
1628 _findClosestOffsetMappings: function (aTargetLocation, |
|
1629 aScript, |
|
1630 aScriptsAndOffsetMappings) { |
|
1631 // If we are given a column, we will try and break only at that location, |
|
1632 // otherwise we will break anytime we get on that line. |
|
1633 |
|
1634 if (aTargetLocation.column == null) { |
|
1635 let offsetMappings = aScript.getLineOffsets(aTargetLocation.line) |
|
1636 .map(o => ({ |
|
1637 line: aTargetLocation.line, |
|
1638 offset: o |
|
1639 })); |
|
1640 if (offsetMappings.length) { |
|
1641 aScriptsAndOffsetMappings.set(aScript, offsetMappings); |
|
1642 } |
|
1643 return; |
|
1644 } |
|
1645 |
|
1646 let offsetMappings = aScript.getAllColumnOffsets() |
|
1647 .filter(({ lineNumber }) => lineNumber === aTargetLocation.line); |
|
1648 |
|
1649 // Attempt to find the current closest offset distance from the target |
|
1650 // location by grabbing any offset mapping in the map by doing one iteration |
|
1651 // and then breaking (they all have the same distance from the target |
|
1652 // location). |
|
1653 let closestDistance = Infinity; |
|
1654 if (aScriptsAndOffsetMappings.size) { |
|
1655 for (let mappings of aScriptsAndOffsetMappings.values()) { |
|
1656 closestDistance = Math.abs(aTargetLocation.column - mappings[0].columnNumber); |
|
1657 break; |
|
1658 } |
|
1659 } |
|
1660 |
|
1661 for (let mapping of offsetMappings) { |
|
1662 let currentDistance = Math.abs(aTargetLocation.column - mapping.columnNumber); |
|
1663 |
|
1664 if (currentDistance > closestDistance) { |
|
1665 continue; |
|
1666 } else if (currentDistance < closestDistance) { |
|
1667 closestDistance = currentDistance; |
|
1668 aScriptsAndOffsetMappings.clear(); |
|
1669 aScriptsAndOffsetMappings.set(aScript, [mapping]); |
|
1670 } else { |
|
1671 if (!aScriptsAndOffsetMappings.has(aScript)) { |
|
1672 aScriptsAndOffsetMappings.set(aScript, []); |
|
1673 } |
|
1674 aScriptsAndOffsetMappings.get(aScript).push(mapping); |
|
1675 } |
|
1676 } |
|
1677 }, |
|
1678 |
|
1679 /** |
|
1680 * Get the script and source lists from the debugger. |
|
1681 */ |
|
1682 _discoverSources: function () { |
|
1683 // Only get one script per url. |
|
1684 const sourcesToScripts = new Map(); |
|
1685 for (let s of this.dbg.findScripts()) { |
|
1686 if (s.source) { |
|
1687 sourcesToScripts.set(s.source, s); |
|
1688 } |
|
1689 } |
|
1690 |
|
1691 return all([this.sources.sourcesForScript(script) |
|
1692 for (script of sourcesToScripts.values())]); |
|
1693 }, |
|
1694 |
|
1695 onSources: function (aRequest) { |
|
1696 return this._discoverSources().then(() => { |
|
1697 return { |
|
1698 sources: [s.form() for (s of this.sources.iter())] |
|
1699 }; |
|
1700 }); |
|
1701 }, |
|
1702 |
|
1703 /** |
|
1704 * Disassociate all breakpoint actors from their scripts and clear the |
|
1705 * breakpoint handlers. This method can be used when the thread actor intends |
|
1706 * to keep the breakpoint store, but needs to clear any actual breakpoints, |
|
1707 * e.g. due to a page navigation. This way the breakpoint actors' script |
|
1708 * caches won't hold on to the Debugger.Script objects leaking memory. |
|
1709 */ |
|
1710 disableAllBreakpoints: function () { |
|
1711 for (let bp of this.breakpointStore.findBreakpoints()) { |
|
1712 if (bp.actor) { |
|
1713 bp.actor.removeScripts(); |
|
1714 } |
|
1715 } |
|
1716 }, |
|
1717 |
|
1718 /** |
|
1719 * Handle a protocol request to pause the debuggee. |
|
1720 */ |
|
1721 onInterrupt: function (aRequest) { |
|
1722 if (this.state == "exited") { |
|
1723 return { type: "exited" }; |
|
1724 } else if (this.state == "paused") { |
|
1725 // TODO: return the actual reason for the existing pause. |
|
1726 return { type: "paused", why: { type: "alreadyPaused" } }; |
|
1727 } else if (this.state != "running") { |
|
1728 return { error: "wrongState", |
|
1729 message: "Received interrupt request in " + this.state + |
|
1730 " state." }; |
|
1731 } |
|
1732 |
|
1733 try { |
|
1734 // Put ourselves in the paused state. |
|
1735 let packet = this._paused(); |
|
1736 if (!packet) { |
|
1737 return { error: "notInterrupted" }; |
|
1738 } |
|
1739 packet.why = { type: "interrupted" }; |
|
1740 |
|
1741 // Send the response to the interrupt request now (rather than |
|
1742 // returning it), because we're going to start a nested event loop |
|
1743 // here. |
|
1744 this.conn.send(packet); |
|
1745 |
|
1746 // Start a nested event loop. |
|
1747 this._pushThreadPause(); |
|
1748 |
|
1749 // We already sent a response to this request, don't send one |
|
1750 // now. |
|
1751 return null; |
|
1752 } catch (e) { |
|
1753 reportError(e); |
|
1754 return { error: "notInterrupted", message: e.toString() }; |
|
1755 } |
|
1756 }, |
|
1757 |
|
1758 /** |
|
1759 * Handle a protocol request to retrieve all the event listeners on the page. |
|
1760 */ |
|
1761 onEventListeners: function (aRequest) { |
|
1762 // This request is only supported in content debugging. |
|
1763 if (!this.global) { |
|
1764 return { |
|
1765 error: "notImplemented", |
|
1766 message: "eventListeners request is only supported in content debugging" |
|
1767 }; |
|
1768 } |
|
1769 |
|
1770 let els = Cc["@mozilla.org/eventlistenerservice;1"] |
|
1771 .getService(Ci.nsIEventListenerService); |
|
1772 |
|
1773 let nodes = this.global.document.getElementsByTagName("*"); |
|
1774 nodes = [this.global].concat([].slice.call(nodes)); |
|
1775 let listeners = []; |
|
1776 |
|
1777 for (let node of nodes) { |
|
1778 let handlers = els.getListenerInfoFor(node); |
|
1779 |
|
1780 for (let handler of handlers) { |
|
1781 // Create a form object for serializing the listener via the protocol. |
|
1782 let listenerForm = Object.create(null); |
|
1783 let listener = handler.listenerObject; |
|
1784 // Native event listeners don't provide any listenerObject or type and |
|
1785 // are not that useful to a JS debugger. |
|
1786 if (!listener || !handler.type) { |
|
1787 continue; |
|
1788 } |
|
1789 |
|
1790 // There will be no tagName if the event listener is set on the window. |
|
1791 let selector = node.tagName ? findCssSelector(node) : "window"; |
|
1792 let nodeDO = this.globalDebugObject.makeDebuggeeValue(node); |
|
1793 listenerForm.node = { |
|
1794 selector: selector, |
|
1795 object: this.createValueGrip(nodeDO) |
|
1796 }; |
|
1797 listenerForm.type = handler.type; |
|
1798 listenerForm.capturing = handler.capturing; |
|
1799 listenerForm.allowsUntrusted = handler.allowsUntrusted; |
|
1800 listenerForm.inSystemEventGroup = handler.inSystemEventGroup; |
|
1801 listenerForm.isEventHandler = !!node["on" + listenerForm.type]; |
|
1802 // Get the Debugger.Object for the listener object. |
|
1803 let listenerDO = this.globalDebugObject.makeDebuggeeValue(listener); |
|
1804 listenerForm.function = this.createValueGrip(listenerDO); |
|
1805 listeners.push(listenerForm); |
|
1806 } |
|
1807 } |
|
1808 return { listeners: listeners }; |
|
1809 }, |
|
1810 |
|
1811 /** |
|
1812 * Return the Debug.Frame for a frame mentioned by the protocol. |
|
1813 */ |
|
1814 _requestFrame: function (aFrameID) { |
|
1815 if (!aFrameID) { |
|
1816 return this.youngestFrame; |
|
1817 } |
|
1818 |
|
1819 if (this._framePool.has(aFrameID)) { |
|
1820 return this._framePool.get(aFrameID).frame; |
|
1821 } |
|
1822 |
|
1823 return undefined; |
|
1824 }, |
|
1825 |
|
1826 _paused: function (aFrame) { |
|
1827 // We don't handle nested pauses correctly. Don't try - if we're |
|
1828 // paused, just continue running whatever code triggered the pause. |
|
1829 // We don't want to actually have nested pauses (although we |
|
1830 // have nested event loops). If code runs in the debuggee during |
|
1831 // a pause, it should cause the actor to resume (dropping |
|
1832 // pause-lifetime actors etc) and then repause when complete. |
|
1833 |
|
1834 if (this.state === "paused") { |
|
1835 return undefined; |
|
1836 } |
|
1837 |
|
1838 // Clear stepping hooks. |
|
1839 this.dbg.onEnterFrame = undefined; |
|
1840 this.dbg.onExceptionUnwind = undefined; |
|
1841 if (aFrame) { |
|
1842 aFrame.onStep = undefined; |
|
1843 aFrame.onPop = undefined; |
|
1844 } |
|
1845 // Clear DOM event breakpoints. |
|
1846 // XPCShell tests don't use actual DOM windows for globals and cause |
|
1847 // removeListenerForAllEvents to throw. |
|
1848 if (this.global && !this.global.toString().contains("Sandbox")) { |
|
1849 let els = Cc["@mozilla.org/eventlistenerservice;1"] |
|
1850 .getService(Ci.nsIEventListenerService); |
|
1851 els.removeListenerForAllEvents(this.global, this._allEventsListener, true); |
|
1852 for (let [,bp] of this._hiddenBreakpoints) { |
|
1853 bp.onDelete(); |
|
1854 } |
|
1855 this._hiddenBreakpoints.clear(); |
|
1856 } |
|
1857 |
|
1858 this._state = "paused"; |
|
1859 |
|
1860 // Create the actor pool that will hold the pause actor and its |
|
1861 // children. |
|
1862 dbg_assert(!this._pausePool, "No pause pool should exist yet"); |
|
1863 this._pausePool = new ActorPool(this.conn); |
|
1864 this.conn.addActorPool(this._pausePool); |
|
1865 |
|
1866 // Give children of the pause pool a quick link back to the |
|
1867 // thread... |
|
1868 this._pausePool.threadActor = this; |
|
1869 |
|
1870 // Create the pause actor itself... |
|
1871 dbg_assert(!this._pauseActor, "No pause actor should exist yet"); |
|
1872 this._pauseActor = new PauseActor(this._pausePool); |
|
1873 this._pausePool.addActor(this._pauseActor); |
|
1874 |
|
1875 // Update the list of frames. |
|
1876 let poppedFrames = this._updateFrames(); |
|
1877 |
|
1878 // Send off the paused packet and spin an event loop. |
|
1879 let packet = { from: this.actorID, |
|
1880 type: "paused", |
|
1881 actor: this._pauseActor.actorID }; |
|
1882 if (aFrame) { |
|
1883 packet.frame = this._createFrameActor(aFrame).form(); |
|
1884 } |
|
1885 |
|
1886 if (poppedFrames) { |
|
1887 packet.poppedFrames = poppedFrames; |
|
1888 } |
|
1889 |
|
1890 return packet; |
|
1891 }, |
|
1892 |
|
1893 _resumed: function () { |
|
1894 this._state = "running"; |
|
1895 |
|
1896 // Drop the actors in the pause actor pool. |
|
1897 this.conn.removeActorPool(this._pausePool); |
|
1898 |
|
1899 this._pausePool = null; |
|
1900 this._pauseActor = null; |
|
1901 |
|
1902 return { from: this.actorID, type: "resumed" }; |
|
1903 }, |
|
1904 |
|
1905 /** |
|
1906 * Expire frame actors for frames that have been popped. |
|
1907 * |
|
1908 * @returns A list of actor IDs whose frames have been popped. |
|
1909 */ |
|
1910 _updateFrames: function () { |
|
1911 let popped = []; |
|
1912 |
|
1913 // Create the actor pool that will hold the still-living frames. |
|
1914 let framePool = new ActorPool(this.conn); |
|
1915 let frameList = []; |
|
1916 |
|
1917 for each (let frameActor in this._frameActors) { |
|
1918 if (frameActor.frame.live) { |
|
1919 framePool.addActor(frameActor); |
|
1920 frameList.push(frameActor); |
|
1921 } else { |
|
1922 popped.push(frameActor.actorID); |
|
1923 } |
|
1924 } |
|
1925 |
|
1926 // Remove the old frame actor pool, this will expire |
|
1927 // any actors that weren't added to the new pool. |
|
1928 if (this._framePool) { |
|
1929 this.conn.removeActorPool(this._framePool); |
|
1930 } |
|
1931 |
|
1932 this._frameActors = frameList; |
|
1933 this._framePool = framePool; |
|
1934 this.conn.addActorPool(framePool); |
|
1935 |
|
1936 return popped; |
|
1937 }, |
|
1938 |
|
1939 _createFrameActor: function (aFrame) { |
|
1940 if (aFrame.actor) { |
|
1941 return aFrame.actor; |
|
1942 } |
|
1943 |
|
1944 let actor = new FrameActor(aFrame, this); |
|
1945 this._frameActors.push(actor); |
|
1946 this._framePool.addActor(actor); |
|
1947 aFrame.actor = actor; |
|
1948 |
|
1949 return actor; |
|
1950 }, |
|
1951 |
|
1952 /** |
|
1953 * Create and return an environment actor that corresponds to the provided |
|
1954 * Debugger.Environment. |
|
1955 * @param Debugger.Environment aEnvironment |
|
1956 * The lexical environment we want to extract. |
|
1957 * @param object aPool |
|
1958 * The pool where the newly-created actor will be placed. |
|
1959 * @return The EnvironmentActor for aEnvironment or undefined for host |
|
1960 * functions or functions scoped to a non-debuggee global. |
|
1961 */ |
|
1962 createEnvironmentActor: function (aEnvironment, aPool) { |
|
1963 if (!aEnvironment) { |
|
1964 return undefined; |
|
1965 } |
|
1966 |
|
1967 if (aEnvironment.actor) { |
|
1968 return aEnvironment.actor; |
|
1969 } |
|
1970 |
|
1971 let actor = new EnvironmentActor(aEnvironment, this); |
|
1972 aPool.addActor(actor); |
|
1973 aEnvironment.actor = actor; |
|
1974 |
|
1975 return actor; |
|
1976 }, |
|
1977 |
|
1978 /** |
|
1979 * Create a grip for the given debuggee value. If the value is an |
|
1980 * object, will create an actor with the given lifetime. |
|
1981 */ |
|
1982 createValueGrip: function (aValue, aPool=false) { |
|
1983 if (!aPool) { |
|
1984 aPool = this._pausePool; |
|
1985 } |
|
1986 |
|
1987 switch (typeof aValue) { |
|
1988 case "boolean": |
|
1989 return aValue; |
|
1990 case "string": |
|
1991 if (this._stringIsLong(aValue)) { |
|
1992 return this.longStringGrip(aValue, aPool); |
|
1993 } |
|
1994 return aValue; |
|
1995 case "number": |
|
1996 if (aValue === Infinity) { |
|
1997 return { type: "Infinity" }; |
|
1998 } else if (aValue === -Infinity) { |
|
1999 return { type: "-Infinity" }; |
|
2000 } else if (Number.isNaN(aValue)) { |
|
2001 return { type: "NaN" }; |
|
2002 } else if (!aValue && 1 / aValue === -Infinity) { |
|
2003 return { type: "-0" }; |
|
2004 } |
|
2005 return aValue; |
|
2006 case "undefined": |
|
2007 return { type: "undefined" }; |
|
2008 case "object": |
|
2009 if (aValue === null) { |
|
2010 return { type: "null" }; |
|
2011 } |
|
2012 return this.objectGrip(aValue, aPool); |
|
2013 default: |
|
2014 dbg_assert(false, "Failed to provide a grip for: " + aValue); |
|
2015 return null; |
|
2016 } |
|
2017 }, |
|
2018 |
|
2019 /** |
|
2020 * Return a protocol completion value representing the given |
|
2021 * Debugger-provided completion value. |
|
2022 */ |
|
2023 createProtocolCompletionValue: function (aCompletion) { |
|
2024 let protoValue = {}; |
|
2025 if ("return" in aCompletion) { |
|
2026 protoValue.return = this.createValueGrip(aCompletion.return); |
|
2027 } else if ("yield" in aCompletion) { |
|
2028 protoValue.return = this.createValueGrip(aCompletion.yield); |
|
2029 } else if ("throw" in aCompletion) { |
|
2030 protoValue.throw = this.createValueGrip(aCompletion.throw); |
|
2031 } else { |
|
2032 protoValue.terminated = true; |
|
2033 } |
|
2034 return protoValue; |
|
2035 }, |
|
2036 |
|
2037 /** |
|
2038 * Create a grip for the given debuggee object. |
|
2039 * |
|
2040 * @param aValue Debugger.Object |
|
2041 * The debuggee object value. |
|
2042 * @param aPool ActorPool |
|
2043 * The actor pool where the new object actor will be added. |
|
2044 */ |
|
2045 objectGrip: function (aValue, aPool) { |
|
2046 if (!aPool.objectActors) { |
|
2047 aPool.objectActors = new WeakMap(); |
|
2048 } |
|
2049 |
|
2050 if (aPool.objectActors.has(aValue)) { |
|
2051 return aPool.objectActors.get(aValue).grip(); |
|
2052 } else if (this.threadLifetimePool.objectActors.has(aValue)) { |
|
2053 return this.threadLifetimePool.objectActors.get(aValue).grip(); |
|
2054 } |
|
2055 |
|
2056 let actor = new PauseScopedObjectActor(aValue, this); |
|
2057 aPool.addActor(actor); |
|
2058 aPool.objectActors.set(aValue, actor); |
|
2059 return actor.grip(); |
|
2060 }, |
|
2061 |
|
2062 /** |
|
2063 * Create a grip for the given debuggee object with a pause lifetime. |
|
2064 * |
|
2065 * @param aValue Debugger.Object |
|
2066 * The debuggee object value. |
|
2067 */ |
|
2068 pauseObjectGrip: function (aValue) { |
|
2069 if (!this._pausePool) { |
|
2070 throw "Object grip requested while not paused."; |
|
2071 } |
|
2072 |
|
2073 return this.objectGrip(aValue, this._pausePool); |
|
2074 }, |
|
2075 |
|
2076 /** |
|
2077 * Extend the lifetime of the provided object actor to thread lifetime. |
|
2078 * |
|
2079 * @param aActor object |
|
2080 * The object actor. |
|
2081 */ |
|
2082 threadObjectGrip: function (aActor) { |
|
2083 // We want to reuse the existing actor ID, so we just remove it from the |
|
2084 // current pool's weak map and then let pool.addActor do the rest. |
|
2085 aActor.registeredPool.objectActors.delete(aActor.obj); |
|
2086 this.threadLifetimePool.addActor(aActor); |
|
2087 this.threadLifetimePool.objectActors.set(aActor.obj, aActor); |
|
2088 }, |
|
2089 |
|
2090 /** |
|
2091 * Handle a protocol request to promote multiple pause-lifetime grips to |
|
2092 * thread-lifetime grips. |
|
2093 * |
|
2094 * @param aRequest object |
|
2095 * The protocol request object. |
|
2096 */ |
|
2097 onThreadGrips: function (aRequest) { |
|
2098 if (this.state != "paused") { |
|
2099 return { error: "wrongState" }; |
|
2100 } |
|
2101 |
|
2102 if (!aRequest.actors) { |
|
2103 return { error: "missingParameter", |
|
2104 message: "no actors were specified" }; |
|
2105 } |
|
2106 |
|
2107 for (let actorID of aRequest.actors) { |
|
2108 let actor = this._pausePool.get(actorID); |
|
2109 if (actor) { |
|
2110 this.threadObjectGrip(actor); |
|
2111 } |
|
2112 } |
|
2113 return {}; |
|
2114 }, |
|
2115 |
|
2116 /** |
|
2117 * Create a grip for the given string. |
|
2118 * |
|
2119 * @param aString String |
|
2120 * The string we are creating a grip for. |
|
2121 * @param aPool ActorPool |
|
2122 * The actor pool where the new actor will be added. |
|
2123 */ |
|
2124 longStringGrip: function (aString, aPool) { |
|
2125 if (!aPool.longStringActors) { |
|
2126 aPool.longStringActors = {}; |
|
2127 } |
|
2128 |
|
2129 if (aPool.longStringActors.hasOwnProperty(aString)) { |
|
2130 return aPool.longStringActors[aString].grip(); |
|
2131 } |
|
2132 |
|
2133 let actor = new LongStringActor(aString, this); |
|
2134 aPool.addActor(actor); |
|
2135 aPool.longStringActors[aString] = actor; |
|
2136 return actor.grip(); |
|
2137 }, |
|
2138 |
|
2139 /** |
|
2140 * Create a long string grip that is scoped to a pause. |
|
2141 * |
|
2142 * @param aString String |
|
2143 * The string we are creating a grip for. |
|
2144 */ |
|
2145 pauseLongStringGrip: function (aString) { |
|
2146 return this.longStringGrip(aString, this._pausePool); |
|
2147 }, |
|
2148 |
|
2149 /** |
|
2150 * Create a long string grip that is scoped to a thread. |
|
2151 * |
|
2152 * @param aString String |
|
2153 * The string we are creating a grip for. |
|
2154 */ |
|
2155 threadLongStringGrip: function (aString) { |
|
2156 return this.longStringGrip(aString, this._threadLifetimePool); |
|
2157 }, |
|
2158 |
|
2159 /** |
|
2160 * Returns true if the string is long enough to use a LongStringActor instead |
|
2161 * of passing the value directly over the protocol. |
|
2162 * |
|
2163 * @param aString String |
|
2164 * The string we are checking the length of. |
|
2165 */ |
|
2166 _stringIsLong: function (aString) { |
|
2167 return aString.length >= DebuggerServer.LONG_STRING_LENGTH; |
|
2168 }, |
|
2169 |
|
2170 // JS Debugger API hooks. |
|
2171 |
|
2172 /** |
|
2173 * A function that the engine calls when a call to a debug event hook, |
|
2174 * breakpoint handler, watchpoint handler, or similar function throws some |
|
2175 * exception. |
|
2176 * |
|
2177 * @param aException exception |
|
2178 * The exception that was thrown in the debugger code. |
|
2179 */ |
|
2180 uncaughtExceptionHook: function (aException) { |
|
2181 dumpn("Got an exception: " + aException.message + "\n" + aException.stack); |
|
2182 }, |
|
2183 |
|
2184 /** |
|
2185 * A function that the engine calls when a debugger statement has been |
|
2186 * executed in the specified frame. |
|
2187 * |
|
2188 * @param aFrame Debugger.Frame |
|
2189 * The stack frame that contained the debugger statement. |
|
2190 */ |
|
2191 onDebuggerStatement: function (aFrame) { |
|
2192 // Don't pause if we are currently stepping (in or over) or the frame is |
|
2193 // black-boxed. |
|
2194 const generatedLocation = getFrameLocation(aFrame); |
|
2195 const { url } = this.synchronize(this.sources.getOriginalLocation( |
|
2196 generatedLocation)); |
|
2197 |
|
2198 return this.sources.isBlackBoxed(url) || aFrame.onStep |
|
2199 ? undefined |
|
2200 : this._pauseAndRespond(aFrame, { type: "debuggerStatement" }); |
|
2201 }, |
|
2202 |
|
2203 /** |
|
2204 * A function that the engine calls when an exception has been thrown and has |
|
2205 * propagated to the specified frame. |
|
2206 * |
|
2207 * @param aFrame Debugger.Frame |
|
2208 * The youngest remaining stack frame. |
|
2209 * @param aValue object |
|
2210 * The exception that was thrown. |
|
2211 */ |
|
2212 onExceptionUnwind: function (aFrame, aValue) { |
|
2213 let willBeCaught = false; |
|
2214 for (let frame = aFrame; frame != null; frame = frame.older) { |
|
2215 if (frame.script.isInCatchScope(frame.offset)) { |
|
2216 willBeCaught = true; |
|
2217 break; |
|
2218 } |
|
2219 } |
|
2220 |
|
2221 if (willBeCaught && this._options.ignoreCaughtExceptions) { |
|
2222 return undefined; |
|
2223 } |
|
2224 |
|
2225 const generatedLocation = getFrameLocation(aFrame); |
|
2226 const { url } = this.synchronize(this.sources.getOriginalLocation( |
|
2227 generatedLocation)); |
|
2228 |
|
2229 if (this.sources.isBlackBoxed(url)) { |
|
2230 return undefined; |
|
2231 } |
|
2232 |
|
2233 try { |
|
2234 let packet = this._paused(aFrame); |
|
2235 if (!packet) { |
|
2236 return undefined; |
|
2237 } |
|
2238 |
|
2239 packet.why = { type: "exception", |
|
2240 exception: this.createValueGrip(aValue) }; |
|
2241 this.conn.send(packet); |
|
2242 |
|
2243 this._pushThreadPause(); |
|
2244 } catch(e) { |
|
2245 reportError(e, "Got an exception during TA_onExceptionUnwind: "); |
|
2246 } |
|
2247 |
|
2248 return undefined; |
|
2249 }, |
|
2250 |
|
2251 /** |
|
2252 * A function that the engine calls when a new script has been loaded into the |
|
2253 * scope of the specified debuggee global. |
|
2254 * |
|
2255 * @param aScript Debugger.Script |
|
2256 * The source script that has been loaded into a debuggee compartment. |
|
2257 * @param aGlobal Debugger.Object |
|
2258 * A Debugger.Object instance whose referent is the global object. |
|
2259 */ |
|
2260 onNewScript: function (aScript, aGlobal) { |
|
2261 this._addScript(aScript); |
|
2262 |
|
2263 // |onNewScript| is only fired for top level scripts (AKA staticLevel == 0), |
|
2264 // so we have to make sure to call |_addScript| on every child script as |
|
2265 // well to restore breakpoints in those scripts. |
|
2266 for (let s of aScript.getChildScripts()) { |
|
2267 this._addScript(s); |
|
2268 } |
|
2269 |
|
2270 this.sources.sourcesForScript(aScript); |
|
2271 }, |
|
2272 |
|
2273 onNewSource: function (aSource) { |
|
2274 this.conn.send({ |
|
2275 from: this.actorID, |
|
2276 type: "newSource", |
|
2277 source: aSource.form() |
|
2278 }); |
|
2279 }, |
|
2280 |
|
2281 /** |
|
2282 * Check if scripts from the provided source URL are allowed to be stored in |
|
2283 * the cache. |
|
2284 * |
|
2285 * @param aSourceUrl String |
|
2286 * The url of the script's source that will be stored. |
|
2287 * @returns true, if the script can be added, false otherwise. |
|
2288 */ |
|
2289 _allowSource: function (aSourceUrl) { |
|
2290 // Ignore anything we don't have a URL for (eval scripts, for example). |
|
2291 if (!aSourceUrl) |
|
2292 return false; |
|
2293 // Ignore XBL bindings for content debugging. |
|
2294 if (aSourceUrl.indexOf("chrome://") == 0) { |
|
2295 return false; |
|
2296 } |
|
2297 // Ignore about:* pages for content debugging. |
|
2298 if (aSourceUrl.indexOf("about:") == 0) { |
|
2299 return false; |
|
2300 } |
|
2301 return true; |
|
2302 }, |
|
2303 |
|
2304 /** |
|
2305 * Restore any pre-existing breakpoints to the scripts that we have access to. |
|
2306 */ |
|
2307 _restoreBreakpoints: function () { |
|
2308 if (this.breakpointStore.size === 0) { |
|
2309 return; |
|
2310 } |
|
2311 |
|
2312 for (let s of this.dbg.findScripts()) { |
|
2313 this._addScript(s); |
|
2314 } |
|
2315 }, |
|
2316 |
|
2317 /** |
|
2318 * Add the provided script to the server cache. |
|
2319 * |
|
2320 * @param aScript Debugger.Script |
|
2321 * The source script that will be stored. |
|
2322 * @returns true, if the script was added; false otherwise. |
|
2323 */ |
|
2324 _addScript: function (aScript) { |
|
2325 if (!this._allowSource(aScript.url)) { |
|
2326 return false; |
|
2327 } |
|
2328 |
|
2329 // Set any stored breakpoints. |
|
2330 |
|
2331 let endLine = aScript.startLine + aScript.lineCount - 1; |
|
2332 for (let bp of this.breakpointStore.findBreakpoints({ url: aScript.url })) { |
|
2333 // Only consider breakpoints that are not already associated with |
|
2334 // scripts, and limit search to the line numbers contained in the new |
|
2335 // script. |
|
2336 if (!bp.actor.scripts.length |
|
2337 && bp.line >= aScript.startLine |
|
2338 && bp.line <= endLine) { |
|
2339 this._setBreakpoint(bp); |
|
2340 } |
|
2341 } |
|
2342 |
|
2343 return true; |
|
2344 }, |
|
2345 |
|
2346 |
|
2347 /** |
|
2348 * Get prototypes and properties of multiple objects. |
|
2349 */ |
|
2350 onPrototypesAndProperties: function (aRequest) { |
|
2351 let result = {}; |
|
2352 for (let actorID of aRequest.actors) { |
|
2353 // This code assumes that there are no lazily loaded actors returned |
|
2354 // by this call. |
|
2355 let actor = this.conn.getActor(actorID); |
|
2356 if (!actor) { |
|
2357 return { from: this.actorID, |
|
2358 error: "noSuchActor" }; |
|
2359 } |
|
2360 let handler = actor.onPrototypeAndProperties; |
|
2361 if (!handler) { |
|
2362 return { from: this.actorID, |
|
2363 error: "unrecognizedPacketType", |
|
2364 message: ('Actor "' + actorID + |
|
2365 '" does not recognize the packet type ' + |
|
2366 '"prototypeAndProperties"') }; |
|
2367 } |
|
2368 result[actorID] = handler.call(actor, {}); |
|
2369 } |
|
2370 return { from: this.actorID, |
|
2371 actors: result }; |
|
2372 } |
|
2373 |
|
2374 }; |
|
2375 |
|
2376 ThreadActor.prototype.requestTypes = { |
|
2377 "attach": ThreadActor.prototype.onAttach, |
|
2378 "detach": ThreadActor.prototype.onDetach, |
|
2379 "reconfigure": ThreadActor.prototype.onReconfigure, |
|
2380 "resume": ThreadActor.prototype.onResume, |
|
2381 "clientEvaluate": ThreadActor.prototype.onClientEvaluate, |
|
2382 "frames": ThreadActor.prototype.onFrames, |
|
2383 "interrupt": ThreadActor.prototype.onInterrupt, |
|
2384 "eventListeners": ThreadActor.prototype.onEventListeners, |
|
2385 "releaseMany": ThreadActor.prototype.onReleaseMany, |
|
2386 "setBreakpoint": ThreadActor.prototype.onSetBreakpoint, |
|
2387 "sources": ThreadActor.prototype.onSources, |
|
2388 "threadGrips": ThreadActor.prototype.onThreadGrips, |
|
2389 "prototypesAndProperties": ThreadActor.prototype.onPrototypesAndProperties |
|
2390 }; |
|
2391 |
|
2392 |
|
2393 /** |
|
2394 * Creates a PauseActor. |
|
2395 * |
|
2396 * PauseActors exist for the lifetime of a given debuggee pause. Used to |
|
2397 * scope pause-lifetime grips. |
|
2398 * |
|
2399 * @param ActorPool aPool |
|
2400 * The actor pool created for this pause. |
|
2401 */ |
|
2402 function PauseActor(aPool) |
|
2403 { |
|
2404 this.pool = aPool; |
|
2405 } |
|
2406 |
|
2407 PauseActor.prototype = { |
|
2408 actorPrefix: "pause" |
|
2409 }; |
|
2410 |
|
2411 |
|
2412 /** |
|
2413 * A base actor for any actors that should only respond receive messages in the |
|
2414 * paused state. Subclasses may expose a `threadActor` which is used to help |
|
2415 * determine when we are in a paused state. Subclasses should set their own |
|
2416 * "constructor" property if they want better error messages. You should never |
|
2417 * instantiate a PauseScopedActor directly, only through subclasses. |
|
2418 */ |
|
2419 function PauseScopedActor() |
|
2420 { |
|
2421 } |
|
2422 |
|
2423 /** |
|
2424 * A function decorator for creating methods to handle protocol messages that |
|
2425 * should only be received while in the paused state. |
|
2426 * |
|
2427 * @param aMethod Function |
|
2428 * The function we are decorating. |
|
2429 */ |
|
2430 PauseScopedActor.withPaused = function (aMethod) { |
|
2431 return function () { |
|
2432 if (this.isPaused()) { |
|
2433 return aMethod.apply(this, arguments); |
|
2434 } else { |
|
2435 return this._wrongState(); |
|
2436 } |
|
2437 }; |
|
2438 }; |
|
2439 |
|
2440 PauseScopedActor.prototype = { |
|
2441 |
|
2442 /** |
|
2443 * Returns true if we are in the paused state. |
|
2444 */ |
|
2445 isPaused: function () { |
|
2446 // When there is not a ThreadActor available (like in the webconsole) we |
|
2447 // have to be optimistic and assume that we are paused so that we can |
|
2448 // respond to requests. |
|
2449 return this.threadActor ? this.threadActor.state === "paused" : true; |
|
2450 }, |
|
2451 |
|
2452 /** |
|
2453 * Returns the wrongState response packet for this actor. |
|
2454 */ |
|
2455 _wrongState: function () { |
|
2456 return { |
|
2457 error: "wrongState", |
|
2458 message: this.constructor.name + |
|
2459 " actors can only be accessed while the thread is paused." |
|
2460 }; |
|
2461 } |
|
2462 }; |
|
2463 |
|
2464 /** |
|
2465 * Resolve a URI back to physical file. |
|
2466 * |
|
2467 * Of course, this works only for URIs pointing to local resources. |
|
2468 * |
|
2469 * @param aURI |
|
2470 * URI to resolve |
|
2471 * @return |
|
2472 * resolved nsIURI |
|
2473 */ |
|
2474 function resolveURIToLocalPath(aURI) { |
|
2475 switch (aURI.scheme) { |
|
2476 case "jar": |
|
2477 case "file": |
|
2478 return aURI; |
|
2479 |
|
2480 case "chrome": |
|
2481 let resolved = Cc["@mozilla.org/chrome/chrome-registry;1"]. |
|
2482 getService(Ci.nsIChromeRegistry).convertChromeURL(aURI); |
|
2483 return resolveURIToLocalPath(resolved); |
|
2484 |
|
2485 case "resource": |
|
2486 resolved = Cc["@mozilla.org/network/protocol;1?name=resource"]. |
|
2487 getService(Ci.nsIResProtocolHandler).resolveURI(aURI); |
|
2488 aURI = Services.io.newURI(resolved, null, null); |
|
2489 return resolveURIToLocalPath(aURI); |
|
2490 |
|
2491 default: |
|
2492 return null; |
|
2493 } |
|
2494 } |
|
2495 |
|
2496 /** |
|
2497 * A SourceActor provides information about the source of a script. |
|
2498 * |
|
2499 * @param String url |
|
2500 * The url of the source we are representing. |
|
2501 * @param ThreadActor thread |
|
2502 * The current thread actor. |
|
2503 * @param SourceMapConsumer sourceMap |
|
2504 * Optional. The source map that introduced this source, if available. |
|
2505 * @param String generatedSource |
|
2506 * Optional, passed in when aSourceMap is also passed in. The generated |
|
2507 * source url that introduced this source. |
|
2508 * @param String text |
|
2509 * Optional. The content text of this source, if immediately available. |
|
2510 * @param String contentType |
|
2511 * Optional. The content type of this source, if immediately available. |
|
2512 */ |
|
2513 function SourceActor({ url, thread, sourceMap, generatedSource, text, |
|
2514 contentType }) { |
|
2515 this._threadActor = thread; |
|
2516 this._url = url; |
|
2517 this._sourceMap = sourceMap; |
|
2518 this._generatedSource = generatedSource; |
|
2519 this._text = text; |
|
2520 this._contentType = contentType; |
|
2521 |
|
2522 this.onSource = this.onSource.bind(this); |
|
2523 this._invertSourceMap = this._invertSourceMap.bind(this); |
|
2524 this._saveMap = this._saveMap.bind(this); |
|
2525 this._getSourceText = this._getSourceText.bind(this); |
|
2526 |
|
2527 this._mapSourceToAddon(); |
|
2528 |
|
2529 if (this.threadActor.sources.isPrettyPrinted(this.url)) { |
|
2530 this._init = this.onPrettyPrint({ |
|
2531 indent: this.threadActor.sources.prettyPrintIndent(this.url) |
|
2532 }).then(null, error => { |
|
2533 DevToolsUtils.reportException("SourceActor", error); |
|
2534 }); |
|
2535 } else { |
|
2536 this._init = null; |
|
2537 } |
|
2538 } |
|
2539 |
|
2540 SourceActor.prototype = { |
|
2541 constructor: SourceActor, |
|
2542 actorPrefix: "source", |
|
2543 |
|
2544 _oldSourceMap: null, |
|
2545 _init: null, |
|
2546 _addonID: null, |
|
2547 _addonPath: null, |
|
2548 |
|
2549 get threadActor() this._threadActor, |
|
2550 get url() this._url, |
|
2551 get addonID() this._addonID, |
|
2552 get addonPath() this._addonPath, |
|
2553 |
|
2554 get prettyPrintWorker() { |
|
2555 return this.threadActor.prettyPrintWorker; |
|
2556 }, |
|
2557 |
|
2558 form: function () { |
|
2559 return { |
|
2560 actor: this.actorID, |
|
2561 url: this._url, |
|
2562 addonID: this._addonID, |
|
2563 addonPath: this._addonPath, |
|
2564 isBlackBoxed: this.threadActor.sources.isBlackBoxed(this.url), |
|
2565 isPrettyPrinted: this.threadActor.sources.isPrettyPrinted(this.url) |
|
2566 // TODO bug 637572: introductionScript |
|
2567 }; |
|
2568 }, |
|
2569 |
|
2570 disconnect: function () { |
|
2571 if (this.registeredPool && this.registeredPool.sourceActors) { |
|
2572 delete this.registeredPool.sourceActors[this.actorID]; |
|
2573 } |
|
2574 }, |
|
2575 |
|
2576 _mapSourceToAddon: function() { |
|
2577 try { |
|
2578 var nsuri = Services.io.newURI(this._url.split(" -> ").pop(), null, null); |
|
2579 } |
|
2580 catch (e) { |
|
2581 // We can't do anything with an invalid URI |
|
2582 return; |
|
2583 } |
|
2584 |
|
2585 let localURI = resolveURIToLocalPath(nsuri); |
|
2586 |
|
2587 let id = {}; |
|
2588 if (localURI && mapURIToAddonID(localURI, id)) { |
|
2589 this._addonID = id.value; |
|
2590 |
|
2591 if (localURI instanceof Ci.nsIJARURI) { |
|
2592 // The path in the add-on is easy for jar: uris |
|
2593 this._addonPath = localURI.JAREntry; |
|
2594 } |
|
2595 else if (localURI instanceof Ci.nsIFileURL) { |
|
2596 // For file: uris walk up to find the last directory that is part of the |
|
2597 // add-on |
|
2598 let target = localURI.file; |
|
2599 let path = target.leafName; |
|
2600 |
|
2601 // We can assume that the directory containing the source file is part |
|
2602 // of the add-on |
|
2603 let root = target.parent; |
|
2604 let file = root.parent; |
|
2605 while (file && mapURIToAddonID(Services.io.newFileURI(file), {})) { |
|
2606 path = root.leafName + "/" + path; |
|
2607 root = file; |
|
2608 file = file.parent; |
|
2609 } |
|
2610 |
|
2611 if (!file) { |
|
2612 const error = new Error("Could not find the root of the add-on for " + this._url); |
|
2613 DevToolsUtils.reportException("SourceActor.prototype._mapSourceToAddon", error) |
|
2614 return; |
|
2615 } |
|
2616 |
|
2617 this._addonPath = path; |
|
2618 } |
|
2619 } |
|
2620 }, |
|
2621 |
|
2622 _getSourceText: function () { |
|
2623 const toResolvedContent = t => resolve({ |
|
2624 content: t, |
|
2625 contentType: this._contentType |
|
2626 }); |
|
2627 |
|
2628 let sc; |
|
2629 if (this._sourceMap && (sc = this._sourceMap.sourceContentFor(this._url))) { |
|
2630 return toResolvedContent(sc); |
|
2631 } |
|
2632 |
|
2633 if (this._text) { |
|
2634 return toResolvedContent(this._text); |
|
2635 } |
|
2636 |
|
2637 // XXX bug 865252: Don't load from the cache if this is a source mapped |
|
2638 // source because we can't guarantee that the cache has the most up to date |
|
2639 // content for this source like we can if it isn't source mapped. |
|
2640 let sourceFetched = fetch(this._url, { loadFromCache: !this._sourceMap }); |
|
2641 |
|
2642 // Record the contentType we just learned during fetching |
|
2643 sourceFetched.then(({ contentType }) => { |
|
2644 this._contentType = contentType; |
|
2645 }); |
|
2646 |
|
2647 return sourceFetched; |
|
2648 }, |
|
2649 |
|
2650 /** |
|
2651 * Handler for the "source" packet. |
|
2652 */ |
|
2653 onSource: function () { |
|
2654 return resolve(this._init) |
|
2655 .then(this._getSourceText) |
|
2656 .then(({ content, contentType }) => { |
|
2657 return { |
|
2658 from: this.actorID, |
|
2659 source: this.threadActor.createValueGrip( |
|
2660 content, this.threadActor.threadLifetimePool), |
|
2661 contentType: contentType |
|
2662 }; |
|
2663 }) |
|
2664 .then(null, aError => { |
|
2665 reportError(aError, "Got an exception during SA_onSource: "); |
|
2666 return { |
|
2667 "from": this.actorID, |
|
2668 "error": "loadSourceError", |
|
2669 "message": "Could not load the source for " + this._url + ".\n" |
|
2670 + DevToolsUtils.safeErrorString(aError) |
|
2671 }; |
|
2672 }); |
|
2673 }, |
|
2674 |
|
2675 /** |
|
2676 * Handler for the "prettyPrint" packet. |
|
2677 */ |
|
2678 onPrettyPrint: function ({ indent }) { |
|
2679 this.threadActor.sources.prettyPrint(this._url, indent); |
|
2680 return this._getSourceText() |
|
2681 .then(this._sendToPrettyPrintWorker(indent)) |
|
2682 .then(this._invertSourceMap) |
|
2683 .then(this._saveMap) |
|
2684 .then(() => { |
|
2685 // We need to reset `_init` now because we have already done the work of |
|
2686 // pretty printing, and don't want onSource to wait forever for |
|
2687 // initialization to complete. |
|
2688 this._init = null; |
|
2689 }) |
|
2690 .then(this.onSource) |
|
2691 .then(null, error => { |
|
2692 this.onDisablePrettyPrint(); |
|
2693 return { |
|
2694 from: this.actorID, |
|
2695 error: "prettyPrintError", |
|
2696 message: DevToolsUtils.safeErrorString(error) |
|
2697 }; |
|
2698 }); |
|
2699 }, |
|
2700 |
|
2701 /** |
|
2702 * Return a function that sends a request to the pretty print worker, waits on |
|
2703 * the worker's response, and then returns the pretty printed code. |
|
2704 * |
|
2705 * @param Number aIndent |
|
2706 * The number of spaces to indent by the code by, when we send the |
|
2707 * request to the pretty print worker. |
|
2708 * @returns Function |
|
2709 * Returns a function which takes an AST, and returns a promise that |
|
2710 * is resolved with `{ code, mappings }` where `code` is the pretty |
|
2711 * printed code, and `mappings` is an array of source mappings. |
|
2712 */ |
|
2713 _sendToPrettyPrintWorker: function (aIndent) { |
|
2714 return ({ content }) => { |
|
2715 const deferred = promise.defer(); |
|
2716 const id = Math.random(); |
|
2717 |
|
2718 const onReply = ({ data }) => { |
|
2719 if (data.id !== id) { |
|
2720 return; |
|
2721 } |
|
2722 this.prettyPrintWorker.removeEventListener("message", onReply, false); |
|
2723 |
|
2724 if (data.error) { |
|
2725 deferred.reject(new Error(data.error)); |
|
2726 } else { |
|
2727 deferred.resolve(data); |
|
2728 } |
|
2729 }; |
|
2730 |
|
2731 this.prettyPrintWorker.addEventListener("message", onReply, false); |
|
2732 this.prettyPrintWorker.postMessage({ |
|
2733 id: id, |
|
2734 url: this._url, |
|
2735 indent: aIndent, |
|
2736 source: content |
|
2737 }); |
|
2738 |
|
2739 return deferred.promise; |
|
2740 }; |
|
2741 }, |
|
2742 |
|
2743 /** |
|
2744 * Invert a source map. So if a source map maps from a to b, return a new |
|
2745 * source map from b to a. We need to do this because the source map we get |
|
2746 * from _generatePrettyCodeAndMap goes the opposite way we want it to for |
|
2747 * debugging. |
|
2748 * |
|
2749 * Note that the source map is modified in place. |
|
2750 */ |
|
2751 _invertSourceMap: function ({ code, mappings }) { |
|
2752 const generator = new SourceMapGenerator({ file: this._url }); |
|
2753 return DevToolsUtils.yieldingEach(mappings, m => { |
|
2754 let mapping = { |
|
2755 generated: { |
|
2756 line: m.generatedLine, |
|
2757 column: m.generatedColumn |
|
2758 } |
|
2759 }; |
|
2760 if (m.source) { |
|
2761 mapping.source = m.source; |
|
2762 mapping.original = { |
|
2763 line: m.originalLine, |
|
2764 column: m.originalColumn |
|
2765 }; |
|
2766 mapping.name = m.name; |
|
2767 } |
|
2768 generator.addMapping(mapping); |
|
2769 }).then(() => { |
|
2770 generator.setSourceContent(this._url, code); |
|
2771 const consumer = SourceMapConsumer.fromSourceMap(generator); |
|
2772 |
|
2773 // XXX bug 918802: Monkey punch the source map consumer, because iterating |
|
2774 // over all mappings and inverting each of them, and then creating a new |
|
2775 // SourceMapConsumer is slow. |
|
2776 |
|
2777 const getOrigPos = consumer.originalPositionFor.bind(consumer); |
|
2778 const getGenPos = consumer.generatedPositionFor.bind(consumer); |
|
2779 |
|
2780 consumer.originalPositionFor = ({ line, column }) => { |
|
2781 const location = getGenPos({ |
|
2782 line: line, |
|
2783 column: column, |
|
2784 source: this._url |
|
2785 }); |
|
2786 location.source = this._url; |
|
2787 return location; |
|
2788 }; |
|
2789 |
|
2790 consumer.generatedPositionFor = ({ line, column }) => getOrigPos({ |
|
2791 line: line, |
|
2792 column: column |
|
2793 }); |
|
2794 |
|
2795 return { |
|
2796 code: code, |
|
2797 map: consumer |
|
2798 }; |
|
2799 }); |
|
2800 }, |
|
2801 |
|
2802 /** |
|
2803 * Save the source map back to our thread's ThreadSources object so that |
|
2804 * stepping, breakpoints, debugger statements, etc can use it. If we are |
|
2805 * pretty printing a source mapped source, we need to compose the existing |
|
2806 * source map with our new one. |
|
2807 */ |
|
2808 _saveMap: function ({ map }) { |
|
2809 if (this._sourceMap) { |
|
2810 // Compose the source maps |
|
2811 this._oldSourceMap = this._sourceMap; |
|
2812 this._sourceMap = SourceMapGenerator.fromSourceMap(this._sourceMap); |
|
2813 this._sourceMap.applySourceMap(map, this._url); |
|
2814 this._sourceMap = SourceMapConsumer.fromSourceMap(this._sourceMap); |
|
2815 this._threadActor.sources.saveSourceMap(this._sourceMap, |
|
2816 this._generatedSource); |
|
2817 } else { |
|
2818 this._sourceMap = map; |
|
2819 this._threadActor.sources.saveSourceMap(this._sourceMap, this._url); |
|
2820 } |
|
2821 }, |
|
2822 |
|
2823 /** |
|
2824 * Handler for the "disablePrettyPrint" packet. |
|
2825 */ |
|
2826 onDisablePrettyPrint: function () { |
|
2827 this._sourceMap = this._oldSourceMap; |
|
2828 this.threadActor.sources.saveSourceMap(this._sourceMap, |
|
2829 this._generatedSource || this._url); |
|
2830 this.threadActor.sources.disablePrettyPrint(this._url); |
|
2831 return this.onSource(); |
|
2832 }, |
|
2833 |
|
2834 /** |
|
2835 * Handler for the "blackbox" packet. |
|
2836 */ |
|
2837 onBlackBox: function (aRequest) { |
|
2838 this.threadActor.sources.blackBox(this.url); |
|
2839 let packet = { |
|
2840 from: this.actorID |
|
2841 }; |
|
2842 if (this.threadActor.state == "paused" |
|
2843 && this.threadActor.youngestFrame |
|
2844 && this.threadActor.youngestFrame.script.url == this.url) { |
|
2845 packet.pausedInSource = true; |
|
2846 } |
|
2847 return packet; |
|
2848 }, |
|
2849 |
|
2850 /** |
|
2851 * Handler for the "unblackbox" packet. |
|
2852 */ |
|
2853 onUnblackBox: function (aRequest) { |
|
2854 this.threadActor.sources.unblackBox(this.url); |
|
2855 return { |
|
2856 from: this.actorID |
|
2857 }; |
|
2858 } |
|
2859 }; |
|
2860 |
|
2861 SourceActor.prototype.requestTypes = { |
|
2862 "source": SourceActor.prototype.onSource, |
|
2863 "blackbox": SourceActor.prototype.onBlackBox, |
|
2864 "unblackbox": SourceActor.prototype.onUnblackBox, |
|
2865 "prettyPrint": SourceActor.prototype.onPrettyPrint, |
|
2866 "disablePrettyPrint": SourceActor.prototype.onDisablePrettyPrint |
|
2867 }; |
|
2868 |
|
2869 |
|
2870 /** |
|
2871 * Determine if a given value is non-primitive. |
|
2872 * |
|
2873 * @param Any aValue |
|
2874 * The value to test. |
|
2875 * @return Boolean |
|
2876 * Whether the value is non-primitive. |
|
2877 */ |
|
2878 function isObject(aValue) { |
|
2879 const type = typeof aValue; |
|
2880 return type == "object" ? aValue !== null : type == "function"; |
|
2881 } |
|
2882 |
|
2883 /** |
|
2884 * Create a function that can safely stringify Debugger.Objects of a given |
|
2885 * builtin type. |
|
2886 * |
|
2887 * @param Function aCtor |
|
2888 * The builtin class constructor. |
|
2889 * @return Function |
|
2890 * The stringifier for the class. |
|
2891 */ |
|
2892 function createBuiltinStringifier(aCtor) { |
|
2893 return aObj => aCtor.prototype.toString.call(aObj.unsafeDereference()); |
|
2894 } |
|
2895 |
|
2896 /** |
|
2897 * Stringify a Debugger.Object-wrapped Error instance. |
|
2898 * |
|
2899 * @param Debugger.Object aObj |
|
2900 * The object to stringify. |
|
2901 * @return String |
|
2902 * The stringification of the object. |
|
2903 */ |
|
2904 function errorStringify(aObj) { |
|
2905 let name = DevToolsUtils.getProperty(aObj, "name"); |
|
2906 if (name === "" || name === undefined) { |
|
2907 name = aObj.class; |
|
2908 } else if (isObject(name)) { |
|
2909 name = stringify(name); |
|
2910 } |
|
2911 |
|
2912 let message = DevToolsUtils.getProperty(aObj, "message"); |
|
2913 if (isObject(message)) { |
|
2914 message = stringify(message); |
|
2915 } |
|
2916 |
|
2917 if (message === "" || message === undefined) { |
|
2918 return name; |
|
2919 } |
|
2920 return name + ": " + message; |
|
2921 } |
|
2922 |
|
2923 /** |
|
2924 * Stringify a Debugger.Object based on its class. |
|
2925 * |
|
2926 * @param Debugger.Object aObj |
|
2927 * The object to stringify. |
|
2928 * @return String |
|
2929 * The stringification for the object. |
|
2930 */ |
|
2931 function stringify(aObj) { |
|
2932 if (aObj.class == "DeadObject") { |
|
2933 const error = new Error("Dead object encountered."); |
|
2934 DevToolsUtils.reportException("stringify", error); |
|
2935 return "<dead object>"; |
|
2936 } |
|
2937 const stringifier = stringifiers[aObj.class] || stringifiers.Object; |
|
2938 return stringifier(aObj); |
|
2939 } |
|
2940 |
|
2941 // Used to prevent infinite recursion when an array is found inside itself. |
|
2942 let seen = null; |
|
2943 |
|
2944 let stringifiers = { |
|
2945 Error: errorStringify, |
|
2946 EvalError: errorStringify, |
|
2947 RangeError: errorStringify, |
|
2948 ReferenceError: errorStringify, |
|
2949 SyntaxError: errorStringify, |
|
2950 TypeError: errorStringify, |
|
2951 URIError: errorStringify, |
|
2952 Boolean: createBuiltinStringifier(Boolean), |
|
2953 Function: createBuiltinStringifier(Function), |
|
2954 Number: createBuiltinStringifier(Number), |
|
2955 RegExp: createBuiltinStringifier(RegExp), |
|
2956 String: createBuiltinStringifier(String), |
|
2957 Object: obj => "[object " + obj.class + "]", |
|
2958 Array: obj => { |
|
2959 // If we're at the top level then we need to create the Set for tracking |
|
2960 // previously stringified arrays. |
|
2961 const topLevel = !seen; |
|
2962 if (topLevel) { |
|
2963 seen = new Set(); |
|
2964 } else if (seen.has(obj)) { |
|
2965 return ""; |
|
2966 } |
|
2967 |
|
2968 seen.add(obj); |
|
2969 |
|
2970 const len = DevToolsUtils.getProperty(obj, "length"); |
|
2971 let string = ""; |
|
2972 |
|
2973 // The following check is only required because the debuggee could possibly |
|
2974 // be a Proxy and return any value. For normal objects, array.length is |
|
2975 // always a non-negative integer. |
|
2976 if (typeof len == "number" && len > 0) { |
|
2977 for (let i = 0; i < len; i++) { |
|
2978 const desc = obj.getOwnPropertyDescriptor(i); |
|
2979 if (desc) { |
|
2980 const { value } = desc; |
|
2981 if (value != null) { |
|
2982 string += isObject(value) ? stringify(value) : value; |
|
2983 } |
|
2984 } |
|
2985 |
|
2986 if (i < len - 1) { |
|
2987 string += ","; |
|
2988 } |
|
2989 } |
|
2990 } |
|
2991 |
|
2992 if (topLevel) { |
|
2993 seen = null; |
|
2994 } |
|
2995 |
|
2996 return string; |
|
2997 }, |
|
2998 DOMException: obj => { |
|
2999 const message = DevToolsUtils.getProperty(obj, "message") || "<no message>"; |
|
3000 const result = (+DevToolsUtils.getProperty(obj, "result")).toString(16); |
|
3001 const code = DevToolsUtils.getProperty(obj, "code"); |
|
3002 const name = DevToolsUtils.getProperty(obj, "name") || "<unknown>"; |
|
3003 |
|
3004 return '[Exception... "' + message + '" ' + |
|
3005 'code: "' + code +'" ' + |
|
3006 'nsresult: "0x' + result + ' (' + name + ')"]'; |
|
3007 } |
|
3008 }; |
|
3009 |
|
3010 /** |
|
3011 * Creates an actor for the specified object. |
|
3012 * |
|
3013 * @param aObj Debugger.Object |
|
3014 * The debuggee object. |
|
3015 * @param aThreadActor ThreadActor |
|
3016 * The parent thread actor for this object. |
|
3017 */ |
|
3018 function ObjectActor(aObj, aThreadActor) |
|
3019 { |
|
3020 dbg_assert(!aObj.optimizedOut, "Should not create object actors for optimized out values!"); |
|
3021 this.obj = aObj; |
|
3022 this.threadActor = aThreadActor; |
|
3023 } |
|
3024 |
|
3025 ObjectActor.prototype = { |
|
3026 actorPrefix: "obj", |
|
3027 |
|
3028 /** |
|
3029 * Returns a grip for this actor for returning in a protocol message. |
|
3030 */ |
|
3031 grip: function () { |
|
3032 this.threadActor._gripDepth++; |
|
3033 |
|
3034 let g = { |
|
3035 "type": "object", |
|
3036 "class": this.obj.class, |
|
3037 "actor": this.actorID, |
|
3038 "extensible": this.obj.isExtensible(), |
|
3039 "frozen": this.obj.isFrozen(), |
|
3040 "sealed": this.obj.isSealed() |
|
3041 }; |
|
3042 |
|
3043 if (this.obj.class != "DeadObject") { |
|
3044 let raw = Cu.unwaiveXrays(this.obj.unsafeDereference()); |
|
3045 if (!DevToolsUtils.isSafeJSObject(raw)) { |
|
3046 raw = null; |
|
3047 } |
|
3048 |
|
3049 let previewers = DebuggerServer.ObjectActorPreviewers[this.obj.class] || |
|
3050 DebuggerServer.ObjectActorPreviewers.Object; |
|
3051 for (let fn of previewers) { |
|
3052 try { |
|
3053 if (fn(this, g, raw)) { |
|
3054 break; |
|
3055 } |
|
3056 } catch (e) { |
|
3057 DevToolsUtils.reportException("ObjectActor.prototype.grip previewer function", e); |
|
3058 } |
|
3059 } |
|
3060 } |
|
3061 |
|
3062 this.threadActor._gripDepth--; |
|
3063 return g; |
|
3064 }, |
|
3065 |
|
3066 /** |
|
3067 * Releases this actor from the pool. |
|
3068 */ |
|
3069 release: function () { |
|
3070 if (this.registeredPool.objectActors) { |
|
3071 this.registeredPool.objectActors.delete(this.obj); |
|
3072 } |
|
3073 this.registeredPool.removeActor(this); |
|
3074 }, |
|
3075 |
|
3076 /** |
|
3077 * Handle a protocol request to provide the definition site of this function |
|
3078 * object. |
|
3079 * |
|
3080 * @param aRequest object |
|
3081 * The protocol request object. |
|
3082 */ |
|
3083 onDefinitionSite: function OA_onDefinitionSite(aRequest) { |
|
3084 if (this.obj.class != "Function") { |
|
3085 return { |
|
3086 from: this.actorID, |
|
3087 error: "objectNotFunction", |
|
3088 message: this.actorID + " is not a function." |
|
3089 }; |
|
3090 } |
|
3091 |
|
3092 if (!this.obj.script) { |
|
3093 return { |
|
3094 from: this.actorID, |
|
3095 error: "noScript", |
|
3096 message: this.actorID + " has no Debugger.Script" |
|
3097 }; |
|
3098 } |
|
3099 |
|
3100 const generatedLocation = { |
|
3101 url: this.obj.script.url, |
|
3102 line: this.obj.script.startLine, |
|
3103 // TODO bug 901138: use Debugger.Script.prototype.startColumn. |
|
3104 column: 0 |
|
3105 }; |
|
3106 |
|
3107 return this.threadActor.sources.getOriginalLocation(generatedLocation) |
|
3108 .then(({ url, line, column }) => { |
|
3109 return { |
|
3110 from: this.actorID, |
|
3111 url: url, |
|
3112 line: line, |
|
3113 column: column |
|
3114 }; |
|
3115 }); |
|
3116 }, |
|
3117 |
|
3118 /** |
|
3119 * Handle a protocol request to provide the names of the properties defined on |
|
3120 * the object and not its prototype. |
|
3121 * |
|
3122 * @param aRequest object |
|
3123 * The protocol request object. |
|
3124 */ |
|
3125 onOwnPropertyNames: function (aRequest) { |
|
3126 return { from: this.actorID, |
|
3127 ownPropertyNames: this.obj.getOwnPropertyNames() }; |
|
3128 }, |
|
3129 |
|
3130 /** |
|
3131 * Handle a protocol request to provide the prototype and own properties of |
|
3132 * the object. |
|
3133 * |
|
3134 * @param aRequest object |
|
3135 * The protocol request object. |
|
3136 */ |
|
3137 onPrototypeAndProperties: function (aRequest) { |
|
3138 let ownProperties = Object.create(null); |
|
3139 let names; |
|
3140 try { |
|
3141 names = this.obj.getOwnPropertyNames(); |
|
3142 } catch (ex) { |
|
3143 // The above can throw if this.obj points to a dead object. |
|
3144 // TODO: we should use Cu.isDeadWrapper() - see bug 885800. |
|
3145 return { from: this.actorID, |
|
3146 prototype: this.threadActor.createValueGrip(null), |
|
3147 ownProperties: ownProperties, |
|
3148 safeGetterValues: Object.create(null) }; |
|
3149 } |
|
3150 for (let name of names) { |
|
3151 ownProperties[name] = this._propertyDescriptor(name); |
|
3152 } |
|
3153 return { from: this.actorID, |
|
3154 prototype: this.threadActor.createValueGrip(this.obj.proto), |
|
3155 ownProperties: ownProperties, |
|
3156 safeGetterValues: this._findSafeGetterValues(ownProperties) }; |
|
3157 }, |
|
3158 |
|
3159 /** |
|
3160 * Find the safe getter values for the current Debugger.Object, |this.obj|. |
|
3161 * |
|
3162 * @private |
|
3163 * @param object aOwnProperties |
|
3164 * The object that holds the list of known ownProperties for |
|
3165 * |this.obj|. |
|
3166 * @param number [aLimit=0] |
|
3167 * Optional limit of getter values to find. |
|
3168 * @return object |
|
3169 * An object that maps property names to safe getter descriptors as |
|
3170 * defined by the remote debugging protocol. |
|
3171 */ |
|
3172 _findSafeGetterValues: function (aOwnProperties, aLimit = 0) |
|
3173 { |
|
3174 let safeGetterValues = Object.create(null); |
|
3175 let obj = this.obj; |
|
3176 let level = 0, i = 0; |
|
3177 |
|
3178 while (obj) { |
|
3179 let getters = this._findSafeGetters(obj); |
|
3180 for (let name of getters) { |
|
3181 // Avoid overwriting properties from prototypes closer to this.obj. Also |
|
3182 // avoid providing safeGetterValues from prototypes if property |name| |
|
3183 // is already defined as an own property. |
|
3184 if (name in safeGetterValues || |
|
3185 (obj != this.obj && name in aOwnProperties)) { |
|
3186 continue; |
|
3187 } |
|
3188 |
|
3189 let desc = null, getter = null; |
|
3190 try { |
|
3191 desc = obj.getOwnPropertyDescriptor(name); |
|
3192 getter = desc.get; |
|
3193 } catch (ex) { |
|
3194 // The above can throw if the cache becomes stale. |
|
3195 } |
|
3196 if (!getter) { |
|
3197 obj._safeGetters = null; |
|
3198 continue; |
|
3199 } |
|
3200 |
|
3201 let result = getter.call(this.obj); |
|
3202 if (result && !("throw" in result)) { |
|
3203 let getterValue = undefined; |
|
3204 if ("return" in result) { |
|
3205 getterValue = result.return; |
|
3206 } else if ("yield" in result) { |
|
3207 getterValue = result.yield; |
|
3208 } |
|
3209 // WebIDL attributes specified with the LenientThis extended attribute |
|
3210 // return undefined and should be ignored. |
|
3211 if (getterValue !== undefined) { |
|
3212 safeGetterValues[name] = { |
|
3213 getterValue: this.threadActor.createValueGrip(getterValue), |
|
3214 getterPrototypeLevel: level, |
|
3215 enumerable: desc.enumerable, |
|
3216 writable: level == 0 ? desc.writable : true, |
|
3217 }; |
|
3218 if (aLimit && ++i == aLimit) { |
|
3219 break; |
|
3220 } |
|
3221 } |
|
3222 } |
|
3223 } |
|
3224 if (aLimit && i == aLimit) { |
|
3225 break; |
|
3226 } |
|
3227 |
|
3228 obj = obj.proto; |
|
3229 level++; |
|
3230 } |
|
3231 |
|
3232 return safeGetterValues; |
|
3233 }, |
|
3234 |
|
3235 /** |
|
3236 * Find the safe getters for a given Debugger.Object. Safe getters are native |
|
3237 * getters which are safe to execute. |
|
3238 * |
|
3239 * @private |
|
3240 * @param Debugger.Object aObject |
|
3241 * The Debugger.Object where you want to find safe getters. |
|
3242 * @return Set |
|
3243 * A Set of names of safe getters. This result is cached for each |
|
3244 * Debugger.Object. |
|
3245 */ |
|
3246 _findSafeGetters: function (aObject) |
|
3247 { |
|
3248 if (aObject._safeGetters) { |
|
3249 return aObject._safeGetters; |
|
3250 } |
|
3251 |
|
3252 let getters = new Set(); |
|
3253 let names = []; |
|
3254 try { |
|
3255 names = aObject.getOwnPropertyNames() |
|
3256 } catch (ex) { |
|
3257 // Calling getOwnPropertyNames() on some wrapped native prototypes is not |
|
3258 // allowed: "cannot modify properties of a WrappedNative". See bug 952093. |
|
3259 } |
|
3260 |
|
3261 for (let name of names) { |
|
3262 let desc = null; |
|
3263 try { |
|
3264 desc = aObject.getOwnPropertyDescriptor(name); |
|
3265 } catch (e) { |
|
3266 // Calling getOwnPropertyDescriptor on wrapped native prototypes is not |
|
3267 // allowed (bug 560072). |
|
3268 } |
|
3269 if (!desc || desc.value !== undefined || !("get" in desc)) { |
|
3270 continue; |
|
3271 } |
|
3272 |
|
3273 if (DevToolsUtils.hasSafeGetter(desc)) { |
|
3274 getters.add(name); |
|
3275 } |
|
3276 } |
|
3277 |
|
3278 aObject._safeGetters = getters; |
|
3279 return getters; |
|
3280 }, |
|
3281 |
|
3282 /** |
|
3283 * Handle a protocol request to provide the prototype of the object. |
|
3284 * |
|
3285 * @param aRequest object |
|
3286 * The protocol request object. |
|
3287 */ |
|
3288 onPrototype: function (aRequest) { |
|
3289 return { from: this.actorID, |
|
3290 prototype: this.threadActor.createValueGrip(this.obj.proto) }; |
|
3291 }, |
|
3292 |
|
3293 /** |
|
3294 * Handle a protocol request to provide the property descriptor of the |
|
3295 * object's specified property. |
|
3296 * |
|
3297 * @param aRequest object |
|
3298 * The protocol request object. |
|
3299 */ |
|
3300 onProperty: function (aRequest) { |
|
3301 if (!aRequest.name) { |
|
3302 return { error: "missingParameter", |
|
3303 message: "no property name was specified" }; |
|
3304 } |
|
3305 |
|
3306 return { from: this.actorID, |
|
3307 descriptor: this._propertyDescriptor(aRequest.name) }; |
|
3308 }, |
|
3309 |
|
3310 /** |
|
3311 * Handle a protocol request to provide the display string for the object. |
|
3312 * |
|
3313 * @param aRequest object |
|
3314 * The protocol request object. |
|
3315 */ |
|
3316 onDisplayString: function (aRequest) { |
|
3317 const string = stringify(this.obj); |
|
3318 return { from: this.actorID, |
|
3319 displayString: this.threadActor.createValueGrip(string) }; |
|
3320 }, |
|
3321 |
|
3322 /** |
|
3323 * A helper method that creates a property descriptor for the provided object, |
|
3324 * properly formatted for sending in a protocol response. |
|
3325 * |
|
3326 * @private |
|
3327 * @param string aName |
|
3328 * The property that the descriptor is generated for. |
|
3329 * @param boolean [aOnlyEnumerable] |
|
3330 * Optional: true if you want a descriptor only for an enumerable |
|
3331 * property, false otherwise. |
|
3332 * @return object|undefined |
|
3333 * The property descriptor, or undefined if this is not an enumerable |
|
3334 * property and aOnlyEnumerable=true. |
|
3335 */ |
|
3336 _propertyDescriptor: function (aName, aOnlyEnumerable) { |
|
3337 let desc; |
|
3338 try { |
|
3339 desc = this.obj.getOwnPropertyDescriptor(aName); |
|
3340 } catch (e) { |
|
3341 // Calling getOwnPropertyDescriptor on wrapped native prototypes is not |
|
3342 // allowed (bug 560072). Inform the user with a bogus, but hopefully |
|
3343 // explanatory, descriptor. |
|
3344 return { |
|
3345 configurable: false, |
|
3346 writable: false, |
|
3347 enumerable: false, |
|
3348 value: e.name |
|
3349 }; |
|
3350 } |
|
3351 |
|
3352 if (!desc || aOnlyEnumerable && !desc.enumerable) { |
|
3353 return undefined; |
|
3354 } |
|
3355 |
|
3356 let retval = { |
|
3357 configurable: desc.configurable, |
|
3358 enumerable: desc.enumerable |
|
3359 }; |
|
3360 |
|
3361 if ("value" in desc) { |
|
3362 retval.writable = desc.writable; |
|
3363 retval.value = this.threadActor.createValueGrip(desc.value); |
|
3364 } else { |
|
3365 if ("get" in desc) { |
|
3366 retval.get = this.threadActor.createValueGrip(desc.get); |
|
3367 } |
|
3368 if ("set" in desc) { |
|
3369 retval.set = this.threadActor.createValueGrip(desc.set); |
|
3370 } |
|
3371 } |
|
3372 return retval; |
|
3373 }, |
|
3374 |
|
3375 /** |
|
3376 * Handle a protocol request to provide the source code of a function. |
|
3377 * |
|
3378 * @param aRequest object |
|
3379 * The protocol request object. |
|
3380 */ |
|
3381 onDecompile: function (aRequest) { |
|
3382 if (this.obj.class !== "Function") { |
|
3383 return { error: "objectNotFunction", |
|
3384 message: "decompile request is only valid for object grips " + |
|
3385 "with a 'Function' class." }; |
|
3386 } |
|
3387 |
|
3388 return { from: this.actorID, |
|
3389 decompiledCode: this.obj.decompile(!!aRequest.pretty) }; |
|
3390 }, |
|
3391 |
|
3392 /** |
|
3393 * Handle a protocol request to provide the parameters of a function. |
|
3394 * |
|
3395 * @param aRequest object |
|
3396 * The protocol request object. |
|
3397 */ |
|
3398 onParameterNames: function (aRequest) { |
|
3399 if (this.obj.class !== "Function") { |
|
3400 return { error: "objectNotFunction", |
|
3401 message: "'parameterNames' request is only valid for object " + |
|
3402 "grips with a 'Function' class." }; |
|
3403 } |
|
3404 |
|
3405 return { parameterNames: this.obj.parameterNames }; |
|
3406 }, |
|
3407 |
|
3408 /** |
|
3409 * Handle a protocol request to release a thread-lifetime grip. |
|
3410 * |
|
3411 * @param aRequest object |
|
3412 * The protocol request object. |
|
3413 */ |
|
3414 onRelease: function (aRequest) { |
|
3415 this.release(); |
|
3416 return {}; |
|
3417 }, |
|
3418 |
|
3419 /** |
|
3420 * Handle a protocol request to provide the lexical scope of a function. |
|
3421 * |
|
3422 * @param aRequest object |
|
3423 * The protocol request object. |
|
3424 */ |
|
3425 onScope: function (aRequest) { |
|
3426 if (this.obj.class !== "Function") { |
|
3427 return { error: "objectNotFunction", |
|
3428 message: "scope request is only valid for object grips with a" + |
|
3429 " 'Function' class." }; |
|
3430 } |
|
3431 |
|
3432 let envActor = this.threadActor.createEnvironmentActor(this.obj.environment, |
|
3433 this.registeredPool); |
|
3434 if (!envActor) { |
|
3435 return { error: "notDebuggee", |
|
3436 message: "cannot access the environment of this function." }; |
|
3437 } |
|
3438 |
|
3439 return { from: this.actorID, scope: envActor.form() }; |
|
3440 } |
|
3441 }; |
|
3442 |
|
3443 ObjectActor.prototype.requestTypes = { |
|
3444 "definitionSite": ObjectActor.prototype.onDefinitionSite, |
|
3445 "parameterNames": ObjectActor.prototype.onParameterNames, |
|
3446 "prototypeAndProperties": ObjectActor.prototype.onPrototypeAndProperties, |
|
3447 "prototype": ObjectActor.prototype.onPrototype, |
|
3448 "property": ObjectActor.prototype.onProperty, |
|
3449 "displayString": ObjectActor.prototype.onDisplayString, |
|
3450 "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames, |
|
3451 "decompile": ObjectActor.prototype.onDecompile, |
|
3452 "release": ObjectActor.prototype.onRelease, |
|
3453 "scope": ObjectActor.prototype.onScope, |
|
3454 }; |
|
3455 |
|
3456 |
|
3457 /** |
|
3458 * Functions for adding information to ObjectActor grips for the purpose of |
|
3459 * having customized output. This object holds arrays mapped by |
|
3460 * Debugger.Object.prototype.class. |
|
3461 * |
|
3462 * In each array you can add functions that take two |
|
3463 * arguments: |
|
3464 * - the ObjectActor instance to make a preview for, |
|
3465 * - the grip object being prepared for the client, |
|
3466 * - the raw JS object after calling Debugger.Object.unsafeDereference(). This |
|
3467 * argument is only provided if the object is safe for reading properties and |
|
3468 * executing methods. See DevToolsUtils.isSafeJSObject(). |
|
3469 * |
|
3470 * Functions must return false if they cannot provide preview |
|
3471 * information for the debugger object, or true otherwise. |
|
3472 */ |
|
3473 DebuggerServer.ObjectActorPreviewers = { |
|
3474 String: [function({obj, threadActor}, aGrip) { |
|
3475 let result = genericObjectPreviewer("String", String, obj, threadActor); |
|
3476 if (result) { |
|
3477 let length = DevToolsUtils.getProperty(obj, "length"); |
|
3478 if (typeof length != "number") { |
|
3479 return false; |
|
3480 } |
|
3481 |
|
3482 aGrip.displayString = result.value; |
|
3483 return true; |
|
3484 } |
|
3485 |
|
3486 return true; |
|
3487 }], |
|
3488 |
|
3489 Boolean: [function({obj, threadActor}, aGrip) { |
|
3490 let result = genericObjectPreviewer("Boolean", Boolean, obj, threadActor); |
|
3491 if (result) { |
|
3492 aGrip.preview = result; |
|
3493 return true; |
|
3494 } |
|
3495 |
|
3496 return false; |
|
3497 }], |
|
3498 |
|
3499 Number: [function({obj, threadActor}, aGrip) { |
|
3500 let result = genericObjectPreviewer("Number", Number, obj, threadActor); |
|
3501 if (result) { |
|
3502 aGrip.preview = result; |
|
3503 return true; |
|
3504 } |
|
3505 |
|
3506 return false; |
|
3507 }], |
|
3508 |
|
3509 Function: [function({obj, threadActor}, aGrip) { |
|
3510 if (obj.name) { |
|
3511 aGrip.name = obj.name; |
|
3512 } |
|
3513 |
|
3514 if (obj.displayName) { |
|
3515 aGrip.displayName = obj.displayName.substr(0, 500); |
|
3516 } |
|
3517 |
|
3518 if (obj.parameterNames) { |
|
3519 aGrip.parameterNames = obj.parameterNames; |
|
3520 } |
|
3521 |
|
3522 // Check if the developer has added a de-facto standard displayName |
|
3523 // property for us to use. |
|
3524 let userDisplayName; |
|
3525 try { |
|
3526 userDisplayName = obj.getOwnPropertyDescriptor("displayName"); |
|
3527 } catch (e) { |
|
3528 // Calling getOwnPropertyDescriptor with displayName might throw |
|
3529 // with "permission denied" errors for some functions. |
|
3530 dumpn(e); |
|
3531 } |
|
3532 |
|
3533 if (userDisplayName && typeof userDisplayName.value == "string" && |
|
3534 userDisplayName.value) { |
|
3535 aGrip.userDisplayName = threadActor.createValueGrip(userDisplayName.value); |
|
3536 } |
|
3537 |
|
3538 return true; |
|
3539 }], |
|
3540 |
|
3541 RegExp: [function({obj, threadActor}, aGrip) { |
|
3542 // Avoid having any special preview for the RegExp.prototype itself. |
|
3543 if (!obj.proto || obj.proto.class != "RegExp") { |
|
3544 return false; |
|
3545 } |
|
3546 |
|
3547 let str = RegExp.prototype.toString.call(obj.unsafeDereference()); |
|
3548 aGrip.displayString = threadActor.createValueGrip(str); |
|
3549 return true; |
|
3550 }], |
|
3551 |
|
3552 Date: [function({obj, threadActor}, aGrip) { |
|
3553 if (!obj.proto || obj.proto.class != "Date") { |
|
3554 return false; |
|
3555 } |
|
3556 |
|
3557 let time = Date.prototype.getTime.call(obj.unsafeDereference()); |
|
3558 |
|
3559 aGrip.preview = { |
|
3560 timestamp: threadActor.createValueGrip(time), |
|
3561 }; |
|
3562 return true; |
|
3563 }], |
|
3564 |
|
3565 Array: [function({obj, threadActor}, aGrip) { |
|
3566 let length = DevToolsUtils.getProperty(obj, "length"); |
|
3567 if (typeof length != "number") { |
|
3568 return false; |
|
3569 } |
|
3570 |
|
3571 aGrip.preview = { |
|
3572 kind: "ArrayLike", |
|
3573 length: length, |
|
3574 }; |
|
3575 |
|
3576 if (threadActor._gripDepth > 1) { |
|
3577 return true; |
|
3578 } |
|
3579 |
|
3580 let raw = obj.unsafeDereference(); |
|
3581 let items = aGrip.preview.items = []; |
|
3582 |
|
3583 for (let [i, value] of Array.prototype.entries.call(raw)) { |
|
3584 if (Object.hasOwnProperty.call(raw, i)) { |
|
3585 value = makeDebuggeeValueIfNeeded(obj, value); |
|
3586 items.push(threadActor.createValueGrip(value)); |
|
3587 } else { |
|
3588 items.push(null); |
|
3589 } |
|
3590 |
|
3591 if (items.length == OBJECT_PREVIEW_MAX_ITEMS) { |
|
3592 break; |
|
3593 } |
|
3594 } |
|
3595 |
|
3596 return true; |
|
3597 }], // Array |
|
3598 |
|
3599 Set: [function({obj, threadActor}, aGrip) { |
|
3600 let size = DevToolsUtils.getProperty(obj, "size"); |
|
3601 if (typeof size != "number") { |
|
3602 return false; |
|
3603 } |
|
3604 |
|
3605 aGrip.preview = { |
|
3606 kind: "ArrayLike", |
|
3607 length: size, |
|
3608 }; |
|
3609 |
|
3610 // Avoid recursive object grips. |
|
3611 if (threadActor._gripDepth > 1) { |
|
3612 return true; |
|
3613 } |
|
3614 |
|
3615 let raw = obj.unsafeDereference(); |
|
3616 let items = aGrip.preview.items = []; |
|
3617 for (let item of Set.prototype.values.call(raw)) { |
|
3618 item = makeDebuggeeValueIfNeeded(obj, item); |
|
3619 items.push(threadActor.createValueGrip(item)); |
|
3620 if (items.length == OBJECT_PREVIEW_MAX_ITEMS) { |
|
3621 break; |
|
3622 } |
|
3623 } |
|
3624 |
|
3625 return true; |
|
3626 }], // Set |
|
3627 |
|
3628 Map: [function({obj, threadActor}, aGrip) { |
|
3629 let size = DevToolsUtils.getProperty(obj, "size"); |
|
3630 if (typeof size != "number") { |
|
3631 return false; |
|
3632 } |
|
3633 |
|
3634 aGrip.preview = { |
|
3635 kind: "MapLike", |
|
3636 size: size, |
|
3637 }; |
|
3638 |
|
3639 if (threadActor._gripDepth > 1) { |
|
3640 return true; |
|
3641 } |
|
3642 |
|
3643 let raw = obj.unsafeDereference(); |
|
3644 let entries = aGrip.preview.entries = []; |
|
3645 for (let [key, value] of Map.prototype.entries.call(raw)) { |
|
3646 key = makeDebuggeeValueIfNeeded(obj, key); |
|
3647 value = makeDebuggeeValueIfNeeded(obj, value); |
|
3648 entries.push([threadActor.createValueGrip(key), |
|
3649 threadActor.createValueGrip(value)]); |
|
3650 if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { |
|
3651 break; |
|
3652 } |
|
3653 } |
|
3654 |
|
3655 return true; |
|
3656 }], // Map |
|
3657 |
|
3658 DOMStringMap: [function({obj, threadActor}, aGrip, aRawObj) { |
|
3659 if (!aRawObj) { |
|
3660 return false; |
|
3661 } |
|
3662 |
|
3663 let keys = obj.getOwnPropertyNames(); |
|
3664 aGrip.preview = { |
|
3665 kind: "MapLike", |
|
3666 size: keys.length, |
|
3667 }; |
|
3668 |
|
3669 if (threadActor._gripDepth > 1) { |
|
3670 return true; |
|
3671 } |
|
3672 |
|
3673 let entries = aGrip.preview.entries = []; |
|
3674 for (let key of keys) { |
|
3675 let value = makeDebuggeeValueIfNeeded(obj, aRawObj[key]); |
|
3676 entries.push([key, threadActor.createValueGrip(value)]); |
|
3677 if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) { |
|
3678 break; |
|
3679 } |
|
3680 } |
|
3681 |
|
3682 return true; |
|
3683 }], // DOMStringMap |
|
3684 }; // DebuggerServer.ObjectActorPreviewers |
|
3685 |
|
3686 /** |
|
3687 * Generic previewer for "simple" classes like String, Number and Boolean. |
|
3688 * |
|
3689 * @param string aClassName |
|
3690 * Class name to expect. |
|
3691 * @param object aClass |
|
3692 * The class to expect, eg. String. The valueOf() method of the class is |
|
3693 * invoked on the given object. |
|
3694 * @param Debugger.Object aObj |
|
3695 * The debugger object we need to preview. |
|
3696 * @param object aThreadActor |
|
3697 * The thread actor to use to create a value grip. |
|
3698 * @return object|null |
|
3699 * An object with one property, "value", which holds the value grip that |
|
3700 * represents the given object. Null is returned if we cant preview the |
|
3701 * object. |
|
3702 */ |
|
3703 function genericObjectPreviewer(aClassName, aClass, aObj, aThreadActor) { |
|
3704 if (!aObj.proto || aObj.proto.class != aClassName) { |
|
3705 return null; |
|
3706 } |
|
3707 |
|
3708 let raw = aObj.unsafeDereference(); |
|
3709 let v = null; |
|
3710 try { |
|
3711 v = aClass.prototype.valueOf.call(raw); |
|
3712 } catch (ex) { |
|
3713 // valueOf() can throw if the raw JS object is "misbehaved". |
|
3714 return null; |
|
3715 } |
|
3716 |
|
3717 if (v !== null) { |
|
3718 v = aThreadActor.createValueGrip(makeDebuggeeValueIfNeeded(aObj, v)); |
|
3719 return { value: v }; |
|
3720 } |
|
3721 |
|
3722 return null; |
|
3723 } |
|
3724 |
|
3725 // Preview functions that do not rely on the object class. |
|
3726 DebuggerServer.ObjectActorPreviewers.Object = [ |
|
3727 function TypedArray({obj, threadActor}, aGrip) { |
|
3728 if (TYPED_ARRAY_CLASSES.indexOf(obj.class) == -1) { |
|
3729 return false; |
|
3730 } |
|
3731 |
|
3732 let length = DevToolsUtils.getProperty(obj, "length"); |
|
3733 if (typeof length != "number") { |
|
3734 return false; |
|
3735 } |
|
3736 |
|
3737 aGrip.preview = { |
|
3738 kind: "ArrayLike", |
|
3739 length: length, |
|
3740 }; |
|
3741 |
|
3742 if (threadActor._gripDepth > 1) { |
|
3743 return true; |
|
3744 } |
|
3745 |
|
3746 let raw = obj.unsafeDereference(); |
|
3747 let global = Cu.getGlobalForObject(DebuggerServer); |
|
3748 let classProto = global[obj.class].prototype; |
|
3749 let safeView = classProto.subarray.call(raw, 0, OBJECT_PREVIEW_MAX_ITEMS); |
|
3750 let items = aGrip.preview.items = []; |
|
3751 for (let i = 0; i < safeView.length; i++) { |
|
3752 items.push(safeView[i]); |
|
3753 } |
|
3754 |
|
3755 return true; |
|
3756 }, |
|
3757 |
|
3758 function Error({obj, threadActor}, aGrip) { |
|
3759 switch (obj.class) { |
|
3760 case "Error": |
|
3761 case "EvalError": |
|
3762 case "RangeError": |
|
3763 case "ReferenceError": |
|
3764 case "SyntaxError": |
|
3765 case "TypeError": |
|
3766 case "URIError": |
|
3767 let name = DevToolsUtils.getProperty(obj, "name"); |
|
3768 let msg = DevToolsUtils.getProperty(obj, "message"); |
|
3769 let stack = DevToolsUtils.getProperty(obj, "stack"); |
|
3770 let fileName = DevToolsUtils.getProperty(obj, "fileName"); |
|
3771 let lineNumber = DevToolsUtils.getProperty(obj, "lineNumber"); |
|
3772 let columnNumber = DevToolsUtils.getProperty(obj, "columnNumber"); |
|
3773 aGrip.preview = { |
|
3774 kind: "Error", |
|
3775 name: threadActor.createValueGrip(name), |
|
3776 message: threadActor.createValueGrip(msg), |
|
3777 stack: threadActor.createValueGrip(stack), |
|
3778 fileName: threadActor.createValueGrip(fileName), |
|
3779 lineNumber: threadActor.createValueGrip(lineNumber), |
|
3780 columnNumber: threadActor.createValueGrip(columnNumber), |
|
3781 }; |
|
3782 return true; |
|
3783 default: |
|
3784 return false; |
|
3785 } |
|
3786 }, |
|
3787 |
|
3788 function CSSMediaRule({obj, threadActor}, aGrip, aRawObj) { |
|
3789 if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSMediaRule)) { |
|
3790 return false; |
|
3791 } |
|
3792 aGrip.preview = { |
|
3793 kind: "ObjectWithText", |
|
3794 text: threadActor.createValueGrip(aRawObj.conditionText), |
|
3795 }; |
|
3796 return true; |
|
3797 }, |
|
3798 |
|
3799 function CSSStyleRule({obj, threadActor}, aGrip, aRawObj) { |
|
3800 if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSStyleRule)) { |
|
3801 return false; |
|
3802 } |
|
3803 aGrip.preview = { |
|
3804 kind: "ObjectWithText", |
|
3805 text: threadActor.createValueGrip(aRawObj.selectorText), |
|
3806 }; |
|
3807 return true; |
|
3808 }, |
|
3809 |
|
3810 function ObjectWithURL({obj, threadActor}, aGrip, aRawObj) { |
|
3811 if (!aRawObj || |
|
3812 !(aRawObj instanceof Ci.nsIDOMCSSImportRule || |
|
3813 aRawObj instanceof Ci.nsIDOMCSSStyleSheet || |
|
3814 aRawObj instanceof Ci.nsIDOMLocation || |
|
3815 aRawObj instanceof Ci.nsIDOMWindow)) { |
|
3816 return false; |
|
3817 } |
|
3818 |
|
3819 let url; |
|
3820 if (aRawObj instanceof Ci.nsIDOMWindow && aRawObj.location) { |
|
3821 url = aRawObj.location.href; |
|
3822 } else if (aRawObj.href) { |
|
3823 url = aRawObj.href; |
|
3824 } else { |
|
3825 return false; |
|
3826 } |
|
3827 |
|
3828 aGrip.preview = { |
|
3829 kind: "ObjectWithURL", |
|
3830 url: threadActor.createValueGrip(url), |
|
3831 }; |
|
3832 |
|
3833 return true; |
|
3834 }, |
|
3835 |
|
3836 function ArrayLike({obj, threadActor}, aGrip, aRawObj) { |
|
3837 if (!aRawObj || |
|
3838 obj.class != "DOMStringList" && |
|
3839 obj.class != "DOMTokenList" && |
|
3840 !(aRawObj instanceof Ci.nsIDOMMozNamedAttrMap || |
|
3841 aRawObj instanceof Ci.nsIDOMCSSRuleList || |
|
3842 aRawObj instanceof Ci.nsIDOMCSSValueList || |
|
3843 aRawObj instanceof Ci.nsIDOMFileList || |
|
3844 aRawObj instanceof Ci.nsIDOMFontFaceList || |
|
3845 aRawObj instanceof Ci.nsIDOMMediaList || |
|
3846 aRawObj instanceof Ci.nsIDOMNodeList || |
|
3847 aRawObj instanceof Ci.nsIDOMStyleSheetList)) { |
|
3848 return false; |
|
3849 } |
|
3850 |
|
3851 if (typeof aRawObj.length != "number") { |
|
3852 return false; |
|
3853 } |
|
3854 |
|
3855 aGrip.preview = { |
|
3856 kind: "ArrayLike", |
|
3857 length: aRawObj.length, |
|
3858 }; |
|
3859 |
|
3860 if (threadActor._gripDepth > 1) { |
|
3861 return true; |
|
3862 } |
|
3863 |
|
3864 let items = aGrip.preview.items = []; |
|
3865 |
|
3866 for (let i = 0; i < aRawObj.length && |
|
3867 items.length < OBJECT_PREVIEW_MAX_ITEMS; i++) { |
|
3868 let value = makeDebuggeeValueIfNeeded(obj, aRawObj[i]); |
|
3869 items.push(threadActor.createValueGrip(value)); |
|
3870 } |
|
3871 |
|
3872 return true; |
|
3873 }, // ArrayLike |
|
3874 |
|
3875 function CSSStyleDeclaration({obj, threadActor}, aGrip, aRawObj) { |
|
3876 if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSStyleDeclaration)) { |
|
3877 return false; |
|
3878 } |
|
3879 |
|
3880 aGrip.preview = { |
|
3881 kind: "MapLike", |
|
3882 size: aRawObj.length, |
|
3883 }; |
|
3884 |
|
3885 let entries = aGrip.preview.entries = []; |
|
3886 |
|
3887 for (let i = 0; i < OBJECT_PREVIEW_MAX_ITEMS && |
|
3888 i < aRawObj.length; i++) { |
|
3889 let prop = aRawObj[i]; |
|
3890 let value = aRawObj.getPropertyValue(prop); |
|
3891 entries.push([prop, threadActor.createValueGrip(value)]); |
|
3892 } |
|
3893 |
|
3894 return true; |
|
3895 }, |
|
3896 |
|
3897 function DOMNode({obj, threadActor}, aGrip, aRawObj) { |
|
3898 if (obj.class == "Object" || !aRawObj || !(aRawObj instanceof Ci.nsIDOMNode)) { |
|
3899 return false; |
|
3900 } |
|
3901 |
|
3902 let preview = aGrip.preview = { |
|
3903 kind: "DOMNode", |
|
3904 nodeType: aRawObj.nodeType, |
|
3905 nodeName: aRawObj.nodeName, |
|
3906 }; |
|
3907 |
|
3908 if (aRawObj instanceof Ci.nsIDOMDocument && aRawObj.location) { |
|
3909 preview.location = threadActor.createValueGrip(aRawObj.location.href); |
|
3910 } else if (aRawObj instanceof Ci.nsIDOMDocumentFragment) { |
|
3911 preview.childNodesLength = aRawObj.childNodes.length; |
|
3912 |
|
3913 if (threadActor._gripDepth < 2) { |
|
3914 preview.childNodes = []; |
|
3915 for (let node of aRawObj.childNodes) { |
|
3916 let actor = threadActor.createValueGrip(obj.makeDebuggeeValue(node)); |
|
3917 preview.childNodes.push(actor); |
|
3918 if (preview.childNodes.length == OBJECT_PREVIEW_MAX_ITEMS) { |
|
3919 break; |
|
3920 } |
|
3921 } |
|
3922 } |
|
3923 } else if (aRawObj instanceof Ci.nsIDOMElement) { |
|
3924 // Add preview for DOM element attributes. |
|
3925 if (aRawObj instanceof Ci.nsIDOMHTMLElement) { |
|
3926 preview.nodeName = preview.nodeName.toLowerCase(); |
|
3927 } |
|
3928 |
|
3929 let i = 0; |
|
3930 preview.attributes = {}; |
|
3931 preview.attributesLength = aRawObj.attributes.length; |
|
3932 for (let attr of aRawObj.attributes) { |
|
3933 preview.attributes[attr.nodeName] = threadActor.createValueGrip(attr.value); |
|
3934 if (++i == OBJECT_PREVIEW_MAX_ITEMS) { |
|
3935 break; |
|
3936 } |
|
3937 } |
|
3938 } else if (aRawObj instanceof Ci.nsIDOMAttr) { |
|
3939 preview.value = threadActor.createValueGrip(aRawObj.value); |
|
3940 } else if (aRawObj instanceof Ci.nsIDOMText || |
|
3941 aRawObj instanceof Ci.nsIDOMComment) { |
|
3942 preview.textContent = threadActor.createValueGrip(aRawObj.textContent); |
|
3943 } |
|
3944 |
|
3945 return true; |
|
3946 }, // DOMNode |
|
3947 |
|
3948 function DOMEvent({obj, threadActor}, aGrip, aRawObj) { |
|
3949 if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMEvent)) { |
|
3950 return false; |
|
3951 } |
|
3952 |
|
3953 let preview = aGrip.preview = { |
|
3954 kind: "DOMEvent", |
|
3955 type: aRawObj.type, |
|
3956 properties: Object.create(null), |
|
3957 }; |
|
3958 |
|
3959 if (threadActor._gripDepth < 2) { |
|
3960 let target = obj.makeDebuggeeValue(aRawObj.target); |
|
3961 preview.target = threadActor.createValueGrip(target); |
|
3962 } |
|
3963 |
|
3964 let props = []; |
|
3965 if (aRawObj instanceof Ci.nsIDOMMouseEvent) { |
|
3966 props.push("buttons", "clientX", "clientY", "layerX", "layerY"); |
|
3967 } else if (aRawObj instanceof Ci.nsIDOMKeyEvent) { |
|
3968 let modifiers = []; |
|
3969 if (aRawObj.altKey) { |
|
3970 modifiers.push("Alt"); |
|
3971 } |
|
3972 if (aRawObj.ctrlKey) { |
|
3973 modifiers.push("Control"); |
|
3974 } |
|
3975 if (aRawObj.metaKey) { |
|
3976 modifiers.push("Meta"); |
|
3977 } |
|
3978 if (aRawObj.shiftKey) { |
|
3979 modifiers.push("Shift"); |
|
3980 } |
|
3981 preview.eventKind = "key"; |
|
3982 preview.modifiers = modifiers; |
|
3983 |
|
3984 props.push("key", "charCode", "keyCode"); |
|
3985 } else if (aRawObj instanceof Ci.nsIDOMTransitionEvent || |
|
3986 aRawObj instanceof Ci.nsIDOMAnimationEvent) { |
|
3987 props.push("animationName", "pseudoElement"); |
|
3988 } else if (aRawObj instanceof Ci.nsIDOMClipboardEvent) { |
|
3989 props.push("clipboardData"); |
|
3990 } |
|
3991 |
|
3992 // Add event-specific properties. |
|
3993 for (let prop of props) { |
|
3994 let value = aRawObj[prop]; |
|
3995 if (value && (typeof value == "object" || typeof value == "function")) { |
|
3996 // Skip properties pointing to objects. |
|
3997 if (threadActor._gripDepth > 1) { |
|
3998 continue; |
|
3999 } |
|
4000 value = obj.makeDebuggeeValue(value); |
|
4001 } |
|
4002 preview.properties[prop] = threadActor.createValueGrip(value); |
|
4003 } |
|
4004 |
|
4005 // Add any properties we find on the event object. |
|
4006 if (!props.length) { |
|
4007 let i = 0; |
|
4008 for (let prop in aRawObj) { |
|
4009 let value = aRawObj[prop]; |
|
4010 if (prop == "target" || prop == "type" || value === null || |
|
4011 typeof value == "function") { |
|
4012 continue; |
|
4013 } |
|
4014 if (value && typeof value == "object") { |
|
4015 if (threadActor._gripDepth > 1) { |
|
4016 continue; |
|
4017 } |
|
4018 value = obj.makeDebuggeeValue(value); |
|
4019 } |
|
4020 preview.properties[prop] = threadActor.createValueGrip(value); |
|
4021 if (++i == OBJECT_PREVIEW_MAX_ITEMS) { |
|
4022 break; |
|
4023 } |
|
4024 } |
|
4025 } |
|
4026 |
|
4027 return true; |
|
4028 }, // DOMEvent |
|
4029 |
|
4030 function DOMException({obj, threadActor}, aGrip, aRawObj) { |
|
4031 if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMDOMException)) { |
|
4032 return false; |
|
4033 } |
|
4034 |
|
4035 aGrip.preview = { |
|
4036 kind: "DOMException", |
|
4037 name: threadActor.createValueGrip(aRawObj.name), |
|
4038 message: threadActor.createValueGrip(aRawObj.message), |
|
4039 code: threadActor.createValueGrip(aRawObj.code), |
|
4040 result: threadActor.createValueGrip(aRawObj.result), |
|
4041 filename: threadActor.createValueGrip(aRawObj.filename), |
|
4042 lineNumber: threadActor.createValueGrip(aRawObj.lineNumber), |
|
4043 columnNumber: threadActor.createValueGrip(aRawObj.columnNumber), |
|
4044 }; |
|
4045 |
|
4046 return true; |
|
4047 }, |
|
4048 |
|
4049 function GenericObject(aObjectActor, aGrip) { |
|
4050 let {obj, threadActor} = aObjectActor; |
|
4051 if (aGrip.preview || aGrip.displayString || threadActor._gripDepth > 1) { |
|
4052 return false; |
|
4053 } |
|
4054 |
|
4055 let i = 0, names = []; |
|
4056 let preview = aGrip.preview = { |
|
4057 kind: "Object", |
|
4058 ownProperties: Object.create(null), |
|
4059 }; |
|
4060 |
|
4061 try { |
|
4062 names = obj.getOwnPropertyNames(); |
|
4063 } catch (ex) { |
|
4064 // Calling getOwnPropertyNames() on some wrapped native prototypes is not |
|
4065 // allowed: "cannot modify properties of a WrappedNative". See bug 952093. |
|
4066 } |
|
4067 |
|
4068 preview.ownPropertiesLength = names.length; |
|
4069 |
|
4070 for (let name of names) { |
|
4071 let desc = aObjectActor._propertyDescriptor(name, true); |
|
4072 if (!desc) { |
|
4073 continue; |
|
4074 } |
|
4075 |
|
4076 preview.ownProperties[name] = desc; |
|
4077 if (++i == OBJECT_PREVIEW_MAX_ITEMS) { |
|
4078 break; |
|
4079 } |
|
4080 } |
|
4081 |
|
4082 if (i < OBJECT_PREVIEW_MAX_ITEMS) { |
|
4083 preview.safeGetterValues = aObjectActor. |
|
4084 _findSafeGetterValues(preview.ownProperties, |
|
4085 OBJECT_PREVIEW_MAX_ITEMS - i); |
|
4086 } |
|
4087 |
|
4088 return true; |
|
4089 }, // GenericObject |
|
4090 ]; // DebuggerServer.ObjectActorPreviewers.Object |
|
4091 |
|
4092 /** |
|
4093 * Creates a pause-scoped actor for the specified object. |
|
4094 * @see ObjectActor |
|
4095 */ |
|
4096 function PauseScopedObjectActor() |
|
4097 { |
|
4098 ObjectActor.apply(this, arguments); |
|
4099 } |
|
4100 |
|
4101 PauseScopedObjectActor.prototype = Object.create(PauseScopedActor.prototype); |
|
4102 |
|
4103 update(PauseScopedObjectActor.prototype, ObjectActor.prototype); |
|
4104 |
|
4105 update(PauseScopedObjectActor.prototype, { |
|
4106 constructor: PauseScopedObjectActor, |
|
4107 actorPrefix: "pausedobj", |
|
4108 |
|
4109 onOwnPropertyNames: |
|
4110 PauseScopedActor.withPaused(ObjectActor.prototype.onOwnPropertyNames), |
|
4111 |
|
4112 onPrototypeAndProperties: |
|
4113 PauseScopedActor.withPaused(ObjectActor.prototype.onPrototypeAndProperties), |
|
4114 |
|
4115 onPrototype: PauseScopedActor.withPaused(ObjectActor.prototype.onPrototype), |
|
4116 onProperty: PauseScopedActor.withPaused(ObjectActor.prototype.onProperty), |
|
4117 onDecompile: PauseScopedActor.withPaused(ObjectActor.prototype.onDecompile), |
|
4118 |
|
4119 onDisplayString: |
|
4120 PauseScopedActor.withPaused(ObjectActor.prototype.onDisplayString), |
|
4121 |
|
4122 onParameterNames: |
|
4123 PauseScopedActor.withPaused(ObjectActor.prototype.onParameterNames), |
|
4124 |
|
4125 /** |
|
4126 * Handle a protocol request to promote a pause-lifetime grip to a |
|
4127 * thread-lifetime grip. |
|
4128 * |
|
4129 * @param aRequest object |
|
4130 * The protocol request object. |
|
4131 */ |
|
4132 onThreadGrip: PauseScopedActor.withPaused(function (aRequest) { |
|
4133 this.threadActor.threadObjectGrip(this); |
|
4134 return {}; |
|
4135 }), |
|
4136 |
|
4137 /** |
|
4138 * Handle a protocol request to release a thread-lifetime grip. |
|
4139 * |
|
4140 * @param aRequest object |
|
4141 * The protocol request object. |
|
4142 */ |
|
4143 onRelease: PauseScopedActor.withPaused(function (aRequest) { |
|
4144 if (this.registeredPool !== this.threadActor.threadLifetimePool) { |
|
4145 return { error: "notReleasable", |
|
4146 message: "Only thread-lifetime actors can be released." }; |
|
4147 } |
|
4148 |
|
4149 this.release(); |
|
4150 return {}; |
|
4151 }), |
|
4152 }); |
|
4153 |
|
4154 update(PauseScopedObjectActor.prototype.requestTypes, { |
|
4155 "threadGrip": PauseScopedObjectActor.prototype.onThreadGrip, |
|
4156 }); |
|
4157 |
|
4158 |
|
4159 /** |
|
4160 * Creates an actor for the specied "very long" string. "Very long" is specified |
|
4161 * at the server's discretion. |
|
4162 * |
|
4163 * @param aString String |
|
4164 * The string. |
|
4165 */ |
|
4166 function LongStringActor(aString) |
|
4167 { |
|
4168 this.string = aString; |
|
4169 this.stringLength = aString.length; |
|
4170 } |
|
4171 |
|
4172 LongStringActor.prototype = { |
|
4173 |
|
4174 actorPrefix: "longString", |
|
4175 |
|
4176 disconnect: function () { |
|
4177 // Because longStringActors is not a weak map, we won't automatically leave |
|
4178 // it so we need to manually leave on disconnect so that we don't leak |
|
4179 // memory. |
|
4180 if (this.registeredPool && this.registeredPool.longStringActors) { |
|
4181 delete this.registeredPool.longStringActors[this.actorID]; |
|
4182 } |
|
4183 }, |
|
4184 |
|
4185 /** |
|
4186 * Returns a grip for this actor for returning in a protocol message. |
|
4187 */ |
|
4188 grip: function () { |
|
4189 return { |
|
4190 "type": "longString", |
|
4191 "initial": this.string.substring( |
|
4192 0, DebuggerServer.LONG_STRING_INITIAL_LENGTH), |
|
4193 "length": this.stringLength, |
|
4194 "actor": this.actorID |
|
4195 }; |
|
4196 }, |
|
4197 |
|
4198 /** |
|
4199 * Handle a request to extract part of this actor's string. |
|
4200 * |
|
4201 * @param aRequest object |
|
4202 * The protocol request object. |
|
4203 */ |
|
4204 onSubstring: function (aRequest) { |
|
4205 return { |
|
4206 "from": this.actorID, |
|
4207 "substring": this.string.substring(aRequest.start, aRequest.end) |
|
4208 }; |
|
4209 }, |
|
4210 |
|
4211 /** |
|
4212 * Handle a request to release this LongStringActor instance. |
|
4213 */ |
|
4214 onRelease: function () { |
|
4215 // TODO: also check if registeredPool === threadActor.threadLifetimePool |
|
4216 // when the web console moves aray from manually releasing pause-scoped |
|
4217 // actors. |
|
4218 if (this.registeredPool.longStringActors) { |
|
4219 delete this.registeredPool.longStringActors[this.actorID]; |
|
4220 } |
|
4221 this.registeredPool.removeActor(this); |
|
4222 return {}; |
|
4223 }, |
|
4224 }; |
|
4225 |
|
4226 LongStringActor.prototype.requestTypes = { |
|
4227 "substring": LongStringActor.prototype.onSubstring, |
|
4228 "release": LongStringActor.prototype.onRelease |
|
4229 }; |
|
4230 |
|
4231 |
|
4232 /** |
|
4233 * Creates an actor for the specified stack frame. |
|
4234 * |
|
4235 * @param aFrame Debugger.Frame |
|
4236 * The debuggee frame. |
|
4237 * @param aThreadActor ThreadActor |
|
4238 * The parent thread actor for this frame. |
|
4239 */ |
|
4240 function FrameActor(aFrame, aThreadActor) |
|
4241 { |
|
4242 this.frame = aFrame; |
|
4243 this.threadActor = aThreadActor; |
|
4244 } |
|
4245 |
|
4246 FrameActor.prototype = { |
|
4247 actorPrefix: "frame", |
|
4248 |
|
4249 /** |
|
4250 * A pool that contains frame-lifetime objects, like the environment. |
|
4251 */ |
|
4252 _frameLifetimePool: null, |
|
4253 get frameLifetimePool() { |
|
4254 if (!this._frameLifetimePool) { |
|
4255 this._frameLifetimePool = new ActorPool(this.conn); |
|
4256 this.conn.addActorPool(this._frameLifetimePool); |
|
4257 } |
|
4258 return this._frameLifetimePool; |
|
4259 }, |
|
4260 |
|
4261 /** |
|
4262 * Finalization handler that is called when the actor is being evicted from |
|
4263 * the pool. |
|
4264 */ |
|
4265 disconnect: function () { |
|
4266 this.conn.removeActorPool(this._frameLifetimePool); |
|
4267 this._frameLifetimePool = null; |
|
4268 }, |
|
4269 |
|
4270 /** |
|
4271 * Returns a frame form for use in a protocol message. |
|
4272 */ |
|
4273 form: function () { |
|
4274 let form = { actor: this.actorID, |
|
4275 type: this.frame.type }; |
|
4276 if (this.frame.type === "call") { |
|
4277 form.callee = this.threadActor.createValueGrip(this.frame.callee); |
|
4278 } |
|
4279 |
|
4280 if (this.frame.environment) { |
|
4281 let envActor = this.threadActor |
|
4282 .createEnvironmentActor(this.frame.environment, |
|
4283 this.frameLifetimePool); |
|
4284 form.environment = envActor.form(); |
|
4285 } |
|
4286 form.this = this.threadActor.createValueGrip(this.frame.this); |
|
4287 form.arguments = this._args(); |
|
4288 if (this.frame.script) { |
|
4289 form.where = getFrameLocation(this.frame); |
|
4290 } |
|
4291 |
|
4292 if (!this.frame.older) { |
|
4293 form.oldest = true; |
|
4294 } |
|
4295 |
|
4296 return form; |
|
4297 }, |
|
4298 |
|
4299 _args: function () { |
|
4300 if (!this.frame.arguments) { |
|
4301 return []; |
|
4302 } |
|
4303 |
|
4304 return [this.threadActor.createValueGrip(arg) |
|
4305 for each (arg in this.frame.arguments)]; |
|
4306 }, |
|
4307 |
|
4308 /** |
|
4309 * Handle a protocol request to pop this frame from the stack. |
|
4310 * |
|
4311 * @param aRequest object |
|
4312 * The protocol request object. |
|
4313 */ |
|
4314 onPop: function (aRequest) { |
|
4315 // TODO: remove this when Debugger.Frame.prototype.pop is implemented |
|
4316 if (typeof this.frame.pop != "function") { |
|
4317 return { error: "notImplemented", |
|
4318 message: "Popping frames is not yet implemented." }; |
|
4319 } |
|
4320 |
|
4321 while (this.frame != this.threadActor.dbg.getNewestFrame()) { |
|
4322 this.threadActor.dbg.getNewestFrame().pop(); |
|
4323 } |
|
4324 this.frame.pop(aRequest.completionValue); |
|
4325 |
|
4326 // TODO: return the watches property when frame pop watch actors are |
|
4327 // implemented. |
|
4328 return { from: this.actorID }; |
|
4329 } |
|
4330 }; |
|
4331 |
|
4332 FrameActor.prototype.requestTypes = { |
|
4333 "pop": FrameActor.prototype.onPop, |
|
4334 }; |
|
4335 |
|
4336 |
|
4337 /** |
|
4338 * Creates a BreakpointActor. BreakpointActors exist for the lifetime of their |
|
4339 * containing thread and are responsible for deleting breakpoints, handling |
|
4340 * breakpoint hits and associating breakpoints with scripts. |
|
4341 * |
|
4342 * @param ThreadActor aThreadActor |
|
4343 * The parent thread actor that contains this breakpoint. |
|
4344 * @param object aLocation |
|
4345 * The location of the breakpoint as specified in the protocol. |
|
4346 */ |
|
4347 function BreakpointActor(aThreadActor, { url, line, column, condition }) |
|
4348 { |
|
4349 this.scripts = []; |
|
4350 this.threadActor = aThreadActor; |
|
4351 this.location = { url: url, line: line, column: column }; |
|
4352 this.condition = condition; |
|
4353 } |
|
4354 |
|
4355 BreakpointActor.prototype = { |
|
4356 actorPrefix: "breakpoint", |
|
4357 condition: null, |
|
4358 |
|
4359 /** |
|
4360 * Called when this same breakpoint is added to another Debugger.Script |
|
4361 * instance, in the case of a page reload. |
|
4362 * |
|
4363 * @param aScript Debugger.Script |
|
4364 * The new source script on which the breakpoint has been set. |
|
4365 * @param ThreadActor aThreadActor |
|
4366 * The parent thread actor that contains this breakpoint. |
|
4367 */ |
|
4368 addScript: function (aScript, aThreadActor) { |
|
4369 this.threadActor = aThreadActor; |
|
4370 this.scripts.push(aScript); |
|
4371 }, |
|
4372 |
|
4373 /** |
|
4374 * Remove the breakpoints from associated scripts and clear the script cache. |
|
4375 */ |
|
4376 removeScripts: function () { |
|
4377 for (let script of this.scripts) { |
|
4378 script.clearBreakpoint(this); |
|
4379 } |
|
4380 this.scripts = []; |
|
4381 }, |
|
4382 |
|
4383 /** |
|
4384 * Check if this breakpoint has a condition that doesn't error and |
|
4385 * evaluates to true in aFrame |
|
4386 * |
|
4387 * @param aFrame Debugger.Frame |
|
4388 * The frame to evaluate the condition in |
|
4389 */ |
|
4390 isValidCondition: function(aFrame) { |
|
4391 if(!this.condition) { |
|
4392 return true; |
|
4393 } |
|
4394 var res = aFrame.eval(this.condition); |
|
4395 return res.return; |
|
4396 }, |
|
4397 |
|
4398 /** |
|
4399 * A function that the engine calls when a breakpoint has been hit. |
|
4400 * |
|
4401 * @param aFrame Debugger.Frame |
|
4402 * The stack frame that contained the breakpoint. |
|
4403 */ |
|
4404 hit: function (aFrame) { |
|
4405 // Don't pause if we are currently stepping (in or over) or the frame is |
|
4406 // black-boxed. |
|
4407 let { url } = this.threadActor.synchronize( |
|
4408 this.threadActor.sources.getOriginalLocation({ |
|
4409 url: this.location.url, |
|
4410 line: this.location.line, |
|
4411 column: this.location.column |
|
4412 })); |
|
4413 |
|
4414 if (this.threadActor.sources.isBlackBoxed(url) |
|
4415 || aFrame.onStep |
|
4416 || !this.isValidCondition(aFrame)) { |
|
4417 return undefined; |
|
4418 } |
|
4419 |
|
4420 let reason = {}; |
|
4421 if (this.threadActor._hiddenBreakpoints.has(this.actorID)) { |
|
4422 reason.type = "pauseOnDOMEvents"; |
|
4423 } else { |
|
4424 reason.type = "breakpoint"; |
|
4425 // TODO: add the rest of the breakpoints on that line (bug 676602). |
|
4426 reason.actors = [ this.actorID ]; |
|
4427 } |
|
4428 return this.threadActor._pauseAndRespond(aFrame, reason); |
|
4429 }, |
|
4430 |
|
4431 /** |
|
4432 * Handle a protocol request to remove this breakpoint. |
|
4433 * |
|
4434 * @param aRequest object |
|
4435 * The protocol request object. |
|
4436 */ |
|
4437 onDelete: function (aRequest) { |
|
4438 // Remove from the breakpoint store. |
|
4439 this.threadActor.breakpointStore.removeBreakpoint(this.location); |
|
4440 this.threadActor.threadLifetimePool.removeActor(this); |
|
4441 // Remove the actual breakpoint from the associated scripts. |
|
4442 this.removeScripts(); |
|
4443 return { from: this.actorID }; |
|
4444 } |
|
4445 }; |
|
4446 |
|
4447 BreakpointActor.prototype.requestTypes = { |
|
4448 "delete": BreakpointActor.prototype.onDelete |
|
4449 }; |
|
4450 |
|
4451 |
|
4452 /** |
|
4453 * Creates an EnvironmentActor. EnvironmentActors are responsible for listing |
|
4454 * the bindings introduced by a lexical environment and assigning new values to |
|
4455 * those identifier bindings. |
|
4456 * |
|
4457 * @param Debugger.Environment aEnvironment |
|
4458 * The lexical environment that will be used to create the actor. |
|
4459 * @param ThreadActor aThreadActor |
|
4460 * The parent thread actor that contains this environment. |
|
4461 */ |
|
4462 function EnvironmentActor(aEnvironment, aThreadActor) |
|
4463 { |
|
4464 this.obj = aEnvironment; |
|
4465 this.threadActor = aThreadActor; |
|
4466 } |
|
4467 |
|
4468 EnvironmentActor.prototype = { |
|
4469 actorPrefix: "environment", |
|
4470 |
|
4471 /** |
|
4472 * Return an environment form for use in a protocol message. |
|
4473 */ |
|
4474 form: function () { |
|
4475 let form = { actor: this.actorID }; |
|
4476 |
|
4477 // What is this environment's type? |
|
4478 if (this.obj.type == "declarative") { |
|
4479 form.type = this.obj.callee ? "function" : "block"; |
|
4480 } else { |
|
4481 form.type = this.obj.type; |
|
4482 } |
|
4483 |
|
4484 // Does this environment have a parent? |
|
4485 if (this.obj.parent) { |
|
4486 form.parent = (this.threadActor |
|
4487 .createEnvironmentActor(this.obj.parent, |
|
4488 this.registeredPool) |
|
4489 .form()); |
|
4490 } |
|
4491 |
|
4492 // Does this environment reflect the properties of an object as variables? |
|
4493 if (this.obj.type == "object" || this.obj.type == "with") { |
|
4494 form.object = this.threadActor.createValueGrip(this.obj.object); |
|
4495 } |
|
4496 |
|
4497 // Is this the environment created for a function call? |
|
4498 if (this.obj.callee) { |
|
4499 form.function = this.threadActor.createValueGrip(this.obj.callee); |
|
4500 } |
|
4501 |
|
4502 // Shall we list this environment's bindings? |
|
4503 if (this.obj.type == "declarative") { |
|
4504 form.bindings = this._bindings(); |
|
4505 } |
|
4506 |
|
4507 return form; |
|
4508 }, |
|
4509 |
|
4510 /** |
|
4511 * Return the identifier bindings object as required by the remote protocol |
|
4512 * specification. |
|
4513 */ |
|
4514 _bindings: function () { |
|
4515 let bindings = { arguments: [], variables: {} }; |
|
4516 |
|
4517 // TODO: this part should be removed in favor of the commented-out part |
|
4518 // below when getVariableDescriptor lands (bug 725815). |
|
4519 if (typeof this.obj.getVariable != "function") { |
|
4520 //if (typeof this.obj.getVariableDescriptor != "function") { |
|
4521 return bindings; |
|
4522 } |
|
4523 |
|
4524 let parameterNames; |
|
4525 if (this.obj.callee) { |
|
4526 parameterNames = this.obj.callee.parameterNames; |
|
4527 } |
|
4528 for each (let name in parameterNames) { |
|
4529 let arg = {}; |
|
4530 |
|
4531 let value = this.obj.getVariable(name); |
|
4532 // The slot is optimized out. |
|
4533 // FIXME: Need actual UI, bug 941287. |
|
4534 if (value && value.optimizedOut) { |
|
4535 continue; |
|
4536 } |
|
4537 |
|
4538 // TODO: this part should be removed in favor of the commented-out part |
|
4539 // below when getVariableDescriptor lands (bug 725815). |
|
4540 let desc = { |
|
4541 value: value, |
|
4542 configurable: false, |
|
4543 writable: true, |
|
4544 enumerable: true |
|
4545 }; |
|
4546 |
|
4547 // let desc = this.obj.getVariableDescriptor(name); |
|
4548 let descForm = { |
|
4549 enumerable: true, |
|
4550 configurable: desc.configurable |
|
4551 }; |
|
4552 if ("value" in desc) { |
|
4553 descForm.value = this.threadActor.createValueGrip(desc.value); |
|
4554 descForm.writable = desc.writable; |
|
4555 } else { |
|
4556 descForm.get = this.threadActor.createValueGrip(desc.get); |
|
4557 descForm.set = this.threadActor.createValueGrip(desc.set); |
|
4558 } |
|
4559 arg[name] = descForm; |
|
4560 bindings.arguments.push(arg); |
|
4561 } |
|
4562 |
|
4563 for each (let name in this.obj.names()) { |
|
4564 if (bindings.arguments.some(function exists(element) { |
|
4565 return !!element[name]; |
|
4566 })) { |
|
4567 continue; |
|
4568 } |
|
4569 |
|
4570 let value = this.obj.getVariable(name); |
|
4571 // The slot is optimized out or arguments on a dead scope. |
|
4572 // FIXME: Need actual UI, bug 941287. |
|
4573 if (value && (value.optimizedOut || value.missingArguments)) { |
|
4574 continue; |
|
4575 } |
|
4576 |
|
4577 // TODO: this part should be removed in favor of the commented-out part |
|
4578 // below when getVariableDescriptor lands. |
|
4579 let desc = { |
|
4580 value: value, |
|
4581 configurable: false, |
|
4582 writable: true, |
|
4583 enumerable: true |
|
4584 }; |
|
4585 |
|
4586 //let desc = this.obj.getVariableDescriptor(name); |
|
4587 let descForm = { |
|
4588 enumerable: true, |
|
4589 configurable: desc.configurable |
|
4590 }; |
|
4591 if ("value" in desc) { |
|
4592 descForm.value = this.threadActor.createValueGrip(desc.value); |
|
4593 descForm.writable = desc.writable; |
|
4594 } else { |
|
4595 descForm.get = this.threadActor.createValueGrip(desc.get); |
|
4596 descForm.set = this.threadActor.createValueGrip(desc.set); |
|
4597 } |
|
4598 bindings.variables[name] = descForm; |
|
4599 } |
|
4600 |
|
4601 return bindings; |
|
4602 }, |
|
4603 |
|
4604 /** |
|
4605 * Handle a protocol request to change the value of a variable bound in this |
|
4606 * lexical environment. |
|
4607 * |
|
4608 * @param aRequest object |
|
4609 * The protocol request object. |
|
4610 */ |
|
4611 onAssign: function (aRequest) { |
|
4612 // TODO: enable the commented-out part when getVariableDescriptor lands |
|
4613 // (bug 725815). |
|
4614 /*let desc = this.obj.getVariableDescriptor(aRequest.name); |
|
4615 |
|
4616 if (!desc.writable) { |
|
4617 return { error: "immutableBinding", |
|
4618 message: "Changing the value of an immutable binding is not " + |
|
4619 "allowed" }; |
|
4620 }*/ |
|
4621 |
|
4622 try { |
|
4623 this.obj.setVariable(aRequest.name, aRequest.value); |
|
4624 } catch (e if e instanceof Debugger.DebuggeeWouldRun) { |
|
4625 return { error: "threadWouldRun", |
|
4626 cause: e.cause ? e.cause : "setter", |
|
4627 message: "Assigning a value would cause the debuggee to run" }; |
|
4628 } |
|
4629 return { from: this.actorID }; |
|
4630 }, |
|
4631 |
|
4632 /** |
|
4633 * Handle a protocol request to fully enumerate the bindings introduced by the |
|
4634 * lexical environment. |
|
4635 * |
|
4636 * @param aRequest object |
|
4637 * The protocol request object. |
|
4638 */ |
|
4639 onBindings: function (aRequest) { |
|
4640 return { from: this.actorID, |
|
4641 bindings: this._bindings() }; |
|
4642 } |
|
4643 }; |
|
4644 |
|
4645 EnvironmentActor.prototype.requestTypes = { |
|
4646 "assign": EnvironmentActor.prototype.onAssign, |
|
4647 "bindings": EnvironmentActor.prototype.onBindings |
|
4648 }; |
|
4649 |
|
4650 /** |
|
4651 * Override the toString method in order to get more meaningful script output |
|
4652 * for debugging the debugger. |
|
4653 */ |
|
4654 Debugger.Script.prototype.toString = function() { |
|
4655 let output = ""; |
|
4656 if (this.url) { |
|
4657 output += this.url; |
|
4658 } |
|
4659 if (typeof this.startLine != "undefined") { |
|
4660 output += ":" + this.startLine; |
|
4661 if (this.lineCount && this.lineCount > 1) { |
|
4662 output += "-" + (this.startLine + this.lineCount - 1); |
|
4663 } |
|
4664 } |
|
4665 if (this.strictMode) { |
|
4666 output += ":strict"; |
|
4667 } |
|
4668 return output; |
|
4669 }; |
|
4670 |
|
4671 /** |
|
4672 * Helper property for quickly getting to the line number a stack frame is |
|
4673 * currently paused at. |
|
4674 */ |
|
4675 Object.defineProperty(Debugger.Frame.prototype, "line", { |
|
4676 configurable: true, |
|
4677 get: function() { |
|
4678 if (this.script) { |
|
4679 return this.script.getOffsetLine(this.offset); |
|
4680 } else { |
|
4681 return null; |
|
4682 } |
|
4683 } |
|
4684 }); |
|
4685 |
|
4686 |
|
4687 /** |
|
4688 * Creates an actor for handling chrome debugging. ChromeDebuggerActor is a |
|
4689 * thin wrapper over ThreadActor, slightly changing some of its behavior. |
|
4690 * |
|
4691 * @param aConnection object |
|
4692 * The DebuggerServerConnection with which this ChromeDebuggerActor |
|
4693 * is associated. (Currently unused, but required to make this |
|
4694 * constructor usable with addGlobalActor.) |
|
4695 * |
|
4696 * @param aHooks object |
|
4697 * An object with preNest and postNest methods for calling when entering |
|
4698 * and exiting a nested event loop. |
|
4699 */ |
|
4700 function ChromeDebuggerActor(aConnection, aHooks) |
|
4701 { |
|
4702 ThreadActor.call(this, aHooks); |
|
4703 } |
|
4704 |
|
4705 ChromeDebuggerActor.prototype = Object.create(ThreadActor.prototype); |
|
4706 |
|
4707 update(ChromeDebuggerActor.prototype, { |
|
4708 constructor: ChromeDebuggerActor, |
|
4709 |
|
4710 // A constant prefix that will be used to form the actor ID by the server. |
|
4711 actorPrefix: "chromeDebugger", |
|
4712 |
|
4713 /** |
|
4714 * Override the eligibility check for scripts and sources to make sure every |
|
4715 * script and source with a URL is stored when debugging chrome. |
|
4716 */ |
|
4717 _allowSource: function(aSourceURL) !!aSourceURL, |
|
4718 |
|
4719 /** |
|
4720 * An object that will be used by ThreadActors to tailor their behavior |
|
4721 * depending on the debugging context being required (chrome or content). |
|
4722 * The methods that this object provides must be bound to the ThreadActor |
|
4723 * before use. |
|
4724 */ |
|
4725 globalManager: { |
|
4726 findGlobals: function () { |
|
4727 // Add every global known to the debugger as debuggee. |
|
4728 this.dbg.addAllGlobalsAsDebuggees(); |
|
4729 }, |
|
4730 |
|
4731 /** |
|
4732 * A function that the engine calls when a new global object has been |
|
4733 * created. |
|
4734 * |
|
4735 * @param aGlobal Debugger.Object |
|
4736 * The new global object that was created. |
|
4737 */ |
|
4738 onNewGlobal: function (aGlobal) { |
|
4739 this.addDebuggee(aGlobal); |
|
4740 // Notify the client. |
|
4741 this.conn.send({ |
|
4742 from: this.actorID, |
|
4743 type: "newGlobal", |
|
4744 // TODO: after bug 801084 lands see if we need to JSONify this. |
|
4745 hostAnnotations: aGlobal.hostAnnotations |
|
4746 }); |
|
4747 } |
|
4748 } |
|
4749 }); |
|
4750 |
|
4751 /** |
|
4752 * Creates an actor for handling add-on debugging. AddonThreadActor is |
|
4753 * a thin wrapper over ThreadActor. |
|
4754 * |
|
4755 * @param aConnection object |
|
4756 * The DebuggerServerConnection with which this AddonThreadActor |
|
4757 * is associated. (Currently unused, but required to make this |
|
4758 * constructor usable with addGlobalActor.) |
|
4759 * |
|
4760 * @param aHooks object |
|
4761 * An object with preNest and postNest methods for calling |
|
4762 * when entering and exiting a nested event loops. |
|
4763 * |
|
4764 * @param aAddonID string |
|
4765 * ID of the add-on this actor will debug. It will be used to |
|
4766 * filter out globals marked for debugging. |
|
4767 */ |
|
4768 |
|
4769 function AddonThreadActor(aConnect, aHooks, aAddonID) { |
|
4770 this.addonID = aAddonID; |
|
4771 ThreadActor.call(this, aHooks); |
|
4772 } |
|
4773 |
|
4774 AddonThreadActor.prototype = Object.create(ThreadActor.prototype); |
|
4775 |
|
4776 update(AddonThreadActor.prototype, { |
|
4777 constructor: AddonThreadActor, |
|
4778 |
|
4779 // A constant prefix that will be used to form the actor ID by the server. |
|
4780 actorPrefix: "addonThread", |
|
4781 |
|
4782 onAttach: function(aRequest) { |
|
4783 if (!this.attached) { |
|
4784 Services.obs.addObserver(this, "chrome-document-global-created", false); |
|
4785 Services.obs.addObserver(this, "content-document-global-created", false); |
|
4786 } |
|
4787 return ThreadActor.prototype.onAttach.call(this, aRequest); |
|
4788 }, |
|
4789 |
|
4790 disconnect: function() { |
|
4791 if (this.attached) { |
|
4792 Services.obs.removeObserver(this, "content-document-global-created"); |
|
4793 Services.obs.removeObserver(this, "chrome-document-global-created"); |
|
4794 } |
|
4795 return ThreadActor.prototype.disconnect.call(this); |
|
4796 }, |
|
4797 |
|
4798 /** |
|
4799 * Called when a new DOM document global is created. Check if the DOM was |
|
4800 * loaded from an add-on and if so make the window a debuggee. |
|
4801 */ |
|
4802 observe: function(aSubject, aTopic, aData) { |
|
4803 let id = {}; |
|
4804 if (mapURIToAddonID(aSubject.location, id) && id.value === this.addonID) { |
|
4805 this.dbg.addDebuggee(aSubject.defaultView); |
|
4806 } |
|
4807 }, |
|
4808 |
|
4809 /** |
|
4810 * Override the eligibility check for scripts and sources to make |
|
4811 * sure every script and source with a URL is stored when debugging |
|
4812 * add-ons. |
|
4813 */ |
|
4814 _allowSource: function(aSourceURL) { |
|
4815 // Hide eval scripts |
|
4816 if (!aSourceURL) { |
|
4817 return false; |
|
4818 } |
|
4819 |
|
4820 // XPIProvider.jsm evals some code in every add-on's bootstrap.js. Hide it |
|
4821 if (aSourceURL == "resource://gre/modules/addons/XPIProvider.jsm") { |
|
4822 return false; |
|
4823 } |
|
4824 |
|
4825 return true; |
|
4826 }, |
|
4827 |
|
4828 /** |
|
4829 * An object that will be used by ThreadActors to tailor their |
|
4830 * behaviour depending on the debugging context being required (chrome, |
|
4831 * addon or content). The methods that this object provides must |
|
4832 * be bound to the ThreadActor before use. |
|
4833 */ |
|
4834 globalManager: { |
|
4835 findGlobals: function ADA_findGlobals() { |
|
4836 for (let global of this.dbg.findAllGlobals()) { |
|
4837 if (this._checkGlobal(global)) { |
|
4838 this.dbg.addDebuggee(global); |
|
4839 } |
|
4840 } |
|
4841 }, |
|
4842 |
|
4843 /** |
|
4844 * A function that the engine calls when a new global object |
|
4845 * has been created. |
|
4846 * |
|
4847 * @param aGlobal Debugger.Object |
|
4848 * The new global object that was created. |
|
4849 */ |
|
4850 onNewGlobal: function ADA_onNewGlobal(aGlobal) { |
|
4851 if (this._checkGlobal(aGlobal)) { |
|
4852 this.addDebuggee(aGlobal); |
|
4853 // Notify the client. |
|
4854 this.conn.send({ |
|
4855 from: this.actorID, |
|
4856 type: "newGlobal", |
|
4857 // TODO: after bug 801084 lands see if we need to JSONify this. |
|
4858 hostAnnotations: aGlobal.hostAnnotations |
|
4859 }); |
|
4860 } |
|
4861 } |
|
4862 }, |
|
4863 |
|
4864 /** |
|
4865 * Checks if the provided global belongs to the debugged add-on. |
|
4866 * |
|
4867 * @param aGlobal Debugger.Object |
|
4868 */ |
|
4869 _checkGlobal: function ADA_checkGlobal(aGlobal) { |
|
4870 let obj = null; |
|
4871 try { |
|
4872 obj = aGlobal.unsafeDereference(); |
|
4873 } |
|
4874 catch (e) { |
|
4875 // Because of bug 991399 we sometimes get bad objects here. If we can't |
|
4876 // dereference them then they won't be useful to us |
|
4877 return false; |
|
4878 } |
|
4879 |
|
4880 try { |
|
4881 // This will fail for non-Sandbox objects, hence the try-catch block. |
|
4882 let metadata = Cu.getSandboxMetadata(obj); |
|
4883 if (metadata) { |
|
4884 return metadata.addonID === this.addonID; |
|
4885 } |
|
4886 } catch (e) { |
|
4887 } |
|
4888 |
|
4889 if (obj instanceof Ci.nsIDOMWindow) { |
|
4890 let id = {}; |
|
4891 if (mapURIToAddonID(obj.document.documentURIObject, id)) { |
|
4892 return id.value === this.addonID; |
|
4893 } |
|
4894 return false; |
|
4895 } |
|
4896 |
|
4897 // Check the global for a __URI__ property and then try to map that to an |
|
4898 // add-on |
|
4899 let uridescriptor = aGlobal.getOwnPropertyDescriptor("__URI__"); |
|
4900 if (uridescriptor && "value" in uridescriptor && uridescriptor.value) { |
|
4901 let uri; |
|
4902 try { |
|
4903 uri = Services.io.newURI(uridescriptor.value, null, null); |
|
4904 } |
|
4905 catch (e) { |
|
4906 DevToolsUtils.reportException("AddonThreadActor.prototype._checkGlobal", |
|
4907 new Error("Invalid URI: " + uridescriptor.value)); |
|
4908 return false; |
|
4909 } |
|
4910 |
|
4911 let id = {}; |
|
4912 if (mapURIToAddonID(uri, id)) { |
|
4913 return id.value === this.addonID; |
|
4914 } |
|
4915 } |
|
4916 |
|
4917 return false; |
|
4918 } |
|
4919 }); |
|
4920 |
|
4921 AddonThreadActor.prototype.requestTypes = Object.create(ThreadActor.prototype.requestTypes); |
|
4922 update(AddonThreadActor.prototype.requestTypes, { |
|
4923 "attach": AddonThreadActor.prototype.onAttach |
|
4924 }); |
|
4925 |
|
4926 /** |
|
4927 * Manages the sources for a thread. Handles source maps, locations in the |
|
4928 * sources, etc for ThreadActors. |
|
4929 */ |
|
4930 function ThreadSources(aThreadActor, aUseSourceMaps, aAllowPredicate, |
|
4931 aOnNewSource) { |
|
4932 this._thread = aThreadActor; |
|
4933 this._useSourceMaps = aUseSourceMaps; |
|
4934 this._allow = aAllowPredicate; |
|
4935 this._onNewSource = aOnNewSource; |
|
4936 |
|
4937 // generated source url --> promise of SourceMapConsumer |
|
4938 this._sourceMapsByGeneratedSource = Object.create(null); |
|
4939 // original source url --> promise of SourceMapConsumer |
|
4940 this._sourceMapsByOriginalSource = Object.create(null); |
|
4941 // source url --> SourceActor |
|
4942 this._sourceActors = Object.create(null); |
|
4943 // original url --> generated url |
|
4944 this._generatedUrlsByOriginalUrl = Object.create(null); |
|
4945 } |
|
4946 |
|
4947 /** |
|
4948 * Must be a class property because it needs to persist across reloads, same as |
|
4949 * the breakpoint store. |
|
4950 */ |
|
4951 ThreadSources._blackBoxedSources = new Set(["self-hosted"]); |
|
4952 ThreadSources._prettyPrintedSources = new Map(); |
|
4953 |
|
4954 ThreadSources.prototype = { |
|
4955 /** |
|
4956 * Return the source actor representing |url|, creating one if none |
|
4957 * exists already. Returns null if |url| is not allowed by the 'allow' |
|
4958 * predicate. |
|
4959 * |
|
4960 * Right now this takes a URL, but in the future it should |
|
4961 * take a Debugger.Source. See bug 637572. |
|
4962 * |
|
4963 * @param String url |
|
4964 * The source URL. |
|
4965 * @param optional SourceMapConsumer sourceMap |
|
4966 * The source map that introduced this source, if any. |
|
4967 * @param optional String generatedSource |
|
4968 * The generated source url that introduced this source via source map, |
|
4969 * if any. |
|
4970 * @param optional String text |
|
4971 * The text content of the source, if immediately available. |
|
4972 * @param optional String contentType |
|
4973 * The content type of the source, if immediately available. |
|
4974 * @returns a SourceActor representing the source at aURL or null. |
|
4975 */ |
|
4976 source: function ({ url, sourceMap, generatedSource, text, contentType }) { |
|
4977 if (!this._allow(url)) { |
|
4978 return null; |
|
4979 } |
|
4980 |
|
4981 if (url in this._sourceActors) { |
|
4982 return this._sourceActors[url]; |
|
4983 } |
|
4984 |
|
4985 let actor = new SourceActor({ |
|
4986 url: url, |
|
4987 thread: this._thread, |
|
4988 sourceMap: sourceMap, |
|
4989 generatedSource: generatedSource, |
|
4990 text: text, |
|
4991 contentType: contentType |
|
4992 }); |
|
4993 this._thread.threadLifetimePool.addActor(actor); |
|
4994 this._sourceActors[url] = actor; |
|
4995 try { |
|
4996 this._onNewSource(actor); |
|
4997 } catch (e) { |
|
4998 reportError(e); |
|
4999 } |
|
5000 return actor; |
|
5001 }, |
|
5002 |
|
5003 /** |
|
5004 * Only to be used when we aren't source mapping. |
|
5005 */ |
|
5006 _sourceForScript: function (aScript) { |
|
5007 const spec = { |
|
5008 url: aScript.url |
|
5009 }; |
|
5010 |
|
5011 // XXX bug 915433: We can't rely on Debugger.Source.prototype.text if the |
|
5012 // source is an HTML-embedded <script> tag. Since we don't have an API |
|
5013 // implemented to detect whether this is the case, we need to be |
|
5014 // conservative and only use Debugger.Source.prototype.text if we get a |
|
5015 // normal .js file. |
|
5016 if (aScript.url) { |
|
5017 try { |
|
5018 const url = Services.io.newURI(aScript.url, null, null) |
|
5019 .QueryInterface(Ci.nsIURL); |
|
5020 if (url.fileExtension === "js") { |
|
5021 spec.contentType = "text/javascript"; |
|
5022 spec.text = aScript.source.text; |
|
5023 } |
|
5024 } catch(ex) { |
|
5025 // Not a valid URI. |
|
5026 } |
|
5027 } |
|
5028 |
|
5029 return this.source(spec); |
|
5030 }, |
|
5031 |
|
5032 /** |
|
5033 * Return a promise of an array of source actors representing all the |
|
5034 * sources of |aScript|. |
|
5035 * |
|
5036 * If source map handling is enabled and |aScript| has a source map, then |
|
5037 * use it to find all of |aScript|'s *original* sources; return a promise |
|
5038 * of an array of source actors for those. |
|
5039 */ |
|
5040 sourcesForScript: function (aScript) { |
|
5041 if (!this._useSourceMaps || !aScript.sourceMapURL) { |
|
5042 return resolve([this._sourceForScript(aScript)].filter(isNotNull)); |
|
5043 } |
|
5044 |
|
5045 return this.sourceMap(aScript) |
|
5046 .then((aSourceMap) => { |
|
5047 return [ |
|
5048 this.source({ url: s, |
|
5049 sourceMap: aSourceMap, |
|
5050 generatedSource: aScript.url }) |
|
5051 for (s of aSourceMap.sources) |
|
5052 ]; |
|
5053 }) |
|
5054 .then(null, (e) => { |
|
5055 reportError(e); |
|
5056 delete this._sourceMapsByGeneratedSource[aScript.url]; |
|
5057 return [this._sourceForScript(aScript)]; |
|
5058 }) |
|
5059 .then(ss => ss.filter(isNotNull)); |
|
5060 }, |
|
5061 |
|
5062 /** |
|
5063 * Return a promise of a SourceMapConsumer for the source map for |
|
5064 * |aScript|; if we already have such a promise extant, return that. |
|
5065 * |aScript| must have a non-null sourceMapURL. |
|
5066 */ |
|
5067 sourceMap: function (aScript) { |
|
5068 dbg_assert(aScript.sourceMapURL, "Script should have a sourceMapURL"); |
|
5069 let sourceMapURL = this._normalize(aScript.sourceMapURL, aScript.url); |
|
5070 let map = this._fetchSourceMap(sourceMapURL, aScript.url) |
|
5071 .then(aSourceMap => this.saveSourceMap(aSourceMap, aScript.url)); |
|
5072 this._sourceMapsByGeneratedSource[aScript.url] = map; |
|
5073 return map; |
|
5074 }, |
|
5075 |
|
5076 /** |
|
5077 * Save the given source map so that we can use it to query source locations |
|
5078 * down the line. |
|
5079 */ |
|
5080 saveSourceMap: function (aSourceMap, aGeneratedSource) { |
|
5081 if (!aSourceMap) { |
|
5082 delete this._sourceMapsByGeneratedSource[aGeneratedSource]; |
|
5083 return null; |
|
5084 } |
|
5085 this._sourceMapsByGeneratedSource[aGeneratedSource] = resolve(aSourceMap); |
|
5086 for (let s of aSourceMap.sources) { |
|
5087 this._generatedUrlsByOriginalUrl[s] = aGeneratedSource; |
|
5088 this._sourceMapsByOriginalSource[s] = resolve(aSourceMap); |
|
5089 } |
|
5090 return aSourceMap; |
|
5091 }, |
|
5092 |
|
5093 /** |
|
5094 * Return a promise of a SourceMapConsumer for the source map located at |
|
5095 * |aAbsSourceMapURL|, which must be absolute. If there is already such a |
|
5096 * promise extant, return it. |
|
5097 * |
|
5098 * @param string aAbsSourceMapURL |
|
5099 * The source map URL, in absolute form, not relative. |
|
5100 * @param string aScriptURL |
|
5101 * When the source map URL is a data URI, there is no sourceRoot on the |
|
5102 * source map, and the source map's sources are relative, we resolve |
|
5103 * them from aScriptURL. |
|
5104 */ |
|
5105 _fetchSourceMap: function (aAbsSourceMapURL, aScriptURL) { |
|
5106 return fetch(aAbsSourceMapURL, { loadFromCache: false }) |
|
5107 .then(({ content }) => { |
|
5108 let map = new SourceMapConsumer(content); |
|
5109 this._setSourceMapRoot(map, aAbsSourceMapURL, aScriptURL); |
|
5110 return map; |
|
5111 }); |
|
5112 }, |
|
5113 |
|
5114 /** |
|
5115 * Sets the source map's sourceRoot to be relative to the source map url. |
|
5116 */ |
|
5117 _setSourceMapRoot: function (aSourceMap, aAbsSourceMapURL, aScriptURL) { |
|
5118 const base = this._dirname( |
|
5119 aAbsSourceMapURL.indexOf("data:") === 0 |
|
5120 ? aScriptURL |
|
5121 : aAbsSourceMapURL); |
|
5122 aSourceMap.sourceRoot = aSourceMap.sourceRoot |
|
5123 ? this._normalize(aSourceMap.sourceRoot, base) |
|
5124 : base; |
|
5125 }, |
|
5126 |
|
5127 _dirname: function (aPath) { |
|
5128 return Services.io.newURI( |
|
5129 ".", null, Services.io.newURI(aPath, null, null)).spec; |
|
5130 }, |
|
5131 |
|
5132 /** |
|
5133 * Returns a promise of the location in the original source if the source is |
|
5134 * source mapped, otherwise a promise of the same location. |
|
5135 */ |
|
5136 getOriginalLocation: function ({ url, line, column }) { |
|
5137 if (url in this._sourceMapsByGeneratedSource) { |
|
5138 column = column || 0; |
|
5139 |
|
5140 return this._sourceMapsByGeneratedSource[url] |
|
5141 .then((aSourceMap) => { |
|
5142 let { source: aSourceURL, line: aLine, column: aColumn } = aSourceMap.originalPositionFor({ |
|
5143 line: line, |
|
5144 column: column |
|
5145 }); |
|
5146 return { |
|
5147 url: aSourceURL, |
|
5148 line: aLine, |
|
5149 column: aColumn |
|
5150 }; |
|
5151 }) |
|
5152 .then(null, error => { |
|
5153 if (!DevToolsUtils.reportingDisabled) { |
|
5154 DevToolsUtils.reportException("ThreadSources.prototype.getOriginalLocation", error); |
|
5155 } |
|
5156 return { url: null, line: null, column: null }; |
|
5157 }); |
|
5158 } |
|
5159 |
|
5160 // No source map |
|
5161 return resolve({ |
|
5162 url: url, |
|
5163 line: line, |
|
5164 column: column |
|
5165 }); |
|
5166 }, |
|
5167 |
|
5168 /** |
|
5169 * Returns a promise of the location in the generated source corresponding to |
|
5170 * the original source and line given. |
|
5171 * |
|
5172 * When we pass a script S representing generated code to |sourceMap|, |
|
5173 * above, that returns a promise P. The process of resolving P populates |
|
5174 * the tables this function uses; thus, it won't know that S's original |
|
5175 * source URLs map to S until P is resolved. |
|
5176 */ |
|
5177 getGeneratedLocation: function ({ url, line, column }) { |
|
5178 if (url in this._sourceMapsByOriginalSource) { |
|
5179 return this._sourceMapsByOriginalSource[url] |
|
5180 .then((aSourceMap) => { |
|
5181 let { line: aLine, column: aColumn } = aSourceMap.generatedPositionFor({ |
|
5182 source: url, |
|
5183 line: line, |
|
5184 column: column == null ? Infinity : column |
|
5185 }); |
|
5186 return { |
|
5187 url: this._generatedUrlsByOriginalUrl[url], |
|
5188 line: aLine, |
|
5189 column: aColumn |
|
5190 }; |
|
5191 }); |
|
5192 } |
|
5193 |
|
5194 // No source map |
|
5195 return resolve({ |
|
5196 url: url, |
|
5197 line: line, |
|
5198 column: column |
|
5199 }); |
|
5200 }, |
|
5201 |
|
5202 /** |
|
5203 * Returns true if URL for the given source is black boxed. |
|
5204 * |
|
5205 * @param aURL String |
|
5206 * The URL of the source which we are checking whether it is black |
|
5207 * boxed or not. |
|
5208 */ |
|
5209 isBlackBoxed: function (aURL) { |
|
5210 return ThreadSources._blackBoxedSources.has(aURL); |
|
5211 }, |
|
5212 |
|
5213 /** |
|
5214 * Add the given source URL to the set of sources that are black boxed. |
|
5215 * |
|
5216 * @param aURL String |
|
5217 * The URL of the source which we are black boxing. |
|
5218 */ |
|
5219 blackBox: function (aURL) { |
|
5220 ThreadSources._blackBoxedSources.add(aURL); |
|
5221 }, |
|
5222 |
|
5223 /** |
|
5224 * Remove the given source URL to the set of sources that are black boxed. |
|
5225 * |
|
5226 * @param aURL String |
|
5227 * The URL of the source which we are no longer black boxing. |
|
5228 */ |
|
5229 unblackBox: function (aURL) { |
|
5230 ThreadSources._blackBoxedSources.delete(aURL); |
|
5231 }, |
|
5232 |
|
5233 /** |
|
5234 * Returns true if the given URL is pretty printed. |
|
5235 * |
|
5236 * @param aURL String |
|
5237 * The URL of the source that might be pretty printed. |
|
5238 */ |
|
5239 isPrettyPrinted: function (aURL) { |
|
5240 return ThreadSources._prettyPrintedSources.has(aURL); |
|
5241 }, |
|
5242 |
|
5243 /** |
|
5244 * Add the given URL to the set of sources that are pretty printed. |
|
5245 * |
|
5246 * @param aURL String |
|
5247 * The URL of the source to be pretty printed. |
|
5248 */ |
|
5249 prettyPrint: function (aURL, aIndent) { |
|
5250 ThreadSources._prettyPrintedSources.set(aURL, aIndent); |
|
5251 }, |
|
5252 |
|
5253 /** |
|
5254 * Return the indent the given URL was pretty printed by. |
|
5255 */ |
|
5256 prettyPrintIndent: function (aURL) { |
|
5257 return ThreadSources._prettyPrintedSources.get(aURL); |
|
5258 }, |
|
5259 |
|
5260 /** |
|
5261 * Remove the given URL from the set of sources that are pretty printed. |
|
5262 * |
|
5263 * @param aURL String |
|
5264 * The URL of the source that is no longer pretty printed. |
|
5265 */ |
|
5266 disablePrettyPrint: function (aURL) { |
|
5267 ThreadSources._prettyPrintedSources.delete(aURL); |
|
5268 }, |
|
5269 |
|
5270 /** |
|
5271 * Normalize multiple relative paths towards the base paths on the right. |
|
5272 */ |
|
5273 _normalize: function (...aURLs) { |
|
5274 dbg_assert(aURLs.length > 1, "Should have more than 1 URL"); |
|
5275 let base = Services.io.newURI(aURLs.pop(), null, null); |
|
5276 let url; |
|
5277 while ((url = aURLs.pop())) { |
|
5278 base = Services.io.newURI(url, null, base); |
|
5279 } |
|
5280 return base.spec; |
|
5281 }, |
|
5282 |
|
5283 iter: function* () { |
|
5284 for (let url in this._sourceActors) { |
|
5285 yield this._sourceActors[url]; |
|
5286 } |
|
5287 } |
|
5288 }; |
|
5289 |
|
5290 // Utility functions. |
|
5291 |
|
5292 // TODO bug 863089: use Debugger.Script.prototype.getOffsetColumn when it is |
|
5293 // implemented. |
|
5294 function getOffsetColumn(aOffset, aScript) { |
|
5295 let bestOffsetMapping = null; |
|
5296 for (let offsetMapping of aScript.getAllColumnOffsets()) { |
|
5297 if (!bestOffsetMapping || |
|
5298 (offsetMapping.offset <= aOffset && |
|
5299 offsetMapping.offset > bestOffsetMapping.offset)) { |
|
5300 bestOffsetMapping = offsetMapping; |
|
5301 } |
|
5302 } |
|
5303 |
|
5304 if (!bestOffsetMapping) { |
|
5305 // XXX: Try not to completely break the experience of using the debugger for |
|
5306 // the user by assuming column 0. Simultaneously, report the error so that |
|
5307 // there is a paper trail if the assumption is bad and the debugging |
|
5308 // experience becomes wonky. |
|
5309 reportError(new Error("Could not find a column for offset " + aOffset |
|
5310 + " in the script " + aScript)); |
|
5311 return 0; |
|
5312 } |
|
5313 |
|
5314 return bestOffsetMapping.columnNumber; |
|
5315 } |
|
5316 |
|
5317 /** |
|
5318 * Return the non-source-mapped location of the given Debugger.Frame. If the |
|
5319 * frame does not have a script, the location's properties are all null. |
|
5320 * |
|
5321 * @param Debugger.Frame aFrame |
|
5322 * The frame whose location we are getting. |
|
5323 * @returns Object |
|
5324 * Returns an object of the form { url, line, column } |
|
5325 */ |
|
5326 function getFrameLocation(aFrame) { |
|
5327 if (!aFrame || !aFrame.script) { |
|
5328 return { url: null, line: null, column: null }; |
|
5329 } |
|
5330 return { |
|
5331 url: aFrame.script.url, |
|
5332 line: aFrame.script.getOffsetLine(aFrame.offset), |
|
5333 column: getOffsetColumn(aFrame.offset, aFrame.script) |
|
5334 } |
|
5335 } |
|
5336 |
|
5337 /** |
|
5338 * Utility function for updating an object with the properties of another |
|
5339 * object. |
|
5340 * |
|
5341 * @param aTarget Object |
|
5342 * The object being updated. |
|
5343 * @param aNewAttrs Object |
|
5344 * The new attributes being set on the target. |
|
5345 */ |
|
5346 function update(aTarget, aNewAttrs) { |
|
5347 for (let key in aNewAttrs) { |
|
5348 let desc = Object.getOwnPropertyDescriptor(aNewAttrs, key); |
|
5349 |
|
5350 if (desc) { |
|
5351 Object.defineProperty(aTarget, key, desc); |
|
5352 } |
|
5353 } |
|
5354 } |
|
5355 |
|
5356 /** |
|
5357 * Returns true if its argument is not null. |
|
5358 */ |
|
5359 function isNotNull(aThing) { |
|
5360 return aThing !== null; |
|
5361 } |
|
5362 |
|
5363 /** |
|
5364 * Performs a request to load the desired URL and returns a promise. |
|
5365 * |
|
5366 * @param aURL String |
|
5367 * The URL we will request. |
|
5368 * @returns Promise |
|
5369 * A promise of the document at that URL, as a string. |
|
5370 * |
|
5371 * XXX: It may be better to use nsITraceableChannel to get to the sources |
|
5372 * without relying on caching when we can (not for eval, etc.): |
|
5373 * http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/ |
|
5374 */ |
|
5375 function fetch(aURL, aOptions={ loadFromCache: true }) { |
|
5376 let deferred = defer(); |
|
5377 let scheme; |
|
5378 let url = aURL.split(" -> ").pop(); |
|
5379 let charset; |
|
5380 let contentType; |
|
5381 |
|
5382 try { |
|
5383 scheme = Services.io.extractScheme(url); |
|
5384 } catch (e) { |
|
5385 // In the xpcshell tests, the script url is the absolute path of the test |
|
5386 // file, which will make a malformed URI error be thrown. Add the file |
|
5387 // scheme prefix ourselves. |
|
5388 url = "file://" + url; |
|
5389 scheme = Services.io.extractScheme(url); |
|
5390 } |
|
5391 |
|
5392 switch (scheme) { |
|
5393 case "file": |
|
5394 case "chrome": |
|
5395 case "resource": |
|
5396 try { |
|
5397 NetUtil.asyncFetch(url, function onFetch(aStream, aStatus, aRequest) { |
|
5398 if (!Components.isSuccessCode(aStatus)) { |
|
5399 deferred.reject(new Error("Request failed with status code = " |
|
5400 + aStatus |
|
5401 + " after NetUtil.asyncFetch for url = " |
|
5402 + url)); |
|
5403 return; |
|
5404 } |
|
5405 |
|
5406 let source = NetUtil.readInputStreamToString(aStream, aStream.available()); |
|
5407 contentType = aRequest.contentType; |
|
5408 deferred.resolve(source); |
|
5409 aStream.close(); |
|
5410 }); |
|
5411 } catch (ex) { |
|
5412 deferred.reject(ex); |
|
5413 } |
|
5414 break; |
|
5415 |
|
5416 default: |
|
5417 let channel; |
|
5418 try { |
|
5419 channel = Services.io.newChannel(url, null, null); |
|
5420 } catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") { |
|
5421 // On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but |
|
5422 // newChannel won't be able to handle it. |
|
5423 url = "file:///" + url; |
|
5424 channel = Services.io.newChannel(url, null, null); |
|
5425 } |
|
5426 let chunks = []; |
|
5427 let streamListener = { |
|
5428 onStartRequest: function(aRequest, aContext, aStatusCode) { |
|
5429 if (!Components.isSuccessCode(aStatusCode)) { |
|
5430 deferred.reject(new Error("Request failed with status code = " |
|
5431 + aStatusCode |
|
5432 + " in onStartRequest handler for url = " |
|
5433 + url)); |
|
5434 } |
|
5435 }, |
|
5436 onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) { |
|
5437 chunks.push(NetUtil.readInputStreamToString(aStream, aCount)); |
|
5438 }, |
|
5439 onStopRequest: function(aRequest, aContext, aStatusCode) { |
|
5440 if (!Components.isSuccessCode(aStatusCode)) { |
|
5441 deferred.reject(new Error("Request failed with status code = " |
|
5442 + aStatusCode |
|
5443 + " in onStopRequest handler for url = " |
|
5444 + url)); |
|
5445 return; |
|
5446 } |
|
5447 |
|
5448 charset = channel.contentCharset; |
|
5449 contentType = channel.contentType; |
|
5450 deferred.resolve(chunks.join("")); |
|
5451 } |
|
5452 }; |
|
5453 |
|
5454 channel.loadFlags = aOptions.loadFromCache |
|
5455 ? channel.LOAD_FROM_CACHE |
|
5456 : channel.LOAD_BYPASS_CACHE; |
|
5457 channel.asyncOpen(streamListener, null); |
|
5458 break; |
|
5459 } |
|
5460 |
|
5461 return deferred.promise.then(source => { |
|
5462 return { |
|
5463 content: convertToUnicode(source, charset), |
|
5464 contentType: contentType |
|
5465 }; |
|
5466 }); |
|
5467 } |
|
5468 |
|
5469 /** |
|
5470 * Convert a given string, encoded in a given character set, to unicode. |
|
5471 * |
|
5472 * @param string aString |
|
5473 * A string. |
|
5474 * @param string aCharset |
|
5475 * A character set. |
|
5476 */ |
|
5477 function convertToUnicode(aString, aCharset=null) { |
|
5478 // Decoding primitives. |
|
5479 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] |
|
5480 .createInstance(Ci.nsIScriptableUnicodeConverter); |
|
5481 try { |
|
5482 converter.charset = aCharset || "UTF-8"; |
|
5483 return converter.ConvertToUnicode(aString); |
|
5484 } catch(e) { |
|
5485 return aString; |
|
5486 } |
|
5487 } |
|
5488 |
|
5489 /** |
|
5490 * Report the given error in the error console and to stdout. |
|
5491 * |
|
5492 * @param Error aError |
|
5493 * The error object you wish to report. |
|
5494 * @param String aPrefix |
|
5495 * An optional prefix for the reported error message. |
|
5496 */ |
|
5497 function reportError(aError, aPrefix="") { |
|
5498 dbg_assert(aError instanceof Error, "Must pass Error objects to reportError"); |
|
5499 let msg = aPrefix + aError.message + ":\n" + aError.stack; |
|
5500 Cu.reportError(msg); |
|
5501 dumpn(msg); |
|
5502 } |
|
5503 |
|
5504 // The following are copied here verbatim from css-logic.js, until we create a |
|
5505 // server-friendly helper module. |
|
5506 |
|
5507 /** |
|
5508 * Find a unique CSS selector for a given element |
|
5509 * @returns a string such that ele.ownerDocument.querySelector(reply) === ele |
|
5510 * and ele.ownerDocument.querySelectorAll(reply).length === 1 |
|
5511 */ |
|
5512 function findCssSelector(ele) { |
|
5513 var document = ele.ownerDocument; |
|
5514 if (ele.id && document.getElementById(ele.id) === ele) { |
|
5515 return '#' + ele.id; |
|
5516 } |
|
5517 |
|
5518 // Inherently unique by tag name |
|
5519 var tagName = ele.tagName.toLowerCase(); |
|
5520 if (tagName === 'html') { |
|
5521 return 'html'; |
|
5522 } |
|
5523 if (tagName === 'head') { |
|
5524 return 'head'; |
|
5525 } |
|
5526 if (tagName === 'body') { |
|
5527 return 'body'; |
|
5528 } |
|
5529 |
|
5530 if (ele.parentNode == null) { |
|
5531 console.log('danger: ' + tagName); |
|
5532 } |
|
5533 |
|
5534 // We might be able to find a unique class name |
|
5535 var selector, index, matches; |
|
5536 if (ele.classList.length > 0) { |
|
5537 for (var i = 0; i < ele.classList.length; i++) { |
|
5538 // Is this className unique by itself? |
|
5539 selector = '.' + ele.classList.item(i); |
|
5540 matches = document.querySelectorAll(selector); |
|
5541 if (matches.length === 1) { |
|
5542 return selector; |
|
5543 } |
|
5544 // Maybe it's unique with a tag name? |
|
5545 selector = tagName + selector; |
|
5546 matches = document.querySelectorAll(selector); |
|
5547 if (matches.length === 1) { |
|
5548 return selector; |
|
5549 } |
|
5550 // Maybe it's unique using a tag name and nth-child |
|
5551 index = positionInNodeList(ele, ele.parentNode.children) + 1; |
|
5552 selector = selector + ':nth-child(' + index + ')'; |
|
5553 matches = document.querySelectorAll(selector); |
|
5554 if (matches.length === 1) { |
|
5555 return selector; |
|
5556 } |
|
5557 } |
|
5558 } |
|
5559 |
|
5560 // So we can be unique w.r.t. our parent, and use recursion |
|
5561 index = positionInNodeList(ele, ele.parentNode.children) + 1; |
|
5562 selector = findCssSelector(ele.parentNode) + ' > ' + |
|
5563 tagName + ':nth-child(' + index + ')'; |
|
5564 |
|
5565 return selector; |
|
5566 }; |
|
5567 |
|
5568 /** |
|
5569 * Find the position of [element] in [nodeList]. |
|
5570 * @returns an index of the match, or -1 if there is no match |
|
5571 */ |
|
5572 function positionInNodeList(element, nodeList) { |
|
5573 for (var i = 0; i < nodeList.length; i++) { |
|
5574 if (element === nodeList[i]) { |
|
5575 return i; |
|
5576 } |
|
5577 } |
|
5578 return -1; |
|
5579 } |
|
5580 |
|
5581 /** |
|
5582 * Make a debuggee value for the given object, if needed. Primitive values |
|
5583 * are left the same. |
|
5584 * |
|
5585 * Use case: you have a raw JS object (after unsafe dereference) and you want to |
|
5586 * send it to the client. In that case you need to use an ObjectActor which |
|
5587 * requires a debuggee value. The Debugger.Object.prototype.makeDebuggeeValue() |
|
5588 * method works only for JS objects and functions. |
|
5589 * |
|
5590 * @param Debugger.Object obj |
|
5591 * @param any value |
|
5592 * @return object |
|
5593 */ |
|
5594 function makeDebuggeeValueIfNeeded(obj, value) { |
|
5595 if (value && (typeof value == "object" || typeof value == "function")) { |
|
5596 return obj.makeDebuggeeValue(value); |
|
5597 } |
|
5598 return value; |
|
5599 } |
|
5600 |
|
5601 function getInnerId(window) { |
|
5602 return window.QueryInterface(Ci.nsIInterfaceRequestor). |
|
5603 getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID; |
|
5604 }; |