browser/devtools/debugger/debugger-controller.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial