Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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/. */
7 "use strict";
9 let B2G_ID = "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}";
11 let TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array",
12 "Uint32Array", "Int8Array", "Int16Array", "Int32Array", "Float32Array",
13 "Float64Array"];
15 // Number of items to preview in objects, arrays, maps, sets, lists,
16 // collections, etc.
17 let OBJECT_PREVIEW_MAX_ITEMS = 10;
19 let addonManager = null;
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 }
32 if (!addonManager) {
33 addonManager = Cc["@mozilla.org/addons/integration;1"].
34 getService(Ci.amIAddonManager);
35 }
37 try {
38 return addonManager.mapURIToAddonID(uri, id);
39 }
40 catch (e) {
41 DevtoolsUtils.reportException("mapURIToAddonID", e);
42 return false;
43 }
44 }
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;
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);
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 }
77 BreakpointStore.prototype = {
78 _size: null,
79 get size() { return this._size; },
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;
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 }
114 this._size++;
115 },
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--;
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 },
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);
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 }
181 return foundBreakpoint;
182 },
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 }
207 return null;
208 },
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 }
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 },
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 },
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 : [];
278 const lines = wholeLines.concat(columnLines).sort();
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 },
291 _iterColumns: function* (aUrl, aLine, aColumn) {
292 if (!this._breakpoints[aUrl] || !this._breakpoints[aUrl][aLine]) {
293 return;
294 }
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 };
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 }
333 EventLoopStack.prototype = {
334 /**
335 * The number of nested event loops on the stack.
336 */
337 get size() {
338 return this._inspector.eventLoopNestLevel;
339 },
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 },
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 },
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 };
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;
401 this.enter = this.enter.bind(this);
402 this.resolve = this.resolve.bind(this);
403 }
405 EventLoop.prototype = {
406 entered: false,
407 resolved: false,
408 get url() { return this._hooks.url; },
410 /**
411 * Enter this nested event loop.
412 */
413 enter: function () {
414 let nestData = this._hooks.preNest
415 ? this._hooks.preNest()
416 : null;
418 this.entered = true;
419 this._inspector.enterNestedEventLoop(this);
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 }
429 dbg_assert(this._thread.state === "running",
430 "Should be in the running state");
432 if (this._hooks.postNest) {
433 this._hooks.postNest(nestData);
434 }
435 },
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 };
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();
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);
491 this._options = {
492 useSourceMaps: false
493 };
495 this._gripDepth = 0;
496 }
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();
504 ThreadActor.prototype = {
505 // Used by the ObjectActor to keep track of the depth of grip() calls.
506 _gripDepth: null,
508 actorPrefix: "context",
510 get state() { return this._state; },
511 get attached() this.state == "attached" ||
512 this.state == "running" ||
513 this.state == "paused",
515 get breakpointStore() { return ThreadActor.breakpointStore; },
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 },
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 },
534 get youngestFrame() {
535 if (this.state != "paused") {
536 return null;
537 }
538 return this.dbg.getNewestFrame();
539 },
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");
547 this._prettyPrintWorker.addEventListener(
548 "error", this._onPrettyPrintError, false);
550 if (dumpn.wantLogging) {
551 this._prettyPrintWorker.addEventListener("message", this._onPrettyPrintMsg, false);
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 },
564 _onPrettyPrintError: function ({ message, filename, lineno }) {
565 reportError(new Error(message + " @ " + filename + ":" + lineno));
566 },
568 _onPrettyPrintMsg: function ({ data }) {
569 dumpn("Received message from prettyPrintWorker: "
570 + JSON.stringify(data, null, 2) + "\n");
571 },
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 },
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 },
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 },
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 },
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 },
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 },
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", {});
671 this.globalDebugObject = this._addDebuggees(this.global);
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 },
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);
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) {}
704 useGlobal = (metadata['inner-window-id'] && metadata['inner-window-id'] == id);
705 }
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 },
722 disconnect: function () {
723 dumpn("in ThreadActor.prototype.disconnect");
724 if (this._state == "paused") {
725 this.onResume();
726 }
728 this.clearDebuggees();
729 this.conn.removeActorPool(this._threadLifetimePool);
730 this._threadLifetimePool = null;
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 }
741 if (!this.dbg) {
742 return;
743 }
744 this.dbg.enabled = false;
745 this.dbg = null;
746 },
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 },
756 // Request handlers
757 onAttach: function (aRequest) {
758 if (this.state === "exited") {
759 return { type: "exited" };
760 }
762 if (this.state !== "detached") {
763 return { error: "wrongState",
764 message: "Current state is " + this.state };
765 }
767 this._state = "attached";
769 update(this._options, aRequest.options || {});
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 });
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" };
793 this._restoreBreakpoints();
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);
800 // Start a nested event loop.
801 this._pushThreadPause();
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 },
812 onDetach: function (aRequest) {
813 this.disconnect();
814 this._state = "detached";
816 dumpn("ThreadActor.prototype.onDetach: returning 'detached' packet");
817 return {
818 type: "detached"
819 };
820 },
822 onReconfigure: function (aRequest) {
823 if (this.state == "exited") {
824 return { error: "wrongState" };
825 }
827 update(this._options, aRequest.options || {});
828 // Clear existing sources, so they can be recreated on next access.
829 this._sources = null;
831 return {};
832 },
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;
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 });
869 this._pushThreadPause();
870 } catch(e) {
871 reportError(e, "Got an exception during TA__pauseAndRespond: ");
872 }
874 return undefined;
875 },
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 },
893 _makeOnEnterFrame: function ({ pauseAndRespond }) {
894 return aFrame => {
895 const generatedLocation = getFrameLocation(aFrame);
896 let { url } = this.synchronize(this.sources.getOriginalLocation(
897 generatedLocation));
899 return this.sources.isBlackBoxed(url)
900 ? undefined
901 : pauseAndRespond(aFrame);
902 };
903 },
905 _makeOnPop: function ({ thread, pauseAndRespond, createValueGrip }) {
906 return function (aCompletion) {
907 // onPop is called with 'this' set to the current frame.
909 const generatedLocation = getFrameLocation(this);
910 const { url } = thread.synchronize(thread.sources.getOriginalLocation(
911 generatedLocation));
913 if (thread.sources.isBlackBoxed(url)) {
914 return undefined;
915 }
917 // Note that we're popping this frame; we need to watch for
918 // subsequent step events on its caller.
919 this.reportedPop = true;
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 },
937 _makeOnStep: function ({ thread, pauseAndRespond, startFrame,
938 startLocation }) {
939 return function () {
940 // onStep is called with 'this' set to the current frame.
942 const generatedLocation = getFrameLocation(this);
943 const newLocation = thread.synchronize(thread.sources.getOriginalLocation(
944 generatedLocation));
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.
961 // Cases 2.1 and 2.2
962 if (newLocation.url == null
963 || thread.sources.isBlackBoxed(newLocation.url)) {
964 return undefined;
965 }
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 }
974 // Otherwise, let execution continue (we haven't executed enough code to
975 // consider this a "step" yet).
976 return undefined;
977 };
978 },
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 };
998 return {
999 onEnterFrame: this._makeOnEnterFrame(steppingHookState),
1000 onPop: this._makeOnPop(steppingHookState),
1001 onStep: this._makeOnStep(steppingHookState)
1002 };
1003 },
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 }
1021 const generatedLocation = getFrameLocation(this.youngestFrame);
1022 return this.sources.getOriginalLocation(generatedLocation)
1023 .then(originalLocation => {
1024 const { onEnterFrame, onPop, onStep } = this._makeSteppingHooks(originalLocation);
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 }
1045 return true;
1046 });
1047 },
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 },
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 },
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 }
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 }
1108 if (aRequest && aRequest.forceCompletion) {
1109 return this._forceCompletion(aRequest);
1110 }
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 }
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 }
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 },
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;
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 });
1167 if (needNest) {
1168 eventLoop = this._nestedEventLoops.push();
1169 eventLoop.enter();
1170 }
1172 return returnVal;
1173 },
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 },
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 },
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);
1216 let targets = els.getEventTargetChainFor(eventTarget);
1217 let listeners = [];
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 },
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 },
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 },
1277 onClientEvaluate: function (aRequest) {
1278 if (this.state !== "paused") {
1279 return { error: "wrongState",
1280 message: "Debuggee must be paused to evaluate code." };
1281 }
1283 let frame = this._requestFrame(aRequest.frame);
1284 if (!frame) {
1285 return { error: "unknownFrame",
1286 message: "Evaluation frame not found" };
1287 }
1289 if (!frame.environment) {
1290 return { error: "notDebuggee",
1291 message: "cannot access the environment of this frame." };
1292 }
1294 let youngest = this.youngestFrame;
1296 // Put ourselves back in the running state and inform the client.
1297 let resumedPacket = this._resumed();
1298 this.conn.send(resumedPacket);
1300 // Run the expression.
1301 // XXX: test syntax errors
1302 let completion = frame.eval(aRequest.expression);
1304 // Put ourselves back in the pause state.
1305 let packet = this._paused(youngest);
1306 packet.why = { type: "clientEvaluated",
1307 frameFinished: this.createProtocolCompletionValue(completion) };
1309 // Return back to our previous pause's event loop.
1310 return packet;
1311 },
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 }
1319 let start = aRequest.start ? aRequest.start : 0;
1320 let count = aRequest.count;
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 }
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);
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 }
1350 return all(promises).then(function () {
1351 return { frames: frames };
1352 });
1353 },
1355 onReleaseMany: function (aRequest) {
1356 if (!aRequest.actors) {
1357 return { error: "missingParameter",
1358 message: "no actors were specified" };
1359 }
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 },
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 }
1385 let { url: originalSource,
1386 line: originalLine,
1387 column: originalColumn } = aRequest.location;
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 }
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 });
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 }
1436 if (url !== originalSource || line !== originalLine) {
1437 aResponse.actualLocation = { url: url, line: line };
1438 }
1440 return aResponse;
1441 });
1442 });
1443 },
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 },
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 }
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 }
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 */
1504 // Debugger.Script -> array of offset mappings
1505 let scriptsAndOffsetMappings = new Map();
1507 for (let script of scripts) {
1508 this._findClosestOffsetMappings(aLocation,
1509 script,
1510 scriptsAndOffsetMappings);
1511 }
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 }
1521 return {
1522 actor: actor.actorID
1523 };
1524 }
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 */
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 });
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);
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 }
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 },
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.
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 }
1646 let offsetMappings = aScript.getAllColumnOffsets()
1647 .filter(({ lineNumber }) => lineNumber === aTargetLocation.line);
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 }
1661 for (let mapping of offsetMappings) {
1662 let currentDistance = Math.abs(aTargetLocation.column - mapping.columnNumber);
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 },
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 }
1691 return all([this.sources.sourcesForScript(script)
1692 for (script of sourcesToScripts.values())]);
1693 },
1695 onSources: function (aRequest) {
1696 return this._discoverSources().then(() => {
1697 return {
1698 sources: [s.form() for (s of this.sources.iter())]
1699 };
1700 });
1701 },
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 },
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 }
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" };
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);
1746 // Start a nested event loop.
1747 this._pushThreadPause();
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 },
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 }
1770 let els = Cc["@mozilla.org/eventlistenerservice;1"]
1771 .getService(Ci.nsIEventListenerService);
1773 let nodes = this.global.document.getElementsByTagName("*");
1774 nodes = [this.global].concat([].slice.call(nodes));
1775 let listeners = [];
1777 for (let node of nodes) {
1778 let handlers = els.getListenerInfoFor(node);
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 }
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 },
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 }
1819 if (this._framePool.has(aFrameID)) {
1820 return this._framePool.get(aFrameID).frame;
1821 }
1823 return undefined;
1824 },
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.
1834 if (this.state === "paused") {
1835 return undefined;
1836 }
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 }
1858 this._state = "paused";
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);
1866 // Give children of the pause pool a quick link back to the
1867 // thread...
1868 this._pausePool.threadActor = this;
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);
1875 // Update the list of frames.
1876 let poppedFrames = this._updateFrames();
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 }
1886 if (poppedFrames) {
1887 packet.poppedFrames = poppedFrames;
1888 }
1890 return packet;
1891 },
1893 _resumed: function () {
1894 this._state = "running";
1896 // Drop the actors in the pause actor pool.
1897 this.conn.removeActorPool(this._pausePool);
1899 this._pausePool = null;
1900 this._pauseActor = null;
1902 return { from: this.actorID, type: "resumed" };
1903 },
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 = [];
1913 // Create the actor pool that will hold the still-living frames.
1914 let framePool = new ActorPool(this.conn);
1915 let frameList = [];
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 }
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 }
1932 this._frameActors = frameList;
1933 this._framePool = framePool;
1934 this.conn.addActorPool(framePool);
1936 return popped;
1937 },
1939 _createFrameActor: function (aFrame) {
1940 if (aFrame.actor) {
1941 return aFrame.actor;
1942 }
1944 let actor = new FrameActor(aFrame, this);
1945 this._frameActors.push(actor);
1946 this._framePool.addActor(actor);
1947 aFrame.actor = actor;
1949 return actor;
1950 },
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 }
1967 if (aEnvironment.actor) {
1968 return aEnvironment.actor;
1969 }
1971 let actor = new EnvironmentActor(aEnvironment, this);
1972 aPool.addActor(actor);
1973 aEnvironment.actor = actor;
1975 return actor;
1976 },
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 }
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 },
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 },
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 }
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 }
2056 let actor = new PauseScopedObjectActor(aValue, this);
2057 aPool.addActor(actor);
2058 aPool.objectActors.set(aValue, actor);
2059 return actor.grip();
2060 },
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 }
2073 return this.objectGrip(aValue, this._pausePool);
2074 },
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 },
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 }
2102 if (!aRequest.actors) {
2103 return { error: "missingParameter",
2104 message: "no actors were specified" };
2105 }
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 },
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 }
2129 if (aPool.longStringActors.hasOwnProperty(aString)) {
2130 return aPool.longStringActors[aString].grip();
2131 }
2133 let actor = new LongStringActor(aString, this);
2134 aPool.addActor(actor);
2135 aPool.longStringActors[aString] = actor;
2136 return actor.grip();
2137 },
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 },
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 },
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 },
2170 // JS Debugger API hooks.
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 },
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));
2198 return this.sources.isBlackBoxed(url) || aFrame.onStep
2199 ? undefined
2200 : this._pauseAndRespond(aFrame, { type: "debuggerStatement" });
2201 },
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 }
2221 if (willBeCaught && this._options.ignoreCaughtExceptions) {
2222 return undefined;
2223 }
2225 const generatedLocation = getFrameLocation(aFrame);
2226 const { url } = this.synchronize(this.sources.getOriginalLocation(
2227 generatedLocation));
2229 if (this.sources.isBlackBoxed(url)) {
2230 return undefined;
2231 }
2233 try {
2234 let packet = this._paused(aFrame);
2235 if (!packet) {
2236 return undefined;
2237 }
2239 packet.why = { type: "exception",
2240 exception: this.createValueGrip(aValue) };
2241 this.conn.send(packet);
2243 this._pushThreadPause();
2244 } catch(e) {
2245 reportError(e, "Got an exception during TA_onExceptionUnwind: ");
2246 }
2248 return undefined;
2249 },
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);
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 }
2270 this.sources.sourcesForScript(aScript);
2271 },
2273 onNewSource: function (aSource) {
2274 this.conn.send({
2275 from: this.actorID,
2276 type: "newSource",
2277 source: aSource.form()
2278 });
2279 },
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 },
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 }
2312 for (let s of this.dbg.findScripts()) {
2313 this._addScript(s);
2314 }
2315 },
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 }
2329 // Set any stored breakpoints.
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 }
2343 return true;
2344 },
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 }
2374 };
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 };
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 }
2407 PauseActor.prototype = {
2408 actorPrefix: "pause"
2409 };
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 }
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 };
2440 PauseScopedActor.prototype = {
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 },
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 };
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;
2480 case "chrome":
2481 let resolved = Cc["@mozilla.org/chrome/chrome-registry;1"].
2482 getService(Ci.nsIChromeRegistry).convertChromeURL(aURI);
2483 return resolveURIToLocalPath(resolved);
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);
2491 default:
2492 return null;
2493 }
2494 }
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;
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);
2527 this._mapSourceToAddon();
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 }
2540 SourceActor.prototype = {
2541 constructor: SourceActor,
2542 actorPrefix: "source",
2544 _oldSourceMap: null,
2545 _init: null,
2546 _addonID: null,
2547 _addonPath: null,
2549 get threadActor() this._threadActor,
2550 get url() this._url,
2551 get addonID() this._addonID,
2552 get addonPath() this._addonPath,
2554 get prettyPrintWorker() {
2555 return this.threadActor.prettyPrintWorker;
2556 },
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 },
2570 disconnect: function () {
2571 if (this.registeredPool && this.registeredPool.sourceActors) {
2572 delete this.registeredPool.sourceActors[this.actorID];
2573 }
2574 },
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 }
2585 let localURI = resolveURIToLocalPath(nsuri);
2587 let id = {};
2588 if (localURI && mapURIToAddonID(localURI, id)) {
2589 this._addonID = id.value;
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;
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 }
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 }
2617 this._addonPath = path;
2618 }
2619 }
2620 },
2622 _getSourceText: function () {
2623 const toResolvedContent = t => resolve({
2624 content: t,
2625 contentType: this._contentType
2626 });
2628 let sc;
2629 if (this._sourceMap && (sc = this._sourceMap.sourceContentFor(this._url))) {
2630 return toResolvedContent(sc);
2631 }
2633 if (this._text) {
2634 return toResolvedContent(this._text);
2635 }
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 });
2642 // Record the contentType we just learned during fetching
2643 sourceFetched.then(({ contentType }) => {
2644 this._contentType = contentType;
2645 });
2647 return sourceFetched;
2648 },
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 },
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 },
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();
2718 const onReply = ({ data }) => {
2719 if (data.id !== id) {
2720 return;
2721 }
2722 this.prettyPrintWorker.removeEventListener("message", onReply, false);
2724 if (data.error) {
2725 deferred.reject(new Error(data.error));
2726 } else {
2727 deferred.resolve(data);
2728 }
2729 };
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 });
2739 return deferred.promise;
2740 };
2741 },
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);
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.
2777 const getOrigPos = consumer.originalPositionFor.bind(consumer);
2778 const getGenPos = consumer.generatedPositionFor.bind(consumer);
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 };
2790 consumer.generatedPositionFor = ({ line, column }) => getOrigPos({
2791 line: line,
2792 column: column
2793 });
2795 return {
2796 code: code,
2797 map: consumer
2798 };
2799 });
2800 },
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 },
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 },
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 },
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 };
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 };
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 }
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 }
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 }
2912 let message = DevToolsUtils.getProperty(aObj, "message");
2913 if (isObject(message)) {
2914 message = stringify(message);
2915 }
2917 if (message === "" || message === undefined) {
2918 return name;
2919 }
2920 return name + ": " + message;
2921 }
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 }
2941 // Used to prevent infinite recursion when an array is found inside itself.
2942 let seen = null;
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 }
2968 seen.add(obj);
2970 const len = DevToolsUtils.getProperty(obj, "length");
2971 let string = "";
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 }
2986 if (i < len - 1) {
2987 string += ",";
2988 }
2989 }
2990 }
2992 if (topLevel) {
2993 seen = null;
2994 }
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>";
3004 return '[Exception... "' + message + '" ' +
3005 'code: "' + code +'" ' +
3006 'nsresult: "0x' + result + ' (' + name + ')"]';
3007 }
3008 };
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 }
3025 ObjectActor.prototype = {
3026 actorPrefix: "obj",
3028 /**
3029 * Returns a grip for this actor for returning in a protocol message.
3030 */
3031 grip: function () {
3032 this.threadActor._gripDepth++;
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 };
3043 if (this.obj.class != "DeadObject") {
3044 let raw = Cu.unwaiveXrays(this.obj.unsafeDereference());
3045 if (!DevToolsUtils.isSafeJSObject(raw)) {
3046 raw = null;
3047 }
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 }
3062 this.threadActor._gripDepth--;
3063 return g;
3064 },
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 },
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 }
3092 if (!this.obj.script) {
3093 return {
3094 from: this.actorID,
3095 error: "noScript",
3096 message: this.actorID + " has no Debugger.Script"
3097 };
3098 }
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 };
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 },
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 },
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 },
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;
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 }
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 }
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 }
3228 obj = obj.proto;
3229 level++;
3230 }
3232 return safeGetterValues;
3233 },
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 }
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 }
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 }
3273 if (DevToolsUtils.hasSafeGetter(desc)) {
3274 getters.add(name);
3275 }
3276 }
3278 aObject._safeGetters = getters;
3279 return getters;
3280 },
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 },
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 }
3306 return { from: this.actorID,
3307 descriptor: this._propertyDescriptor(aRequest.name) };
3308 },
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 },
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 }
3352 if (!desc || aOnlyEnumerable && !desc.enumerable) {
3353 return undefined;
3354 }
3356 let retval = {
3357 configurable: desc.configurable,
3358 enumerable: desc.enumerable
3359 };
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 },
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 }
3388 return { from: this.actorID,
3389 decompiledCode: this.obj.decompile(!!aRequest.pretty) };
3390 },
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 }
3405 return { parameterNames: this.obj.parameterNames };
3406 },
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 },
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 }
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 }
3439 return { from: this.actorID, scope: envActor.form() };
3440 }
3441 };
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 };
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 }
3482 aGrip.displayString = result.value;
3483 return true;
3484 }
3486 return true;
3487 }],
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 }
3496 return false;
3497 }],
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 }
3506 return false;
3507 }],
3509 Function: [function({obj, threadActor}, aGrip) {
3510 if (obj.name) {
3511 aGrip.name = obj.name;
3512 }
3514 if (obj.displayName) {
3515 aGrip.displayName = obj.displayName.substr(0, 500);
3516 }
3518 if (obj.parameterNames) {
3519 aGrip.parameterNames = obj.parameterNames;
3520 }
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 }
3533 if (userDisplayName && typeof userDisplayName.value == "string" &&
3534 userDisplayName.value) {
3535 aGrip.userDisplayName = threadActor.createValueGrip(userDisplayName.value);
3536 }
3538 return true;
3539 }],
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 }
3547 let str = RegExp.prototype.toString.call(obj.unsafeDereference());
3548 aGrip.displayString = threadActor.createValueGrip(str);
3549 return true;
3550 }],
3552 Date: [function({obj, threadActor}, aGrip) {
3553 if (!obj.proto || obj.proto.class != "Date") {
3554 return false;
3555 }
3557 let time = Date.prototype.getTime.call(obj.unsafeDereference());
3559 aGrip.preview = {
3560 timestamp: threadActor.createValueGrip(time),
3561 };
3562 return true;
3563 }],
3565 Array: [function({obj, threadActor}, aGrip) {
3566 let length = DevToolsUtils.getProperty(obj, "length");
3567 if (typeof length != "number") {
3568 return false;
3569 }
3571 aGrip.preview = {
3572 kind: "ArrayLike",
3573 length: length,
3574 };
3576 if (threadActor._gripDepth > 1) {
3577 return true;
3578 }
3580 let raw = obj.unsafeDereference();
3581 let items = aGrip.preview.items = [];
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 }
3591 if (items.length == OBJECT_PREVIEW_MAX_ITEMS) {
3592 break;
3593 }
3594 }
3596 return true;
3597 }], // Array
3599 Set: [function({obj, threadActor}, aGrip) {
3600 let size = DevToolsUtils.getProperty(obj, "size");
3601 if (typeof size != "number") {
3602 return false;
3603 }
3605 aGrip.preview = {
3606 kind: "ArrayLike",
3607 length: size,
3608 };
3610 // Avoid recursive object grips.
3611 if (threadActor._gripDepth > 1) {
3612 return true;
3613 }
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 }
3625 return true;
3626 }], // Set
3628 Map: [function({obj, threadActor}, aGrip) {
3629 let size = DevToolsUtils.getProperty(obj, "size");
3630 if (typeof size != "number") {
3631 return false;
3632 }
3634 aGrip.preview = {
3635 kind: "MapLike",
3636 size: size,
3637 };
3639 if (threadActor._gripDepth > 1) {
3640 return true;
3641 }
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 }
3655 return true;
3656 }], // Map
3658 DOMStringMap: [function({obj, threadActor}, aGrip, aRawObj) {
3659 if (!aRawObj) {
3660 return false;
3661 }
3663 let keys = obj.getOwnPropertyNames();
3664 aGrip.preview = {
3665 kind: "MapLike",
3666 size: keys.length,
3667 };
3669 if (threadActor._gripDepth > 1) {
3670 return true;
3671 }
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 }
3682 return true;
3683 }], // DOMStringMap
3684 }; // DebuggerServer.ObjectActorPreviewers
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 }
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 }
3717 if (v !== null) {
3718 v = aThreadActor.createValueGrip(makeDebuggeeValueIfNeeded(aObj, v));
3719 return { value: v };
3720 }
3722 return null;
3723 }
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 }
3732 let length = DevToolsUtils.getProperty(obj, "length");
3733 if (typeof length != "number") {
3734 return false;
3735 }
3737 aGrip.preview = {
3738 kind: "ArrayLike",
3739 length: length,
3740 };
3742 if (threadActor._gripDepth > 1) {
3743 return true;
3744 }
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 }
3755 return true;
3756 },
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 },
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 },
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 },
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 }
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 }
3828 aGrip.preview = {
3829 kind: "ObjectWithURL",
3830 url: threadActor.createValueGrip(url),
3831 };
3833 return true;
3834 },
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 }
3851 if (typeof aRawObj.length != "number") {
3852 return false;
3853 }
3855 aGrip.preview = {
3856 kind: "ArrayLike",
3857 length: aRawObj.length,
3858 };
3860 if (threadActor._gripDepth > 1) {
3861 return true;
3862 }
3864 let items = aGrip.preview.items = [];
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 }
3872 return true;
3873 }, // ArrayLike
3875 function CSSStyleDeclaration({obj, threadActor}, aGrip, aRawObj) {
3876 if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMCSSStyleDeclaration)) {
3877 return false;
3878 }
3880 aGrip.preview = {
3881 kind: "MapLike",
3882 size: aRawObj.length,
3883 };
3885 let entries = aGrip.preview.entries = [];
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 }
3894 return true;
3895 },
3897 function DOMNode({obj, threadActor}, aGrip, aRawObj) {
3898 if (obj.class == "Object" || !aRawObj || !(aRawObj instanceof Ci.nsIDOMNode)) {
3899 return false;
3900 }
3902 let preview = aGrip.preview = {
3903 kind: "DOMNode",
3904 nodeType: aRawObj.nodeType,
3905 nodeName: aRawObj.nodeName,
3906 };
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;
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 }
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 }
3945 return true;
3946 }, // DOMNode
3948 function DOMEvent({obj, threadActor}, aGrip, aRawObj) {
3949 if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMEvent)) {
3950 return false;
3951 }
3953 let preview = aGrip.preview = {
3954 kind: "DOMEvent",
3955 type: aRawObj.type,
3956 properties: Object.create(null),
3957 };
3959 if (threadActor._gripDepth < 2) {
3960 let target = obj.makeDebuggeeValue(aRawObj.target);
3961 preview.target = threadActor.createValueGrip(target);
3962 }
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;
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 }
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 }
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 }
4027 return true;
4028 }, // DOMEvent
4030 function DOMException({obj, threadActor}, aGrip, aRawObj) {
4031 if (!aRawObj || !(aRawObj instanceof Ci.nsIDOMDOMException)) {
4032 return false;
4033 }
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 };
4046 return true;
4047 },
4049 function GenericObject(aObjectActor, aGrip) {
4050 let {obj, threadActor} = aObjectActor;
4051 if (aGrip.preview || aGrip.displayString || threadActor._gripDepth > 1) {
4052 return false;
4053 }
4055 let i = 0, names = [];
4056 let preview = aGrip.preview = {
4057 kind: "Object",
4058 ownProperties: Object.create(null),
4059 };
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 }
4068 preview.ownPropertiesLength = names.length;
4070 for (let name of names) {
4071 let desc = aObjectActor._propertyDescriptor(name, true);
4072 if (!desc) {
4073 continue;
4074 }
4076 preview.ownProperties[name] = desc;
4077 if (++i == OBJECT_PREVIEW_MAX_ITEMS) {
4078 break;
4079 }
4080 }
4082 if (i < OBJECT_PREVIEW_MAX_ITEMS) {
4083 preview.safeGetterValues = aObjectActor.
4084 _findSafeGetterValues(preview.ownProperties,
4085 OBJECT_PREVIEW_MAX_ITEMS - i);
4086 }
4088 return true;
4089 }, // GenericObject
4090 ]; // DebuggerServer.ObjectActorPreviewers.Object
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 }
4101 PauseScopedObjectActor.prototype = Object.create(PauseScopedActor.prototype);
4103 update(PauseScopedObjectActor.prototype, ObjectActor.prototype);
4105 update(PauseScopedObjectActor.prototype, {
4106 constructor: PauseScopedObjectActor,
4107 actorPrefix: "pausedobj",
4109 onOwnPropertyNames:
4110 PauseScopedActor.withPaused(ObjectActor.prototype.onOwnPropertyNames),
4112 onPrototypeAndProperties:
4113 PauseScopedActor.withPaused(ObjectActor.prototype.onPrototypeAndProperties),
4115 onPrototype: PauseScopedActor.withPaused(ObjectActor.prototype.onPrototype),
4116 onProperty: PauseScopedActor.withPaused(ObjectActor.prototype.onProperty),
4117 onDecompile: PauseScopedActor.withPaused(ObjectActor.prototype.onDecompile),
4119 onDisplayString:
4120 PauseScopedActor.withPaused(ObjectActor.prototype.onDisplayString),
4122 onParameterNames:
4123 PauseScopedActor.withPaused(ObjectActor.prototype.onParameterNames),
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 }),
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 }
4149 this.release();
4150 return {};
4151 }),
4152 });
4154 update(PauseScopedObjectActor.prototype.requestTypes, {
4155 "threadGrip": PauseScopedObjectActor.prototype.onThreadGrip,
4156 });
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 }
4172 LongStringActor.prototype = {
4174 actorPrefix: "longString",
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 },
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 },
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 },
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 };
4226 LongStringActor.prototype.requestTypes = {
4227 "substring": LongStringActor.prototype.onSubstring,
4228 "release": LongStringActor.prototype.onRelease
4229 };
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 }
4246 FrameActor.prototype = {
4247 actorPrefix: "frame",
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 },
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 },
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 }
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 }
4292 if (!this.frame.older) {
4293 form.oldest = true;
4294 }
4296 return form;
4297 },
4299 _args: function () {
4300 if (!this.frame.arguments) {
4301 return [];
4302 }
4304 return [this.threadActor.createValueGrip(arg)
4305 for each (arg in this.frame.arguments)];
4306 },
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 }
4321 while (this.frame != this.threadActor.dbg.getNewestFrame()) {
4322 this.threadActor.dbg.getNewestFrame().pop();
4323 }
4324 this.frame.pop(aRequest.completionValue);
4326 // TODO: return the watches property when frame pop watch actors are
4327 // implemented.
4328 return { from: this.actorID };
4329 }
4330 };
4332 FrameActor.prototype.requestTypes = {
4333 "pop": FrameActor.prototype.onPop,
4334 };
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 }
4355 BreakpointActor.prototype = {
4356 actorPrefix: "breakpoint",
4357 condition: null,
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 },
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 },
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 },
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 }));
4414 if (this.threadActor.sources.isBlackBoxed(url)
4415 || aFrame.onStep
4416 || !this.isValidCondition(aFrame)) {
4417 return undefined;
4418 }
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 },
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 };
4447 BreakpointActor.prototype.requestTypes = {
4448 "delete": BreakpointActor.prototype.onDelete
4449 };
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 }
4468 EnvironmentActor.prototype = {
4469 actorPrefix: "environment",
4471 /**
4472 * Return an environment form for use in a protocol message.
4473 */
4474 form: function () {
4475 let form = { actor: this.actorID };
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 }
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 }
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 }
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 }
4502 // Shall we list this environment's bindings?
4503 if (this.obj.type == "declarative") {
4504 form.bindings = this._bindings();
4505 }
4507 return form;
4508 },
4510 /**
4511 * Return the identifier bindings object as required by the remote protocol
4512 * specification.
4513 */
4514 _bindings: function () {
4515 let bindings = { arguments: [], variables: {} };
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 }
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 = {};
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 }
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 };
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 }
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 }
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 }
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 };
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 }
4601 return bindings;
4602 },
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);
4616 if (!desc.writable) {
4617 return { error: "immutableBinding",
4618 message: "Changing the value of an immutable binding is not " +
4619 "allowed" };
4620 }*/
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 },
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 };
4645 EnvironmentActor.prototype.requestTypes = {
4646 "assign": EnvironmentActor.prototype.onAssign,
4647 "bindings": EnvironmentActor.prototype.onBindings
4648 };
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 };
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 });
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 }
4705 ChromeDebuggerActor.prototype = Object.create(ThreadActor.prototype);
4707 update(ChromeDebuggerActor.prototype, {
4708 constructor: ChromeDebuggerActor,
4710 // A constant prefix that will be used to form the actor ID by the server.
4711 actorPrefix: "chromeDebugger",
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,
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 },
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 });
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 */
4769 function AddonThreadActor(aConnect, aHooks, aAddonID) {
4770 this.addonID = aAddonID;
4771 ThreadActor.call(this, aHooks);
4772 }
4774 AddonThreadActor.prototype = Object.create(ThreadActor.prototype);
4776 update(AddonThreadActor.prototype, {
4777 constructor: AddonThreadActor,
4779 // A constant prefix that will be used to form the actor ID by the server.
4780 actorPrefix: "addonThread",
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 },
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 },
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 },
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 }
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 }
4825 return true;
4826 },
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 },
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 },
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 }
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 }
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 }
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 }
4911 let id = {};
4912 if (mapURIToAddonID(uri, id)) {
4913 return id.value === this.addonID;
4914 }
4915 }
4917 return false;
4918 }
4919 });
4921 AddonThreadActor.prototype.requestTypes = Object.create(ThreadActor.prototype.requestTypes);
4922 update(AddonThreadActor.prototype.requestTypes, {
4923 "attach": AddonThreadActor.prototype.onAttach
4924 });
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;
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 }
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();
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 }
4981 if (url in this._sourceActors) {
4982 return this._sourceActors[url];
4983 }
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 },
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 };
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 }
5029 return this.source(spec);
5030 },
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 }
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 },
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 },
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 },
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 },
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 },
5127 _dirname: function (aPath) {
5128 return Services.io.newURI(
5129 ".", null, Services.io.newURI(aPath, null, null)).spec;
5130 },
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;
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 }
5160 // No source map
5161 return resolve({
5162 url: url,
5163 line: line,
5164 column: column
5165 });
5166 },
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 }
5194 // No source map
5195 return resolve({
5196 url: url,
5197 line: line,
5198 column: column
5199 });
5200 },
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 },
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 },
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 },
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 },
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 },
5253 /**
5254 * Return the indent the given URL was pretty printed by.
5255 */
5256 prettyPrintIndent: function (aURL) {
5257 return ThreadSources._prettyPrintedSources.get(aURL);
5258 },
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 },
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 },
5283 iter: function* () {
5284 for (let url in this._sourceActors) {
5285 yield this._sourceActors[url];
5286 }
5287 }
5288 };
5290 // Utility functions.
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 }
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 }
5314 return bestOffsetMapping.columnNumber;
5315 }
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 }
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);
5350 if (desc) {
5351 Object.defineProperty(aTarget, key, desc);
5352 }
5353 }
5354 }
5356 /**
5357 * Returns true if its argument is not null.
5358 */
5359 function isNotNull(aThing) {
5360 return aThing !== null;
5361 }
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;
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 }
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 }
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;
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 }
5448 charset = channel.contentCharset;
5449 contentType = channel.contentType;
5450 deferred.resolve(chunks.join(""));
5451 }
5452 };
5454 channel.loadFlags = aOptions.loadFromCache
5455 ? channel.LOAD_FROM_CACHE
5456 : channel.LOAD_BYPASS_CACHE;
5457 channel.asyncOpen(streamListener, null);
5458 break;
5459 }
5461 return deferred.promise.then(source => {
5462 return {
5463 content: convertToUnicode(source, charset),
5464 contentType: contentType
5465 };
5466 });
5467 }
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 }
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 }
5504 // The following are copied here verbatim from css-logic.js, until we create a
5505 // server-friendly helper module.
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 }
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 }
5530 if (ele.parentNode == null) {
5531 console.log('danger: ' + tagName);
5532 }
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 }
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 + ')';
5565 return selector;
5566 };
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 }
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 }
5601 function getInnerId(window) {
5602 return window.QueryInterface(Ci.nsIInterfaceRequestor).
5603 getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
5604 };