browser/devtools/debugger/debugger-controller.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:caf00f2f1508
1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 "use strict";
7
8 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
9
10 const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
11 const NEW_SOURCE_IGNORED_URLS = ["debugger eval code", "self-hosted", "XStringBundle"];
12 const NEW_SOURCE_DISPLAY_DELAY = 200; // ms
13 const FETCH_SOURCE_RESPONSE_DELAY = 200; // ms
14 const FETCH_EVENT_LISTENERS_DELAY = 200; // ms
15 const FRAME_STEP_CLEAR_DELAY = 100; // ms
16 const CALL_STACK_PAGE_SIZE = 25; // frames
17
18 // The panel's window global is an EventEmitter firing the following events:
19 const EVENTS = {
20 // When the debugger's source editor instance finishes loading or unloading.
21 EDITOR_LOADED: "Debugger:EditorLoaded",
22 EDITOR_UNLOADED: "Debugger:EditorUnoaded",
23
24 // When new sources are received from the debugger server.
25 NEW_SOURCE: "Debugger:NewSource",
26 SOURCES_ADDED: "Debugger:SourcesAdded",
27
28 // When a source is shown in the source editor.
29 SOURCE_SHOWN: "Debugger:EditorSourceShown",
30 SOURCE_ERROR_SHOWN: "Debugger:EditorSourceErrorShown",
31
32 // When the editor has shown a source and set the line / column position
33 EDITOR_LOCATION_SET: "Debugger:EditorLocationSet",
34
35 // When scopes, variables, properties and watch expressions are fetched and
36 // displayed in the variables view.
37 FETCHED_SCOPES: "Debugger:FetchedScopes",
38 FETCHED_VARIABLES: "Debugger:FetchedVariables",
39 FETCHED_PROPERTIES: "Debugger:FetchedProperties",
40 FETCHED_BUBBLE_PROPERTIES: "Debugger:FetchedBubbleProperties",
41 FETCHED_WATCH_EXPRESSIONS: "Debugger:FetchedWatchExpressions",
42
43 // When a breakpoint has been added or removed on the debugger server.
44 BREAKPOINT_ADDED: "Debugger:BreakpointAdded",
45 BREAKPOINT_REMOVED: "Debugger:BreakpointRemoved",
46
47 // When a breakpoint has been shown or hidden in the source editor.
48 BREAKPOINT_SHOWN: "Debugger:BreakpointShown",
49 BREAKPOINT_HIDDEN: "Debugger:BreakpointHidden",
50
51 // When a conditional breakpoint's popup is showing or hiding.
52 CONDITIONAL_BREAKPOINT_POPUP_SHOWING: "Debugger:ConditionalBreakpointPopupShowing",
53 CONDITIONAL_BREAKPOINT_POPUP_HIDING: "Debugger:ConditionalBreakpointPopupHiding",
54
55 // When event listeners are fetched or event breakpoints are updated.
56 EVENT_LISTENERS_FETCHED: "Debugger:EventListenersFetched",
57 EVENT_BREAKPOINTS_UPDATED: "Debugger:EventBreakpointsUpdated",
58
59 // When a file search was performed.
60 FILE_SEARCH_MATCH_FOUND: "Debugger:FileSearch:MatchFound",
61 FILE_SEARCH_MATCH_NOT_FOUND: "Debugger:FileSearch:MatchNotFound",
62
63 // When a function search was performed.
64 FUNCTION_SEARCH_MATCH_FOUND: "Debugger:FunctionSearch:MatchFound",
65 FUNCTION_SEARCH_MATCH_NOT_FOUND: "Debugger:FunctionSearch:MatchNotFound",
66
67 // When a global text search was performed.
68 GLOBAL_SEARCH_MATCH_FOUND: "Debugger:GlobalSearch:MatchFound",
69 GLOBAL_SEARCH_MATCH_NOT_FOUND: "Debugger:GlobalSearch:MatchNotFound",
70
71 // After the stackframes are cleared and debugger won't pause anymore.
72 AFTER_FRAMES_CLEARED: "Debugger:AfterFramesCleared",
73
74 // When the options popup is showing or hiding.
75 OPTIONS_POPUP_SHOWING: "Debugger:OptionsPopupShowing",
76 OPTIONS_POPUP_HIDDEN: "Debugger:OptionsPopupHidden",
77
78 // When the widgets layout has been changed.
79 LAYOUT_CHANGED: "Debugger:LayoutChanged"
80 };
81
82 // Descriptions for what a stack frame represents after the debugger pauses.
83 const FRAME_TYPE = {
84 NORMAL: 0,
85 CONDITIONAL_BREAKPOINT_EVAL: 1,
86 WATCH_EXPRESSIONS_EVAL: 2,
87 PUBLIC_CLIENT_EVAL: 3
88 };
89
90 Cu.import("resource://gre/modules/Services.jsm");
91 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
92 Cu.import("resource://gre/modules/devtools/event-emitter.js");
93 Cu.import("resource://gre/modules/Task.jsm");
94 Cu.import("resource:///modules/devtools/SimpleListWidget.jsm");
95 Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm");
96 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
97 Cu.import("resource:///modules/devtools/VariablesView.jsm");
98 Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
99 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
100
101 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
102 const promise = require("devtools/toolkit/deprecated-sync-thenables");
103 const Editor = require("devtools/sourceeditor/editor");
104 const DebuggerEditor = require("devtools/sourceeditor/debugger.js");
105 const {Tooltip} = require("devtools/shared/widgets/Tooltip");
106 const FastListWidget = require("devtools/shared/widgets/FastListWidget");
107
108 XPCOMUtils.defineLazyModuleGetter(this, "Parser",
109 "resource:///modules/devtools/Parser.jsm");
110
111 XPCOMUtils.defineLazyModuleGetter(this, "devtools",
112 "resource://gre/modules/devtools/Loader.jsm");
113
114 XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils",
115 "resource://gre/modules/devtools/DevToolsUtils.jsm");
116
117 XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
118 "resource://gre/modules/ShortcutUtils.jsm");
119
120 Object.defineProperty(this, "NetworkHelper", {
121 get: function() {
122 return devtools.require("devtools/toolkit/webconsole/network-helper");
123 },
124 configurable: true,
125 enumerable: true
126 });
127
128 /**
129 * Object defining the debugger controller components.
130 */
131 let DebuggerController = {
132 /**
133 * Initializes the debugger controller.
134 */
135 initialize: function() {
136 dumpn("Initializing the DebuggerController");
137
138 this.startupDebugger = this.startupDebugger.bind(this);
139 this.shutdownDebugger = this.shutdownDebugger.bind(this);
140 this._onTabNavigated = this._onTabNavigated.bind(this);
141 this._onTabDetached = this._onTabDetached.bind(this);
142 },
143
144 /**
145 * Initializes the view.
146 *
147 * @return object
148 * A promise that is resolved when the debugger finishes startup.
149 */
150 startupDebugger: function() {
151 if (this._startup) {
152 return this._startup;
153 }
154
155 return this._startup = DebuggerView.initialize();
156 },
157
158 /**
159 * Destroys the view and disconnects the debugger client from the server.
160 *
161 * @return object
162 * A promise that is resolved when the debugger finishes shutdown.
163 */
164 shutdownDebugger: function() {
165 if (this._shutdown) {
166 return this._shutdown;
167 }
168
169 return this._shutdown = DebuggerView.destroy().then(() => {
170 DebuggerView.destroy();
171 this.SourceScripts.disconnect();
172 this.StackFrames.disconnect();
173 this.ThreadState.disconnect();
174 this.Tracer.disconnect();
175 this.disconnect();
176 });
177 },
178
179 /**
180 * Initiates remote debugging based on the current target, wiring event
181 * handlers as necessary.
182 *
183 * @return object
184 * A promise that is resolved when the debugger finishes connecting.
185 */
186 connect: function() {
187 if (this._connection) {
188 return this._connection;
189 }
190
191 let startedDebugging = promise.defer();
192 this._connection = startedDebugging.promise;
193
194 let target = this._target;
195 let { client, form: { chromeDebugger, traceActor, addonActor } } = target;
196 target.on("close", this._onTabDetached);
197 target.on("navigate", this._onTabNavigated);
198 target.on("will-navigate", this._onTabNavigated);
199 this.client = client;
200
201 if (addonActor) {
202 this._startAddonDebugging(addonActor, startedDebugging.resolve);
203 } else if (target.chrome) {
204 this._startChromeDebugging(chromeDebugger, startedDebugging.resolve);
205 } else {
206 this._startDebuggingTab(startedDebugging.resolve);
207 const startedTracing = promise.defer();
208 if (Prefs.tracerEnabled && traceActor) {
209 this._startTracingTab(traceActor, startedTracing.resolve);
210 } else {
211 startedTracing.resolve();
212 }
213
214 return promise.all([startedDebugging.promise, startedTracing.promise]);
215 }
216
217 return startedDebugging.promise;
218 },
219
220 /**
221 * Disconnects the debugger client and removes event handlers as necessary.
222 */
223 disconnect: function() {
224 // Return early if the client didn't even have a chance to instantiate.
225 if (!this.client) {
226 return;
227 }
228
229 this._connection = null;
230 this.client = null;
231 this.activeThread = null;
232 },
233
234 /**
235 * Called for each location change in the debugged tab.
236 *
237 * @param string aType
238 * Packet type.
239 * @param object aPacket
240 * Packet received from the server.
241 */
242 _onTabNavigated: function(aType, aPacket) {
243 switch (aType) {
244 case "will-navigate": {
245 // Reset UI.
246 DebuggerView.handleTabNavigation();
247
248 // Discard all the cached sources *before* the target starts navigating.
249 // Sources may be fetched during navigation, in which case we don't
250 // want to hang on to the old source contents.
251 DebuggerController.SourceScripts.clearCache();
252 DebuggerController.Parser.clearCache();
253 SourceUtils.clearCache();
254
255 // Prevent performing any actions that were scheduled before navigation.
256 clearNamedTimeout("new-source");
257 clearNamedTimeout("event-breakpoints-update");
258 clearNamedTimeout("event-listeners-fetch");
259 break;
260 }
261 case "navigate": {
262 this.ThreadState.handleTabNavigation();
263 this.StackFrames.handleTabNavigation();
264 this.SourceScripts.handleTabNavigation();
265 break;
266 }
267 }
268 },
269
270 /**
271 * Called when the debugged tab is closed.
272 */
273 _onTabDetached: function() {
274 this.shutdownDebugger();
275 },
276
277 /**
278 * Warn if resuming execution produced a wrongOrder error.
279 */
280 _ensureResumptionOrder: function(aResponse) {
281 if (aResponse.error == "wrongOrder") {
282 DebuggerView.Toolbar.showResumeWarning(aResponse.lastPausedUrl);
283 }
284 },
285
286 /**
287 * Sets up a debugging session.
288 *
289 * @param function aCallback
290 * A function to invoke once the client attaches to the active thread.
291 */
292 _startDebuggingTab: function(aCallback) {
293 this._target.activeTab.attachThread({
294 useSourceMaps: Prefs.sourceMapsEnabled
295 }, (aResponse, aThreadClient) => {
296 if (!aThreadClient) {
297 Cu.reportError("Couldn't attach to thread: " + aResponse.error);
298 return;
299 }
300 this.activeThread = aThreadClient;
301
302 this.ThreadState.connect();
303 this.StackFrames.connect();
304 this.SourceScripts.connect();
305 if (aThreadClient.paused) {
306 aThreadClient.resume(this._ensureResumptionOrder);
307 }
308
309 if (aCallback) {
310 aCallback();
311 }
312 });
313 },
314
315 /**
316 * Sets up an addon debugging session.
317 *
318 * @param object aAddonActor
319 * The actor for the addon that is being debugged.
320 * @param function aCallback
321 * A function to invoke once the client attaches to the active thread.
322 */
323 _startAddonDebugging: function(aAddonActor, aCallback) {
324 this.client.attachAddon(aAddonActor, (aResponse) => {
325 return this._startChromeDebugging(aResponse.threadActor, aCallback);
326 });
327 },
328
329 /**
330 * Sets up a chrome debugging session.
331 *
332 * @param object aChromeDebugger
333 * The remote protocol grip of the chrome debugger.
334 * @param function aCallback
335 * A function to invoke once the client attaches to the active thread.
336 */
337 _startChromeDebugging: function(aChromeDebugger, aCallback) {
338 this.client.attachThread(aChromeDebugger, (aResponse, aThreadClient) => {
339 if (!aThreadClient) {
340 Cu.reportError("Couldn't attach to thread: " + aResponse.error);
341 return;
342 }
343 this.activeThread = aThreadClient;
344
345 this.ThreadState.connect();
346 this.StackFrames.connect();
347 this.SourceScripts.connect();
348 if (aThreadClient.paused) {
349 aThreadClient.resume(this._ensureResumptionOrder);
350 }
351
352 if (aCallback) {
353 aCallback();
354 }
355 }, { useSourceMaps: Prefs.sourceMapsEnabled });
356 },
357
358 /**
359 * Sets up an execution tracing session.
360 *
361 * @param object aTraceActor
362 * The remote protocol grip of the trace actor.
363 * @param function aCallback
364 * A function to invoke once the client attaches to the tracer.
365 */
366 _startTracingTab: function(aTraceActor, aCallback) {
367 this.client.attachTracer(aTraceActor, (response, traceClient) => {
368 if (!traceClient) {
369 DevToolsUtils.reportException("DebuggerController._startTracingTab",
370 new Error("Failed to attach to tracing actor."));
371 return;
372 }
373
374 this.traceClient = traceClient;
375 this.Tracer.connect();
376
377 if (aCallback) {
378 aCallback();
379 }
380 });
381 },
382
383 /**
384 * Detach and reattach to the thread actor with useSourceMaps true, blow
385 * away old sources and get them again.
386 */
387 reconfigureThread: function(aUseSourceMaps) {
388 this.activeThread.reconfigure({ useSourceMaps: aUseSourceMaps }, aResponse => {
389 if (aResponse.error) {
390 let msg = "Couldn't reconfigure thread: " + aResponse.message;
391 Cu.reportError(msg);
392 dumpn(msg);
393 return;
394 }
395
396 // Reset the view and fetch all the sources again.
397 DebuggerView.handleTabNavigation();
398 this.SourceScripts.handleTabNavigation();
399
400 // Update the stack frame list.
401 if (this.activeThread.paused) {
402 this.activeThread._clearFrames();
403 this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
404 }
405 });
406 },
407
408 _startup: null,
409 _shutdown: null,
410 _connection: null,
411 client: null,
412 activeThread: null
413 };
414
415 /**
416 * ThreadState keeps the UI up to date with the state of the
417 * thread (paused/attached/etc.).
418 */
419 function ThreadState() {
420 this._update = this._update.bind(this);
421 }
422
423 ThreadState.prototype = {
424 get activeThread() DebuggerController.activeThread,
425
426 /**
427 * Connect to the current thread client.
428 */
429 connect: function() {
430 dumpn("ThreadState is connecting...");
431 this.activeThread.addListener("paused", this._update);
432 this.activeThread.addListener("resumed", this._update);
433 this.activeThread.pauseOnExceptions(Prefs.pauseOnExceptions,
434 Prefs.ignoreCaughtExceptions);
435 this.handleTabNavigation();
436 },
437
438 /**
439 * Disconnect from the client.
440 */
441 disconnect: function() {
442 if (!this.activeThread) {
443 return;
444 }
445 dumpn("ThreadState is disconnecting...");
446 this.activeThread.removeListener("paused", this._update);
447 this.activeThread.removeListener("resumed", this._update);
448 },
449
450 /**
451 * Handles any initialization on a tab navigation event issued by the client.
452 */
453 handleTabNavigation: function() {
454 if (!this.activeThread) {
455 return;
456 }
457 dumpn("Handling tab navigation in the ThreadState");
458 this._update();
459 },
460
461 /**
462 * Update the UI after a thread state change.
463 */
464 _update: function(aEvent) {
465 DebuggerView.Toolbar.toggleResumeButtonState(this.activeThread.state);
466
467 if (gTarget && (aEvent == "paused" || aEvent == "resumed")) {
468 gTarget.emit("thread-" + aEvent);
469 }
470 }
471 };
472
473 /**
474 * Keeps the stack frame list up-to-date, using the thread client's
475 * stack frame cache.
476 */
477 function StackFrames() {
478 this._onPaused = this._onPaused.bind(this);
479 this._onResumed = this._onResumed.bind(this);
480 this._onFrames = this._onFrames.bind(this);
481 this._onFramesCleared = this._onFramesCleared.bind(this);
482 this._onBlackBoxChange = this._onBlackBoxChange.bind(this);
483 this._onPrettyPrintChange = this._onPrettyPrintChange.bind(this);
484 this._afterFramesCleared = this._afterFramesCleared.bind(this);
485 this.evaluate = this.evaluate.bind(this);
486 }
487
488 StackFrames.prototype = {
489 get activeThread() DebuggerController.activeThread,
490 currentFrameDepth: -1,
491 _currentFrameDescription: FRAME_TYPE.NORMAL,
492 _syncedWatchExpressions: null,
493 _currentWatchExpressions: null,
494 _currentBreakpointLocation: null,
495 _currentEvaluation: null,
496 _currentException: null,
497 _currentReturnedValue: null,
498
499 /**
500 * Connect to the current thread client.
501 */
502 connect: function() {
503 dumpn("StackFrames is connecting...");
504 this.activeThread.addListener("paused", this._onPaused);
505 this.activeThread.addListener("resumed", this._onResumed);
506 this.activeThread.addListener("framesadded", this._onFrames);
507 this.activeThread.addListener("framescleared", this._onFramesCleared);
508 this.activeThread.addListener("blackboxchange", this._onBlackBoxChange);
509 this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange);
510 this.handleTabNavigation();
511 },
512
513 /**
514 * Disconnect from the client.
515 */
516 disconnect: function() {
517 if (!this.activeThread) {
518 return;
519 }
520 dumpn("StackFrames is disconnecting...");
521 this.activeThread.removeListener("paused", this._onPaused);
522 this.activeThread.removeListener("resumed", this._onResumed);
523 this.activeThread.removeListener("framesadded", this._onFrames);
524 this.activeThread.removeListener("framescleared", this._onFramesCleared);
525 this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange);
526 this.activeThread.removeListener("prettyprintchange", this._onPrettyPrintChange);
527 clearNamedTimeout("frames-cleared");
528 },
529
530 /**
531 * Handles any initialization on a tab navigation event issued by the client.
532 */
533 handleTabNavigation: function() {
534 dumpn("Handling tab navigation in the StackFrames");
535 // Nothing to do here yet.
536 },
537
538 /**
539 * Handler for the thread client's paused notification.
540 *
541 * @param string aEvent
542 * The name of the notification ("paused" in this case).
543 * @param object aPacket
544 * The response packet.
545 */
546 _onPaused: function(aEvent, aPacket) {
547 switch (aPacket.why.type) {
548 // If paused by a breakpoint, store the breakpoint location.
549 case "breakpoint":
550 this._currentBreakpointLocation = aPacket.frame.where;
551 break;
552 // If paused by a client evaluation, store the evaluated value.
553 case "clientEvaluated":
554 this._currentEvaluation = aPacket.why.frameFinished;
555 break;
556 // If paused by an exception, store the exception value.
557 case "exception":
558 this._currentException = aPacket.why.exception;
559 break;
560 // If paused while stepping out of a frame, store the returned value or
561 // thrown exception.
562 case "resumeLimit":
563 if (!aPacket.why.frameFinished) {
564 break;
565 } else if (aPacket.why.frameFinished.throw) {
566 this._currentException = aPacket.why.frameFinished.throw;
567 } else if (aPacket.why.frameFinished.return) {
568 this._currentReturnedValue = aPacket.why.frameFinished.return;
569 }
570 break;
571 }
572
573 this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
574 DebuggerView.editor.focus();
575 },
576
577 /**
578 * Handler for the thread client's resumed notification.
579 */
580 _onResumed: function() {
581 // Prepare the watch expression evaluation string for the next pause.
582 if (this._currentFrameDescription != FRAME_TYPE.WATCH_EXPRESSIONS_EVAL) {
583 this._currentWatchExpressions = this._syncedWatchExpressions;
584 }
585 },
586
587 /**
588 * Handler for the thread client's framesadded notification.
589 */
590 _onFrames: function() {
591 // Ignore useless notifications.
592 if (!this.activeThread || !this.activeThread.cachedFrames.length) {
593 return;
594 }
595
596 let waitForNextPause = false;
597 let breakLocation = this._currentBreakpointLocation;
598 let watchExpressions = this._currentWatchExpressions;
599 let client = DebuggerController.activeThread.client;
600
601 // We moved conditional breakpoint handling to the server, but
602 // need to support it in the client for a while until most of the
603 // server code in production is updated with it. bug 990137 is
604 // filed to mark this code to be removed.
605 if (!client.mainRoot.traits.conditionalBreakpoints) {
606 // Conditional breakpoints are { breakpoint, expression } tuples. The
607 // boolean evaluation of the expression decides if the active thread
608 // automatically resumes execution or not.
609 if (breakLocation) {
610 // Make sure a breakpoint actually exists at the specified url and line.
611 let breakpointPromise = DebuggerController.Breakpoints._getAdded(breakLocation);
612 if (breakpointPromise) {
613 breakpointPromise.then(({ conditionalExpression: e }) => { if (e) {
614 // Evaluating the current breakpoint's conditional expression will
615 // cause the stack frames to be cleared and active thread to pause,
616 // sending a 'clientEvaluated' packed and adding the frames again.
617 this.evaluate(e, { depth: 0, meta: FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL });
618 waitForNextPause = true;
619 }});
620 }
621 }
622 // We'll get our evaluation of the current breakpoint's conditional
623 // expression the next time the thread client pauses...
624 if (waitForNextPause) {
625 return;
626 }
627 if (this._currentFrameDescription == FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL) {
628 this._currentFrameDescription = FRAME_TYPE.NORMAL;
629 // If the breakpoint's conditional expression evaluation is falsy,
630 // automatically resume execution.
631 if (VariablesView.isFalsy({ value: this._currentEvaluation.return })) {
632 this.activeThread.resume(DebuggerController._ensureResumptionOrder);
633 return;
634 }
635 }
636 }
637
638 // Watch expressions are evaluated in the context of the topmost frame,
639 // and the results are displayed in the variables view.
640 // TODO: handle all of this server-side: Bug 832470, comment 14.
641 if (watchExpressions) {
642 // Evaluation causes the stack frames to be cleared and active thread to
643 // pause, sending a 'clientEvaluated' packet and adding the frames again.
644 this.evaluate(watchExpressions, { depth: 0, meta: FRAME_TYPE.WATCH_EXPRESSIONS_EVAL });
645 waitForNextPause = true;
646 }
647 // We'll get our evaluation of the current watch expressions the next time
648 // the thread client pauses...
649 if (waitForNextPause) {
650 return;
651 }
652 if (this._currentFrameDescription == FRAME_TYPE.WATCH_EXPRESSIONS_EVAL) {
653 this._currentFrameDescription = FRAME_TYPE.NORMAL;
654 // If an error was thrown during the evaluation of the watch expressions,
655 // then at least one expression evaluation could not be performed. So
656 // remove the most recent watch expression and try again.
657 if (this._currentEvaluation.throw) {
658 DebuggerView.WatchExpressions.removeAt(0);
659 DebuggerController.StackFrames.syncWatchExpressions();
660 return;
661 }
662 }
663
664 // Make sure the debugger view panes are visible, then refill the frames.
665 DebuggerView.showInstrumentsPane();
666 this._refillFrames();
667
668 // No additional processing is necessary for this stack frame.
669 if (this._currentFrameDescription != FRAME_TYPE.NORMAL) {
670 this._currentFrameDescription = FRAME_TYPE.NORMAL;
671 }
672 },
673
674 /**
675 * Fill the StackFrames view with the frames we have in the cache, compressing
676 * frames which have black boxed sources into single frames.
677 */
678 _refillFrames: function() {
679 // Make sure all the previous stackframes are removed before re-adding them.
680 DebuggerView.StackFrames.empty();
681
682 for (let frame of this.activeThread.cachedFrames) {
683 let { depth, where: { url, line }, source } = frame;
684 let isBlackBoxed = source ? this.activeThread.source(source).isBlackBoxed : false;
685 let location = NetworkHelper.convertToUnicode(unescape(url));
686 let title = StackFrameUtils.getFrameTitle(frame);
687 DebuggerView.StackFrames.addFrame(title, location, line, depth, isBlackBoxed);
688 }
689
690 DebuggerView.StackFrames.selectedDepth = Math.max(this.currentFrameDepth, 0);
691 DebuggerView.StackFrames.dirty = this.activeThread.moreFrames;
692 },
693
694 /**
695 * Handler for the thread client's framescleared notification.
696 */
697 _onFramesCleared: function() {
698 switch (this._currentFrameDescription) {
699 case FRAME_TYPE.NORMAL:
700 this._currentEvaluation = null;
701 this._currentException = null;
702 this._currentReturnedValue = null;
703 break;
704 case FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL:
705 this._currentBreakpointLocation = null;
706 break;
707 case FRAME_TYPE.WATCH_EXPRESSIONS_EVAL:
708 this._currentWatchExpressions = null;
709 break;
710 }
711
712 // After each frame step (in, over, out), framescleared is fired, which
713 // forces the UI to be emptied and rebuilt on framesadded. Most of the times
714 // this is not necessary, and will result in a brief redraw flicker.
715 // To avoid it, invalidate the UI only after a short time if necessary.
716 setNamedTimeout("frames-cleared", FRAME_STEP_CLEAR_DELAY, this._afterFramesCleared);
717 },
718
719 /**
720 * Handler for the debugger's blackboxchange notification.
721 */
722 _onBlackBoxChange: function() {
723 if (this.activeThread.state == "paused") {
724 // Hack to avoid selecting the topmost frame after blackboxing a source.
725 this.currentFrameDepth = NaN;
726 this._refillFrames();
727 }
728 },
729
730 /**
731 * Handler for the debugger's prettyprintchange notification.
732 */
733 _onPrettyPrintChange: function() {
734 // Makes sure the selected source remains selected
735 // after the fillFrames is called.
736 const source = DebuggerView.Sources.selectedValue;
737 if (this.activeThread.state == "paused") {
738 this.activeThread.fillFrames(
739 CALL_STACK_PAGE_SIZE,
740 () => DebuggerView.Sources.selectedValue = source);
741 }
742 },
743
744 /**
745 * Called soon after the thread client's framescleared notification.
746 */
747 _afterFramesCleared: function() {
748 // Ignore useless notifications.
749 if (this.activeThread.cachedFrames.length) {
750 return;
751 }
752 DebuggerView.editor.clearDebugLocation();
753 DebuggerView.StackFrames.empty();
754 DebuggerView.Sources.unhighlightBreakpoint();
755 DebuggerView.WatchExpressions.toggleContents(true);
756 DebuggerView.Variables.empty(0);
757
758 window.emit(EVENTS.AFTER_FRAMES_CLEARED);
759 },
760
761 /**
762 * Marks the stack frame at the specified depth as selected and updates the
763 * properties view with the stack frame's data.
764 *
765 * @param number aDepth
766 * The depth of the frame in the stack.
767 */
768 selectFrame: function(aDepth) {
769 // Make sure the frame at the specified depth exists first.
770 let frame = this.activeThread.cachedFrames[this.currentFrameDepth = aDepth];
771 if (!frame) {
772 return;
773 }
774
775 // Check if the frame does not represent the evaluation of debuggee code.
776 let { environment, where } = frame;
777 if (!environment) {
778 return;
779 }
780
781 // Don't change the editor's location if the execution was paused by a
782 // public client evaluation. This is useful for adding overlays on
783 // top of the editor, like a variable inspection popup.
784 let isClientEval = this._currentFrameDescription == FRAME_TYPE.PUBLIC_CLIENT_EVAL;
785 let isPopupShown = DebuggerView.VariableBubble.contentsShown();
786 if (!isClientEval && !isPopupShown) {
787 // Move the editor's caret to the proper url and line.
788 DebuggerView.setEditorLocation(where.url, where.line);
789 // Highlight the breakpoint at the specified url and line if it exists.
790 DebuggerView.Sources.highlightBreakpoint(where, { noEditorUpdate: true });
791 }
792
793 // Don't display the watch expressions textbox inputs in the pane.
794 DebuggerView.WatchExpressions.toggleContents(false);
795
796 // Start recording any added variables or properties in any scope and
797 // clear existing scopes to create each one dynamically.
798 DebuggerView.Variables.empty();
799
800 // If watch expressions evaluation results are available, create a scope
801 // to contain all the values.
802 if (this._syncedWatchExpressions && aDepth == 0) {
803 let label = L10N.getStr("watchExpressionsScopeLabel");
804 let scope = DebuggerView.Variables.addScope(label);
805
806 // Customize the scope for holding watch expressions evaluations.
807 scope.descriptorTooltip = false;
808 scope.contextMenuId = "debuggerWatchExpressionsContextMenu";
809 scope.separatorStr = L10N.getStr("watchExpressionsSeparatorLabel");
810 scope.switch = DebuggerView.WatchExpressions.switchExpression;
811 scope.delete = DebuggerView.WatchExpressions.deleteExpression;
812
813 // The evaluation hasn't thrown, so fetch and add the returned results.
814 this._fetchWatchExpressions(scope, this._currentEvaluation.return);
815
816 // The watch expressions scope is always automatically expanded.
817 scope.expand();
818 }
819
820 do {
821 // Create a scope to contain all the inspected variables in the
822 // current environment.
823 let label = StackFrameUtils.getScopeLabel(environment);
824 let scope = DebuggerView.Variables.addScope(label);
825 let innermost = environment == frame.environment;
826
827 // Handle special additions to the innermost scope.
828 if (innermost) {
829 this._insertScopeFrameReferences(scope, frame);
830 }
831
832 // Handle the expansion of the scope, lazily populating it with the
833 // variables in the current environment.
834 DebuggerView.Variables.controller.addExpander(scope, environment);
835
836 // The innermost scope is always automatically expanded, because it
837 // contains the variables in the current stack frame which are likely to
838 // be inspected.
839 if (innermost) {
840 scope.expand();
841 }
842 } while ((environment = environment.parent));
843
844 // Signal that scope environments have been shown.
845 window.emit(EVENTS.FETCHED_SCOPES);
846 },
847
848 /**
849 * Loads more stack frames from the debugger server cache.
850 */
851 addMoreFrames: function() {
852 this.activeThread.fillFrames(
853 this.activeThread.cachedFrames.length + CALL_STACK_PAGE_SIZE);
854 },
855
856 /**
857 * Evaluate an expression in the context of the selected frame.
858 *
859 * @param string aExpression
860 * The expression to evaluate.
861 * @param object aOptions [optional]
862 * Additional options for this client evaluation:
863 * - depth: the frame depth used for evaluation, 0 being the topmost.
864 * - meta: some meta-description for what this evaluation represents.
865 * @return object
866 * A promise that is resolved when the evaluation finishes,
867 * or rejected if there was no stack frame available or some
868 * other error occurred.
869 */
870 evaluate: function(aExpression, aOptions = {}) {
871 let depth = "depth" in aOptions ? aOptions.depth : this.currentFrameDepth;
872 let frame = this.activeThread.cachedFrames[depth];
873 if (frame == null) {
874 return promise.reject(new Error("No stack frame available."));
875 }
876
877 let deferred = promise.defer();
878
879 this.activeThread.addOneTimeListener("paused", (aEvent, aPacket) => {
880 let { type, frameFinished } = aPacket.why;
881 if (type == "clientEvaluated") {
882 if (!("terminated" in frameFinished)) {
883 deferred.resolve(frameFinished);
884 } else {
885 deferred.reject(new Error("The execution was abruptly terminated."));
886 }
887 } else {
888 deferred.reject(new Error("Active thread paused unexpectedly."));
889 }
890 });
891
892 let meta = "meta" in aOptions ? aOptions.meta : FRAME_TYPE.PUBLIC_CLIENT_EVAL;
893 this._currentFrameDescription = meta;
894 this.activeThread.eval(frame.actor, aExpression);
895
896 return deferred.promise;
897 },
898
899 /**
900 * Add nodes for special frame references in the innermost scope.
901 *
902 * @param Scope aScope
903 * The scope where the references will be placed into.
904 * @param object aFrame
905 * The frame to get some references from.
906 */
907 _insertScopeFrameReferences: function(aScope, aFrame) {
908 // Add any thrown exception.
909 if (this._currentException) {
910 let excRef = aScope.addItem("<exception>", { value: this._currentException });
911 DebuggerView.Variables.controller.addExpander(excRef, this._currentException);
912 }
913 // Add any returned value.
914 if (this._currentReturnedValue) {
915 let retRef = aScope.addItem("<return>", { value: this._currentReturnedValue });
916 DebuggerView.Variables.controller.addExpander(retRef, this._currentReturnedValue);
917 }
918 // Add "this".
919 if (aFrame.this) {
920 let thisRef = aScope.addItem("this", { value: aFrame.this });
921 DebuggerView.Variables.controller.addExpander(thisRef, aFrame.this);
922 }
923 },
924
925 /**
926 * Adds the watch expressions evaluation results to a scope in the view.
927 *
928 * @param Scope aScope
929 * The scope where the watch expressions will be placed into.
930 * @param object aExp
931 * The grip of the evaluation results.
932 */
933 _fetchWatchExpressions: function(aScope, aExp) {
934 // Fetch the expressions only once.
935 if (aScope._fetched) {
936 return;
937 }
938 aScope._fetched = true;
939
940 // Add nodes for every watch expression in scope.
941 this.activeThread.pauseGrip(aExp).getPrototypeAndProperties(aResponse => {
942 let ownProperties = aResponse.ownProperties;
943 let totalExpressions = DebuggerView.WatchExpressions.itemCount;
944
945 for (let i = 0; i < totalExpressions; i++) {
946 let name = DebuggerView.WatchExpressions.getString(i);
947 let expVal = ownProperties[i].value;
948 let expRef = aScope.addItem(name, ownProperties[i]);
949 DebuggerView.Variables.controller.addExpander(expRef, expVal);
950
951 // Revert some of the custom watch expressions scope presentation flags,
952 // so that they don't propagate to child items.
953 expRef.switch = null;
954 expRef.delete = null;
955 expRef.descriptorTooltip = true;
956 expRef.separatorStr = L10N.getStr("variablesSeparatorLabel");
957 }
958
959 // Signal that watch expressions have been fetched.
960 window.emit(EVENTS.FETCHED_WATCH_EXPRESSIONS);
961 });
962 },
963
964 /**
965 * Updates a list of watch expressions to evaluate on each pause.
966 * TODO: handle all of this server-side: Bug 832470, comment 14.
967 */
968 syncWatchExpressions: function() {
969 let list = DebuggerView.WatchExpressions.getAllStrings();
970
971 // Sanity check all watch expressions before syncing them. To avoid
972 // having the whole watch expressions array throw because of a single
973 // faulty expression, simply convert it to a string describing the error.
974 // There's no other information necessary to be offered in such cases.
975 let sanitizedExpressions = list.map(aString => {
976 // Reflect.parse throws when it encounters a syntax error.
977 try {
978 Parser.reflectionAPI.parse(aString);
979 return aString; // Watch expression can be executed safely.
980 } catch (e) {
981 return "\"" + e.name + ": " + e.message + "\""; // Syntax error.
982 }
983 });
984
985 if (sanitizedExpressions.length) {
986 this._syncedWatchExpressions =
987 this._currentWatchExpressions =
988 "[" +
989 sanitizedExpressions.map(aString =>
990 "eval(\"" +
991 "try {" +
992 // Make sure all quotes are escaped in the expression's syntax,
993 // and add a newline after the statement to avoid comments
994 // breaking the code integrity inside the eval block.
995 aString.replace(/"/g, "\\$&") + "\" + " + "'\\n'" + " + \"" +
996 "} catch (e) {" +
997 "e.name + ': ' + e.message;" + // TODO: Bug 812765, 812764.
998 "}" +
999 "\")"
1000 ).join(",") +
1001 "]";
1002 } else {
1003 this._syncedWatchExpressions =
1004 this._currentWatchExpressions = null;
1005 }
1006
1007 this.currentFrameDepth = -1;
1008 this._onFrames();
1009 }
1010 };
1011
1012 /**
1013 * Keeps the source script list up-to-date, using the thread client's
1014 * source script cache.
1015 */
1016 function SourceScripts() {
1017 this._onNewGlobal = this._onNewGlobal.bind(this);
1018 this._onNewSource = this._onNewSource.bind(this);
1019 this._onSourcesAdded = this._onSourcesAdded.bind(this);
1020 this._onBlackBoxChange = this._onBlackBoxChange.bind(this);
1021 this._onPrettyPrintChange = this._onPrettyPrintChange.bind(this);
1022 }
1023
1024 SourceScripts.prototype = {
1025 get activeThread() DebuggerController.activeThread,
1026 get debuggerClient() DebuggerController.client,
1027 _cache: new Map(),
1028
1029 /**
1030 * Connect to the current thread client.
1031 */
1032 connect: function() {
1033 dumpn("SourceScripts is connecting...");
1034 this.debuggerClient.addListener("newGlobal", this._onNewGlobal);
1035 this.debuggerClient.addListener("newSource", this._onNewSource);
1036 this.activeThread.addListener("blackboxchange", this._onBlackBoxChange);
1037 this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange);
1038 this.handleTabNavigation();
1039 },
1040
1041 /**
1042 * Disconnect from the client.
1043 */
1044 disconnect: function() {
1045 if (!this.activeThread) {
1046 return;
1047 }
1048 dumpn("SourceScripts is disconnecting...");
1049 this.debuggerClient.removeListener("newGlobal", this._onNewGlobal);
1050 this.debuggerClient.removeListener("newSource", this._onNewSource);
1051 this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange);
1052 this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange);
1053 },
1054
1055 /**
1056 * Clears all the cached source contents.
1057 */
1058 clearCache: function() {
1059 this._cache.clear();
1060 },
1061
1062 /**
1063 * Handles any initialization on a tab navigation event issued by the client.
1064 */
1065 handleTabNavigation: function() {
1066 if (!this.activeThread) {
1067 return;
1068 }
1069 dumpn("Handling tab navigation in the SourceScripts");
1070
1071 // Retrieve the list of script sources known to the server from before
1072 // the client was ready to handle "newSource" notifications.
1073 this.activeThread.getSources(this._onSourcesAdded);
1074 },
1075
1076 /**
1077 * Handler for the debugger client's unsolicited newGlobal notification.
1078 */
1079 _onNewGlobal: function(aNotification, aPacket) {
1080 // TODO: bug 806775, update the globals list using aPacket.hostAnnotations
1081 // from bug 801084.
1082 },
1083
1084 /**
1085 * Handler for the debugger client's unsolicited newSource notification.
1086 */
1087 _onNewSource: function(aNotification, aPacket) {
1088 // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets.
1089 if (NEW_SOURCE_IGNORED_URLS.indexOf(aPacket.source.url) != -1) {
1090 return;
1091 }
1092
1093 // Add the source in the debugger view sources container.
1094 DebuggerView.Sources.addSource(aPacket.source, { staged: false });
1095
1096 // Select this source if it's the preferred one.
1097 let preferredValue = DebuggerView.Sources.preferredValue;
1098 if (aPacket.source.url == preferredValue) {
1099 DebuggerView.Sources.selectedValue = preferredValue;
1100 }
1101 // ..or the first entry if there's none selected yet after a while
1102 else {
1103 setNamedTimeout("new-source", NEW_SOURCE_DISPLAY_DELAY, () => {
1104 // If after a certain delay the preferred source still wasn't received,
1105 // just give up on waiting and display the first entry.
1106 if (!DebuggerView.Sources.selectedValue) {
1107 DebuggerView.Sources.selectedIndex = 0;
1108 }
1109 });
1110 }
1111
1112 // If there are any stored breakpoints for this source, display them again,
1113 // both in the editor and the breakpoints pane.
1114 DebuggerController.Breakpoints.updateEditorBreakpoints();
1115 DebuggerController.Breakpoints.updatePaneBreakpoints();
1116
1117 // Make sure the events listeners are up to date.
1118 if (DebuggerView.instrumentsPaneTab == "events-tab") {
1119 DebuggerController.Breakpoints.DOM.scheduleEventListenersFetch();
1120 }
1121
1122 // Signal that a new source has been added.
1123 window.emit(EVENTS.NEW_SOURCE);
1124 },
1125
1126 /**
1127 * Callback for the debugger's active thread getSources() method.
1128 */
1129 _onSourcesAdded: function(aResponse) {
1130 if (aResponse.error) {
1131 let msg = "Error getting sources: " + aResponse.message;
1132 Cu.reportError(msg);
1133 dumpn(msg);
1134 return;
1135 }
1136
1137 if (aResponse.sources.length === 0) {
1138 DebuggerView.Sources.emptyText = L10N.getStr("noSourcesText");
1139 window.emit(EVENTS.SOURCES_ADDED);
1140 return;
1141 }
1142
1143 // Add all the sources in the debugger view sources container.
1144 for (let source of aResponse.sources) {
1145 // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets.
1146 if (NEW_SOURCE_IGNORED_URLS.indexOf(source.url) == -1) {
1147 DebuggerView.Sources.addSource(source, { staged: true });
1148 }
1149 }
1150
1151 // Flushes all the prepared sources into the sources container.
1152 DebuggerView.Sources.commit({ sorted: true });
1153
1154 // Select the preferred source if it exists and was part of the response.
1155 let preferredValue = DebuggerView.Sources.preferredValue;
1156 if (DebuggerView.Sources.containsValue(preferredValue)) {
1157 DebuggerView.Sources.selectedValue = preferredValue;
1158 }
1159 // ..or the first entry if there's no one selected yet.
1160 else if (!DebuggerView.Sources.selectedValue) {
1161 DebuggerView.Sources.selectedIndex = 0;
1162 }
1163
1164 // If there are any stored breakpoints for the sources, display them again,
1165 // both in the editor and the breakpoints pane.
1166 DebuggerController.Breakpoints.updateEditorBreakpoints();
1167 DebuggerController.Breakpoints.updatePaneBreakpoints();
1168
1169 // Signal that sources have been added.
1170 window.emit(EVENTS.SOURCES_ADDED);
1171 },
1172
1173 /**
1174 * Handler for the debugger client's 'blackboxchange' notification.
1175 */
1176 _onBlackBoxChange: function (aEvent, { url, isBlackBoxed }) {
1177 const item = DebuggerView.Sources.getItemByValue(url);
1178 if (item) {
1179 if (isBlackBoxed) {
1180 item.target.classList.add("black-boxed");
1181 } else {
1182 item.target.classList.remove("black-boxed");
1183 }
1184 }
1185 DebuggerView.Sources.updateToolbarButtonsState();
1186 DebuggerView.maybeShowBlackBoxMessage();
1187 },
1188
1189 /**
1190 * Set the black boxed status of the given source.
1191 *
1192 * @param Object aSource
1193 * The source form.
1194 * @param bool aBlackBoxFlag
1195 * True to black box the source, false to un-black box it.
1196 * @returns Promise
1197 * A promize that resolves to [aSource, isBlackBoxed] or rejects to
1198 * [aSource, error].
1199 */
1200 setBlackBoxing: function(aSource, aBlackBoxFlag) {
1201 const sourceClient = this.activeThread.source(aSource);
1202 const deferred = promise.defer();
1203
1204 sourceClient[aBlackBoxFlag ? "blackBox" : "unblackBox"](aPacket => {
1205 const { error, message } = aPacket;
1206 if (error) {
1207 let msg = "Couldn't toggle black boxing for " + aSource.url + ": " + message;
1208 dumpn(msg);
1209 Cu.reportError(msg);
1210 deferred.reject([aSource, msg]);
1211 } else {
1212 deferred.resolve([aSource, sourceClient.isBlackBoxed]);
1213 }
1214 });
1215
1216 return deferred.promise;
1217 },
1218
1219 /**
1220 * Toggle the pretty printing of a source's text. All subsequent calls to
1221 * |getText| will return the pretty-toggled text. Nothing will happen for
1222 * non-javascript files.
1223 *
1224 * @param Object aSource
1225 * The source form from the RDP.
1226 * @returns Promise
1227 * A promise that resolves to [aSource, prettyText] or rejects to
1228 * [aSource, error].
1229 */
1230 togglePrettyPrint: function(aSource) {
1231 // Only attempt to pretty print JavaScript sources.
1232 if (!SourceUtils.isJavaScript(aSource.url, aSource.contentType)) {
1233 return promise.reject([aSource, "Can't prettify non-javascript files."]);
1234 }
1235
1236 const sourceClient = this.activeThread.source(aSource);
1237 const wantPretty = !sourceClient.isPrettyPrinted;
1238
1239 // Only use the existing promise if it is pretty printed.
1240 let textPromise = this._cache.get(aSource.url);
1241 if (textPromise && textPromise.pretty === wantPretty) {
1242 return textPromise;
1243 }
1244
1245 const deferred = promise.defer();
1246 deferred.promise.pretty = wantPretty;
1247 this._cache.set(aSource.url, deferred.promise);
1248
1249 const afterToggle = ({ error, message, source: text, contentType }) => {
1250 if (error) {
1251 // Revert the rejected promise from the cache, so that the original
1252 // source's text may be shown when the source is selected.
1253 this._cache.set(aSource.url, textPromise);
1254 deferred.reject([aSource, message || error]);
1255 return;
1256 }
1257 deferred.resolve([aSource, text, contentType]);
1258 };
1259
1260 if (wantPretty) {
1261 sourceClient.prettyPrint(Prefs.editorTabSize, afterToggle);
1262 } else {
1263 sourceClient.disablePrettyPrint(afterToggle);
1264 }
1265
1266 return deferred.promise;
1267 },
1268
1269 /**
1270 * Handler for the debugger's prettyprintchange notification.
1271 */
1272 _onPrettyPrintChange: function(aEvent, { url }) {
1273 // Remove the cached source AST from the Parser, to avoid getting
1274 // wrong locations when searching for functions.
1275 DebuggerController.Parser.clearSource(url);
1276 },
1277
1278 /**
1279 * Gets a specified source's text.
1280 *
1281 * @param object aSource
1282 * The source object coming from the active thread.
1283 * @param function aOnTimeout [optional]
1284 * Function called when the source text takes a long time to fetch,
1285 * but not necessarily failing. Long fetch times don't cause the
1286 * rejection of the returned promise.
1287 * @param number aDelay [optional]
1288 * The amount of time it takes to consider a source slow to fetch.
1289 * If unspecified, it defaults to a predefined value.
1290 * @return object
1291 * A promise that is resolved after the source text has been fetched.
1292 */
1293 getText: function(aSource, aOnTimeout, aDelay = FETCH_SOURCE_RESPONSE_DELAY) {
1294 // Fetch the source text only once.
1295 let textPromise = this._cache.get(aSource.url);
1296 if (textPromise) {
1297 return textPromise;
1298 }
1299
1300 let deferred = promise.defer();
1301 this._cache.set(aSource.url, deferred.promise);
1302
1303 // If the source text takes a long time to fetch, invoke a callback.
1304 if (aOnTimeout) {
1305 var fetchTimeout = window.setTimeout(() => aOnTimeout(aSource), aDelay);
1306 }
1307
1308 // Get the source text from the active thread.
1309 this.activeThread.source(aSource)
1310 .source(({ error, message, source: text, contentType }) => {
1311 if (aOnTimeout) {
1312 window.clearTimeout(fetchTimeout);
1313 }
1314 if (error) {
1315 deferred.reject([aSource, message || error]);
1316 } else {
1317 deferred.resolve([aSource, text, contentType]);
1318 }
1319 });
1320
1321 return deferred.promise;
1322 },
1323
1324 /**
1325 * Starts fetching all the sources, silently.
1326 *
1327 * @param array aUrls
1328 * The urls for the sources to fetch. If fetching a source's text
1329 * takes too long, it will be discarded.
1330 * @return object
1331 * A promise that is resolved after source texts have been fetched.
1332 */
1333 getTextForSources: function(aUrls) {
1334 let deferred = promise.defer();
1335 let pending = new Set(aUrls);
1336 let fetched = [];
1337
1338 // Can't use promise.all, because if one fetch operation is rejected, then
1339 // everything is considered rejected, thus no other subsequent source will
1340 // be getting fetched. We don't want that. Something like Q's allSettled
1341 // would work like a charm here.
1342
1343 // Try to fetch as many sources as possible.
1344 for (let url of aUrls) {
1345 let sourceItem = DebuggerView.Sources.getItemByValue(url);
1346 let sourceForm = sourceItem.attachment.source;
1347 this.getText(sourceForm, onTimeout).then(onFetch, onError);
1348 }
1349
1350 /* Called if fetching a source takes too long. */
1351 function onTimeout(aSource) {
1352 onError([aSource]);
1353 }
1354
1355 /* Called if fetching a source finishes successfully. */
1356 function onFetch([aSource, aText, aContentType]) {
1357 // If fetching the source has previously timed out, discard it this time.
1358 if (!pending.has(aSource.url)) {
1359 return;
1360 }
1361 pending.delete(aSource.url);
1362 fetched.push([aSource.url, aText, aContentType]);
1363 maybeFinish();
1364 }
1365
1366 /* Called if fetching a source failed because of an error. */
1367 function onError([aSource, aError]) {
1368 pending.delete(aSource.url);
1369 maybeFinish();
1370 }
1371
1372 /* Called every time something interesting happens while fetching sources. */
1373 function maybeFinish() {
1374 if (pending.size == 0) {
1375 // Sort the fetched sources alphabetically by their url.
1376 deferred.resolve(fetched.sort(([aFirst], [aSecond]) => aFirst > aSecond));
1377 }
1378 }
1379
1380 return deferred.promise;
1381 }
1382 };
1383
1384 /**
1385 * Tracer update the UI according to the messages exchanged with the tracer
1386 * actor.
1387 */
1388 function Tracer() {
1389 this._trace = null;
1390 this._idCounter = 0;
1391 this.onTraces = this.onTraces.bind(this);
1392 }
1393
1394 Tracer.prototype = {
1395 get client() {
1396 return DebuggerController.client;
1397 },
1398
1399 get traceClient() {
1400 return DebuggerController.traceClient;
1401 },
1402
1403 get tracing() {
1404 return !!this._trace;
1405 },
1406
1407 /**
1408 * Hooks up the debugger controller with the tracer client.
1409 */
1410 connect: function() {
1411 this._stack = [];
1412 this.client.addListener("traces", this.onTraces);
1413 },
1414
1415 /**
1416 * Disconnects the debugger controller from the tracer client. Any further
1417 * communcation with the tracer actor will not have any effect on the UI.
1418 */
1419 disconnect: function() {
1420 this._stack = null;
1421 this.client.removeListener("traces", this.onTraces);
1422 },
1423
1424 /**
1425 * Instructs the tracer actor to start tracing.
1426 */
1427 startTracing: function(aCallback = () => {}) {
1428 DebuggerView.Tracer.selectTab();
1429 if (this.tracing) {
1430 return;
1431 }
1432 this._trace = "dbg.trace" + Math.random();
1433 this.traceClient.startTrace([
1434 "name",
1435 "location",
1436 "parameterNames",
1437 "depth",
1438 "arguments",
1439 "return",
1440 "throw",
1441 "yield"
1442 ], this._trace, (aResponse) => {
1443 const { error } = aResponse;
1444 if (error) {
1445 DevToolsUtils.reportException("Tracer.prototype.startTracing", error);
1446 this._trace = null;
1447 }
1448
1449 aCallback(aResponse);
1450 });
1451 },
1452
1453 /**
1454 * Instructs the tracer actor to stop tracing.
1455 */
1456 stopTracing: function(aCallback = () => {}) {
1457 if (!this.tracing) {
1458 return;
1459 }
1460 this.traceClient.stopTrace(this._trace, aResponse => {
1461 const { error } = aResponse;
1462 if (error) {
1463 DevToolsUtils.reportException("Tracer.prototype.stopTracing", error);
1464 }
1465
1466 this._trace = null;
1467 aCallback(aResponse);
1468 });
1469 },
1470
1471 onTraces: function (aEvent, { traces }) {
1472 const tracesLength = traces.length;
1473 let tracesToShow;
1474 if (tracesLength > TracerView.MAX_TRACES) {
1475 tracesToShow = traces.slice(tracesLength - TracerView.MAX_TRACES,
1476 tracesLength);
1477 DebuggerView.Tracer.empty();
1478 this._stack.splice(0, this._stack.length);
1479 } else {
1480 tracesToShow = traces;
1481 }
1482
1483 for (let t of tracesToShow) {
1484 if (t.type == "enteredFrame") {
1485 this._onCall(t);
1486 } else {
1487 this._onReturn(t);
1488 }
1489 }
1490
1491 DebuggerView.Tracer.commit();
1492 },
1493
1494 /**
1495 * Callback for handling a new call frame.
1496 */
1497 _onCall: function({ name, location, parameterNames, depth, arguments: args }) {
1498 const item = {
1499 name: name,
1500 location: location,
1501 id: this._idCounter++
1502 };
1503
1504 this._stack.push(item);
1505 DebuggerView.Tracer.addTrace({
1506 type: "call",
1507 name: name,
1508 location: location,
1509 depth: depth,
1510 parameterNames: parameterNames,
1511 arguments: args,
1512 frameId: item.id
1513 });
1514 },
1515
1516 /**
1517 * Callback for handling an exited frame.
1518 */
1519 _onReturn: function(aPacket) {
1520 if (!this._stack.length) {
1521 return;
1522 }
1523
1524 const { name, id, location } = this._stack.pop();
1525 DebuggerView.Tracer.addTrace({
1526 type: aPacket.why,
1527 name: name,
1528 location: location,
1529 depth: aPacket.depth,
1530 frameId: id,
1531 returnVal: aPacket.return || aPacket.throw || aPacket.yield
1532 });
1533 },
1534
1535 /**
1536 * Create an object which has the same interface as a normal object client,
1537 * but since we already have all the information for an object that we will
1538 * ever get (the server doesn't create actors when tracing, just firehoses
1539 * data and forgets about it) just return the data immdiately.
1540 *
1541 * @param Object aObject
1542 * The tracer object "grip" (more like a limited snapshot).
1543 * @returns Object
1544 * The synchronous client object.
1545 */
1546 syncGripClient: function(aObject) {
1547 return {
1548 get isFrozen() { return aObject.frozen; },
1549 get isSealed() { return aObject.sealed; },
1550 get isExtensible() { return aObject.extensible; },
1551
1552 get ownProperties() { return aObject.ownProperties; },
1553 get prototype() { return null; },
1554
1555 getParameterNames: callback => callback(aObject),
1556 getPrototypeAndProperties: callback => callback(aObject),
1557 getPrototype: callback => callback(aObject),
1558
1559 getOwnPropertyNames: (callback) => {
1560 callback({
1561 ownPropertyNames: aObject.ownProperties
1562 ? Object.keys(aObject.ownProperties)
1563 : []
1564 });
1565 },
1566
1567 getProperty: (property, callback) => {
1568 callback({
1569 descriptor: aObject.ownProperties
1570 ? aObject.ownProperties[property]
1571 : null
1572 });
1573 },
1574
1575 getDisplayString: callback => callback("[object " + aObject.class + "]"),
1576
1577 getScope: callback => callback({
1578 error: "scopeNotAvailable",
1579 message: "Cannot get scopes for traced objects"
1580 })
1581 };
1582 },
1583
1584 /**
1585 * Wraps object snapshots received from the tracer server so that we can
1586 * differentiate them from long living object grips from the debugger server
1587 * in the variables view.
1588 *
1589 * @param Object aObject
1590 * The object snapshot from the tracer actor.
1591 */
1592 WrappedObject: function(aObject) {
1593 this.object = aObject;
1594 }
1595 };
1596
1597 /**
1598 * Handles breaking on event listeners in the currently debugged target.
1599 */
1600 function EventListeners() {
1601 this._onEventListeners = this._onEventListeners.bind(this);
1602 }
1603
1604 EventListeners.prototype = {
1605 /**
1606 * A list of event names on which the debuggee will automatically pause
1607 * when invoked.
1608 */
1609 activeEventNames: [],
1610
1611 /**
1612 * Updates the list of events types with listeners that, when invoked,
1613 * will automatically pause the debuggee. The respective events are
1614 * retrieved from the UI.
1615 */
1616 scheduleEventBreakpointsUpdate: function() {
1617 // Make sure we're not sending a batch of closely repeated requests.
1618 // This can easily happen when toggling all events of a certain type.
1619 setNamedTimeout("event-breakpoints-update", 0, () => {
1620 this.activeEventNames = DebuggerView.EventListeners.getCheckedEvents();
1621 gThreadClient.pauseOnDOMEvents(this.activeEventNames);
1622
1623 // Notify that event breakpoints were added/removed on the server.
1624 window.emit(EVENTS.EVENT_BREAKPOINTS_UPDATED);
1625 });
1626 },
1627
1628 /**
1629 * Fetches the currently attached event listeners from the debugee.
1630 */
1631 scheduleEventListenersFetch: function() {
1632 let getListeners = aCallback => gThreadClient.eventListeners(aResponse => {
1633 if (aResponse.error) {
1634 let msg = "Error getting event listeners: " + aResponse.message;
1635 DevToolsUtils.reportException("scheduleEventListenersFetch", msg);
1636 return;
1637 }
1638
1639 let outstandingListenersDefinitionSite = aResponse.listeners.map(aListener => {
1640 const deferred = promise.defer();
1641
1642 gThreadClient.pauseGrip(aListener.function).getDefinitionSite(aResponse => {
1643 if (aResponse.error) {
1644 const msg = "Error getting function definition site: " + aResponse.message;
1645 DevToolsUtils.reportException("scheduleEventListenersFetch", msg);
1646 } else {
1647 aListener.function.url = aResponse.url;
1648 }
1649
1650 deferred.resolve(aListener);
1651 });
1652
1653 return deferred.promise;
1654 });
1655
1656 promise.all(outstandingListenersDefinitionSite).then(aListeners => {
1657 this._onEventListeners(aListeners);
1658
1659 // Notify that event listeners were fetched and shown in the view,
1660 // and callback to resume the active thread if necessary.
1661 window.emit(EVENTS.EVENT_LISTENERS_FETCHED);
1662 aCallback && aCallback();
1663 });
1664 });
1665
1666 // Make sure we're not sending a batch of closely repeated requests.
1667 // This can easily happen whenever new sources are fetched.
1668 setNamedTimeout("event-listeners-fetch", FETCH_EVENT_LISTENERS_DELAY, () => {
1669 if (gThreadClient.state != "paused") {
1670 gThreadClient.interrupt(() => getListeners(() => gThreadClient.resume()));
1671 } else {
1672 getListeners();
1673 }
1674 });
1675 },
1676
1677 /**
1678 * Callback for a debugger's successful active thread eventListeners() call.
1679 */
1680 _onEventListeners: function(aListeners) {
1681 // Add all the listeners in the debugger view event linsteners container.
1682 for (let listener of aListeners) {
1683 DebuggerView.EventListeners.addListener(listener, { staged: true });
1684 }
1685
1686 // Flushes all the prepared events into the event listeners container.
1687 DebuggerView.EventListeners.commit();
1688 }
1689 };
1690
1691 /**
1692 * Handles all the breakpoints in the current debugger.
1693 */
1694 function Breakpoints() {
1695 this._onEditorBreakpointAdd = this._onEditorBreakpointAdd.bind(this);
1696 this._onEditorBreakpointRemove = this._onEditorBreakpointRemove.bind(this);
1697 this.addBreakpoint = this.addBreakpoint.bind(this);
1698 this.removeBreakpoint = this.removeBreakpoint.bind(this);
1699 }
1700
1701 Breakpoints.prototype = {
1702 /**
1703 * A map of breakpoint promises as tracked by the debugger frontend.
1704 * The keys consist of a string representation of the breakpoint location.
1705 */
1706 _added: new Map(),
1707 _removing: new Map(),
1708 _disabled: new Map(),
1709
1710 /**
1711 * Adds the source editor breakpoint handlers.
1712 *
1713 * @return object
1714 * A promise that is resolved when the breakpoints finishes initializing.
1715 */
1716 initialize: function() {
1717 DebuggerView.editor.on("breakpointAdded", this._onEditorBreakpointAdd);
1718 DebuggerView.editor.on("breakpointRemoved", this._onEditorBreakpointRemove);
1719
1720 // Initialization is synchronous, for now.
1721 return promise.resolve(null);
1722 },
1723
1724 /**
1725 * Removes the source editor breakpoint handlers & all the added breakpoints.
1726 *
1727 * @return object
1728 * A promise that is resolved when the breakpoints finishes destroying.
1729 */
1730 destroy: function() {
1731 DebuggerView.editor.off("breakpointAdded", this._onEditorBreakpointAdd);
1732 DebuggerView.editor.off("breakpointRemoved", this._onEditorBreakpointRemove);
1733
1734 return this.removeAllBreakpoints();
1735 },
1736
1737 /**
1738 * Event handler for new breakpoints that come from the editor.
1739 *
1740 * @param number aLine
1741 * Line number where breakpoint was set.
1742 */
1743 _onEditorBreakpointAdd: function(_, aLine) {
1744 let url = DebuggerView.Sources.selectedValue;
1745 let location = { url: url, line: aLine + 1 };
1746
1747 // Initialize the breakpoint, but don't update the editor, since this
1748 // callback is invoked because a breakpoint was added in the editor itself.
1749 this.addBreakpoint(location, { noEditorUpdate: true }).then(aBreakpointClient => {
1750 // If the breakpoint client has an "requestedLocation" attached, then
1751 // the original requested placement for the breakpoint wasn't accepted.
1752 // In this case, we need to update the editor with the new location.
1753 if (aBreakpointClient.requestedLocation) {
1754 DebuggerView.editor.removeBreakpoint(aBreakpointClient.requestedLocation.line - 1);
1755 DebuggerView.editor.addBreakpoint(aBreakpointClient.location.line - 1);
1756 }
1757 // Notify that we've shown a breakpoint in the source editor.
1758 window.emit(EVENTS.BREAKPOINT_SHOWN);
1759 });
1760 },
1761
1762 /**
1763 * Event handler for breakpoints that are removed from the editor.
1764 *
1765 * @param number aLine
1766 * Line number where breakpoint was removed.
1767 */
1768 _onEditorBreakpointRemove: function(_, aLine) {
1769 let url = DebuggerView.Sources.selectedValue;
1770 let location = { url: url, line: aLine + 1 };
1771
1772 // Destroy the breakpoint, but don't update the editor, since this callback
1773 // is invoked because a breakpoint was removed from the editor itself.
1774 this.removeBreakpoint(location, { noEditorUpdate: true }).then(() => {
1775 // Notify that we've hidden a breakpoint in the source editor.
1776 window.emit(EVENTS.BREAKPOINT_HIDDEN);
1777 });
1778 },
1779
1780 /**
1781 * Update the breakpoints in the editor view. This function takes the list of
1782 * breakpoints in the debugger and adds them back into the editor view.
1783 * This is invoked when the selected script is changed, or when new sources
1784 * are received via the _onNewSource and _onSourcesAdded event listeners.
1785 */
1786 updateEditorBreakpoints: function() {
1787 for (let breakpointPromise of this._addedOrDisabled) {
1788 breakpointPromise.then(aBreakpointClient => {
1789 let currentSourceUrl = DebuggerView.Sources.selectedValue;
1790 let breakpointUrl = aBreakpointClient.location.url;
1791
1792 // Update the view only if the breakpoint is in the currently shown source.
1793 if (currentSourceUrl == breakpointUrl) {
1794 this._showBreakpoint(aBreakpointClient, { noPaneUpdate: true });
1795 }
1796 });
1797 }
1798 },
1799
1800 /**
1801 * Update the breakpoints in the pane view. This function takes the list of
1802 * breakpoints in the debugger and adds them back into the breakpoints pane.
1803 * This is invoked when new sources are received via the _onNewSource and
1804 * _onSourcesAdded event listeners.
1805 */
1806 updatePaneBreakpoints: function() {
1807 for (let breakpointPromise of this._addedOrDisabled) {
1808 breakpointPromise.then(aBreakpointClient => {
1809 let container = DebuggerView.Sources;
1810 let breakpointUrl = aBreakpointClient.location.url;
1811
1812 // Update the view only if the breakpoint exists in a known source.
1813 if (container.containsValue(breakpointUrl)) {
1814 this._showBreakpoint(aBreakpointClient, { noEditorUpdate: true });
1815 }
1816 });
1817 }
1818 },
1819
1820 /**
1821 * Add a breakpoint.
1822 *
1823 * @param object aLocation
1824 * The location where you want the breakpoint.
1825 * This object must have two properties:
1826 * - url: the breakpoint's source location.
1827 * - line: the breakpoint's line number.
1828 * It can also have the following optional properties:
1829 * - condition: only pause if this condition evaluates truthy
1830 * @param object aOptions [optional]
1831 * Additional options or flags supported by this operation:
1832 * - openPopup: tells if the expression popup should be shown.
1833 * - noEditorUpdate: tells if you want to skip editor updates.
1834 * - noPaneUpdate: tells if you want to skip breakpoint pane updates.
1835 * @return object
1836 * A promise that is resolved after the breakpoint is added, or
1837 * rejected if there was an error.
1838 */
1839 addBreakpoint: Task.async(function*(aLocation, aOptions = {}) {
1840 // Make sure a proper location is available.
1841 if (!aLocation) {
1842 throw new Error("Invalid breakpoint location.");
1843 }
1844 let addedPromise, removingPromise;
1845
1846 // If the breakpoint was already added, or is currently being added at the
1847 // specified location, then return that promise immediately.
1848 if ((addedPromise = this._getAdded(aLocation))) {
1849 return addedPromise;
1850 }
1851
1852 // If the breakpoint is currently being removed from the specified location,
1853 // then wait for that to finish.
1854 if ((removingPromise = this._getRemoving(aLocation))) {
1855 yield removingPromise;
1856 }
1857
1858 let deferred = promise.defer();
1859
1860 // Remember the breakpoint initialization promise in the store.
1861 let identifier = this.getIdentifier(aLocation);
1862 this._added.set(identifier, deferred.promise);
1863
1864 // Try adding the breakpoint.
1865 gThreadClient.setBreakpoint(aLocation, Task.async(function*(aResponse, aBreakpointClient) {
1866 // If the breakpoint response has an "actualLocation" attached, then
1867 // the original requested placement for the breakpoint wasn't accepted.
1868 if (aResponse.actualLocation) {
1869 // Remember the initialization promise for the new location instead.
1870 let oldIdentifier = identifier;
1871 let newIdentifier = identifier = this.getIdentifier(aResponse.actualLocation);
1872 this._added.delete(oldIdentifier);
1873 this._added.set(newIdentifier, deferred.promise);
1874 }
1875
1876 // By default, new breakpoints are always enabled. Disabled breakpoints
1877 // are, in fact, removed from the server but preserved in the frontend,
1878 // so that they may not be forgotten across target navigations.
1879 let disabledPromise = this._disabled.get(identifier);
1880 if (disabledPromise) {
1881 let aPrevBreakpointClient = yield disabledPromise;
1882 let condition = aPrevBreakpointClient.getCondition();
1883 this._disabled.delete(identifier);
1884
1885 if (condition) {
1886 aBreakpointClient = yield aBreakpointClient.setCondition(
1887 gThreadClient,
1888 condition
1889 );
1890 }
1891 }
1892
1893 if (aResponse.actualLocation) {
1894 // Store the originally requested location in case it's ever needed
1895 // and update the breakpoint client with the actual location.
1896 aBreakpointClient.requestedLocation = aLocation;
1897 aBreakpointClient.location = aResponse.actualLocation;
1898 }
1899
1900 // Preserve information about the breakpoint's line text, to display it
1901 // in the sources pane without requiring fetching the source (for example,
1902 // after the target navigated). Note that this will get out of sync
1903 // if the source text contents change.
1904 let line = aBreakpointClient.location.line - 1;
1905 aBreakpointClient.text = DebuggerView.editor.getText(line).trim();
1906
1907 // Show the breakpoint in the editor and breakpoints pane, and resolve.
1908 this._showBreakpoint(aBreakpointClient, aOptions);
1909
1910 // Notify that we've added a breakpoint.
1911 window.emit(EVENTS.BREAKPOINT_ADDED, aBreakpointClient);
1912 deferred.resolve(aBreakpointClient);
1913 }.bind(this)));
1914
1915 return deferred.promise;
1916 }),
1917
1918 /**
1919 * Remove a breakpoint.
1920 *
1921 * @param object aLocation
1922 * @see DebuggerController.Breakpoints.addBreakpoint
1923 * @param object aOptions [optional]
1924 * @see DebuggerController.Breakpoints.addBreakpoint
1925 * @return object
1926 * A promise that is resolved after the breakpoint is removed, or
1927 * rejected if there was an error.
1928 */
1929 removeBreakpoint: function(aLocation, aOptions = {}) {
1930 // Make sure a proper location is available.
1931 if (!aLocation) {
1932 return promise.reject(new Error("Invalid breakpoint location."));
1933 }
1934
1935 // If the breakpoint was already removed, or has never even been added,
1936 // then return a resolved promise immediately.
1937 let addedPromise = this._getAdded(aLocation);
1938 if (!addedPromise) {
1939 return promise.resolve(aLocation);
1940 }
1941
1942 // If the breakpoint is currently being removed from the specified location,
1943 // then return that promise immediately.
1944 let removingPromise = this._getRemoving(aLocation);
1945 if (removingPromise) {
1946 return removingPromise;
1947 }
1948
1949 let deferred = promise.defer();
1950
1951 // Remember the breakpoint removal promise in the store.
1952 let identifier = this.getIdentifier(aLocation);
1953 this._removing.set(identifier, deferred.promise);
1954
1955 // Retrieve the corresponding breakpoint client first.
1956 addedPromise.then(aBreakpointClient => {
1957 // Try removing the breakpoint.
1958 aBreakpointClient.remove(aResponse => {
1959 // If there was an error removing the breakpoint, reject the promise
1960 // and forget about it that the breakpoint may be re-removed later.
1961 if (aResponse.error) {
1962 deferred.reject(aResponse);
1963 return void this._removing.delete(identifier);
1964 }
1965
1966 // When a breakpoint is removed, the frontend may wish to preserve some
1967 // details about it, so that it can be easily re-added later. In such
1968 // cases, breakpoints are marked and stored as disabled, so that they
1969 // may not be forgotten across target navigations.
1970 if (aOptions.rememberDisabled) {
1971 aBreakpointClient.disabled = true;
1972 this._disabled.set(identifier, promise.resolve(aBreakpointClient));
1973 }
1974
1975 // Forget both the initialization and removal promises from the store.
1976 this._added.delete(identifier);
1977 this._removing.delete(identifier);
1978
1979 // Hide the breakpoint from the editor and breakpoints pane, and resolve.
1980 this._hideBreakpoint(aLocation, aOptions);
1981
1982 // Notify that we've removed a breakpoint.
1983 window.emit(EVENTS.BREAKPOINT_REMOVED, aLocation);
1984 deferred.resolve(aLocation);
1985 });
1986 });
1987
1988 return deferred.promise;
1989 },
1990
1991 /**
1992 * Removes all the currently enabled breakpoints.
1993 *
1994 * @return object
1995 * A promise that is resolved after all breakpoints are removed, or
1996 * rejected if there was an error.
1997 */
1998 removeAllBreakpoints: function() {
1999 /* Gets an array of all the existing breakpoints promises. */
2000 let getActiveBreakpoints = (aPromises, aStore = []) => {
2001 for (let [, breakpointPromise] of aPromises) {
2002 aStore.push(breakpointPromise);
2003 }
2004 return aStore;
2005 }
2006
2007 /* Gets an array of all the removed breakpoints promises. */
2008 let getRemovedBreakpoints = (aClients, aStore = []) => {
2009 for (let breakpointClient of aClients) {
2010 aStore.push(this.removeBreakpoint(breakpointClient.location));
2011 }
2012 return aStore;
2013 }
2014
2015 // First, populate an array of all the currently added breakpoints promises.
2016 // Then, once all the breakpoints clients are retrieved, populate an array
2017 // of all the removed breakpoints promises and wait for their fulfillment.
2018 return promise.all(getActiveBreakpoints(this._added)).then(aBreakpointClients => {
2019 return promise.all(getRemovedBreakpoints(aBreakpointClients));
2020 });
2021 },
2022
2023 /**
2024 * Update the condition of a breakpoint.
2025 *
2026 * @param object aLocation
2027 * @see DebuggerController.Breakpoints.addBreakpoint
2028 * @param string aClients
2029 * The condition to set on the breakpoint
2030 * @return object
2031 * A promise that will be resolved with the breakpoint client
2032 */
2033 updateCondition: function(aLocation, aCondition) {
2034 let addedPromise = this._getAdded(aLocation);
2035 if (!addedPromise) {
2036 return promise.reject(new Error('breakpoint does not exist ' +
2037 'in specified location'));
2038 }
2039
2040 var promise = addedPromise.then(aBreakpointClient => {
2041 return aBreakpointClient.setCondition(gThreadClient, aCondition);
2042 });
2043
2044 // `setCondition` returns a new breakpoint that has the condition,
2045 // so we need to update the store
2046 this._added.set(this.getIdentifier(aLocation), promise);
2047 return promise;
2048 },
2049
2050 /**
2051 * Update the editor and breakpoints pane to show a specified breakpoint.
2052 *
2053 * @param object aBreakpointData
2054 * Information about the breakpoint to be shown.
2055 * This object must have the following properties:
2056 * - location: the breakpoint's source location and line number
2057 * - disabled: the breakpoint's disabled state, boolean
2058 * - text: the breakpoint's line text to be displayed
2059 * @param object aOptions [optional]
2060 * @see DebuggerController.Breakpoints.addBreakpoint
2061 */
2062 _showBreakpoint: function(aBreakpointData, aOptions = {}) {
2063 let currentSourceUrl = DebuggerView.Sources.selectedValue;
2064 let location = aBreakpointData.location;
2065
2066 // Update the editor if required.
2067 if (!aOptions.noEditorUpdate && !aBreakpointData.disabled) {
2068 if (location.url == currentSourceUrl) {
2069 DebuggerView.editor.addBreakpoint(location.line - 1);
2070 }
2071 }
2072
2073 // Update the breakpoints pane if required.
2074 if (!aOptions.noPaneUpdate) {
2075 DebuggerView.Sources.addBreakpoint(aBreakpointData, aOptions);
2076 }
2077 },
2078
2079 /**
2080 * Update the editor and breakpoints pane to hide a specified breakpoint.
2081 *
2082 * @param object aLocation
2083 * @see DebuggerController.Breakpoints.addBreakpoint
2084 * @param object aOptions [optional]
2085 * @see DebuggerController.Breakpoints.addBreakpoint
2086 */
2087 _hideBreakpoint: function(aLocation, aOptions = {}) {
2088 let currentSourceUrl = DebuggerView.Sources.selectedValue;
2089
2090 // Update the editor if required.
2091 if (!aOptions.noEditorUpdate) {
2092 if (aLocation.url == currentSourceUrl) {
2093 DebuggerView.editor.removeBreakpoint(aLocation.line - 1);
2094 }
2095 }
2096
2097 // Update the breakpoints pane if required.
2098 if (!aOptions.noPaneUpdate) {
2099 DebuggerView.Sources.removeBreakpoint(aLocation);
2100 }
2101 },
2102
2103 /**
2104 * Get a Promise for the BreakpointActor client object which is already added
2105 * or currently being added at the given location.
2106 *
2107 * @param object aLocation
2108 * @see DebuggerController.Breakpoints.addBreakpoint
2109 * @return object | null
2110 * A promise that is resolved after the breakpoint is added, or
2111 * null if no breakpoint was found.
2112 */
2113 _getAdded: function(aLocation) {
2114 return this._added.get(this.getIdentifier(aLocation));
2115 },
2116
2117 /**
2118 * Get a Promise for the BreakpointActor client object which is currently
2119 * being removed from the given location.
2120 *
2121 * @param object aLocation
2122 * @see DebuggerController.Breakpoints.addBreakpoint
2123 * @return object | null
2124 * A promise that is resolved after the breakpoint is removed, or
2125 * null if no breakpoint was found.
2126 */
2127 _getRemoving: function(aLocation) {
2128 return this._removing.get(this.getIdentifier(aLocation));
2129 },
2130
2131 /**
2132 * Get an identifier string for a given location. Breakpoint promises are
2133 * identified in the store by a string representation of their location.
2134 *
2135 * @param object aLocation
2136 * The location to serialize to a string.
2137 * @return string
2138 * The identifier string.
2139 */
2140 getIdentifier: function(aLocation) {
2141 return aLocation.url + ":" + aLocation.line;
2142 }
2143 };
2144
2145 /**
2146 * Gets all Promises for the BreakpointActor client objects that are
2147 * either enabled (added to the server) or disabled (removed from the server,
2148 * but for which some details are preserved).
2149 */
2150 Object.defineProperty(Breakpoints.prototype, "_addedOrDisabled", {
2151 get: function* () {
2152 yield* this._added.values();
2153 yield* this._disabled.values();
2154 }
2155 });
2156
2157 /**
2158 * Localization convenience methods.
2159 */
2160 let L10N = new ViewHelpers.L10N(DBG_STRINGS_URI);
2161
2162 /**
2163 * Shortcuts for accessing various debugger preferences.
2164 */
2165 let Prefs = new ViewHelpers.Prefs("devtools", {
2166 sourcesWidth: ["Int", "debugger.ui.panes-sources-width"],
2167 instrumentsWidth: ["Int", "debugger.ui.panes-instruments-width"],
2168 panesVisibleOnStartup: ["Bool", "debugger.ui.panes-visible-on-startup"],
2169 variablesSortingEnabled: ["Bool", "debugger.ui.variables-sorting-enabled"],
2170 variablesOnlyEnumVisible: ["Bool", "debugger.ui.variables-only-enum-visible"],
2171 variablesSearchboxVisible: ["Bool", "debugger.ui.variables-searchbox-visible"],
2172 pauseOnExceptions: ["Bool", "debugger.pause-on-exceptions"],
2173 ignoreCaughtExceptions: ["Bool", "debugger.ignore-caught-exceptions"],
2174 sourceMapsEnabled: ["Bool", "debugger.source-maps-enabled"],
2175 prettyPrintEnabled: ["Bool", "debugger.pretty-print-enabled"],
2176 autoPrettyPrint: ["Bool", "debugger.auto-pretty-print"],
2177 tracerEnabled: ["Bool", "debugger.tracer"],
2178 editorTabSize: ["Int", "editor.tabsize"]
2179 });
2180
2181 /**
2182 * Convenient way of emitting events from the panel window.
2183 */
2184 EventEmitter.decorate(this);
2185
2186 /**
2187 * Preliminary setup for the DebuggerController object.
2188 */
2189 DebuggerController.initialize();
2190 DebuggerController.Parser = new Parser();
2191 DebuggerController.ThreadState = new ThreadState();
2192 DebuggerController.StackFrames = new StackFrames();
2193 DebuggerController.SourceScripts = new SourceScripts();
2194 DebuggerController.Breakpoints = new Breakpoints();
2195 DebuggerController.Breakpoints.DOM = new EventListeners();
2196 DebuggerController.Tracer = new Tracer();
2197
2198 /**
2199 * Export some properties to the global scope for easier access.
2200 */
2201 Object.defineProperties(window, {
2202 "gTarget": {
2203 get: function() DebuggerController._target
2204 },
2205 "gHostType": {
2206 get: function() DebuggerView._hostType
2207 },
2208 "gClient": {
2209 get: function() DebuggerController.client
2210 },
2211 "gThreadClient": {
2212 get: function() DebuggerController.activeThread
2213 },
2214 "gCallStackPageSize": {
2215 get: function() CALL_STACK_PAGE_SIZE
2216 }
2217 });
2218
2219 /**
2220 * Helper method for debugging.
2221 * @param string
2222 */
2223 function dumpn(str) {
2224 if (wantLogging) {
2225 dump("DBG-FRONTEND: " + str + "\n");
2226 }
2227 }
2228
2229 let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");

mercurial