|
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"); |