michael@0: /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: const { classes: Cc, interfaces: Ci, utils: Cu } = Components; michael@0: michael@0: const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties"; michael@0: const NEW_SOURCE_IGNORED_URLS = ["debugger eval code", "self-hosted", "XStringBundle"]; michael@0: const NEW_SOURCE_DISPLAY_DELAY = 200; // ms michael@0: const FETCH_SOURCE_RESPONSE_DELAY = 200; // ms michael@0: const FETCH_EVENT_LISTENERS_DELAY = 200; // ms michael@0: const FRAME_STEP_CLEAR_DELAY = 100; // ms michael@0: const CALL_STACK_PAGE_SIZE = 25; // frames michael@0: michael@0: // The panel's window global is an EventEmitter firing the following events: michael@0: const EVENTS = { michael@0: // When the debugger's source editor instance finishes loading or unloading. michael@0: EDITOR_LOADED: "Debugger:EditorLoaded", michael@0: EDITOR_UNLOADED: "Debugger:EditorUnoaded", michael@0: michael@0: // When new sources are received from the debugger server. michael@0: NEW_SOURCE: "Debugger:NewSource", michael@0: SOURCES_ADDED: "Debugger:SourcesAdded", michael@0: michael@0: // When a source is shown in the source editor. michael@0: SOURCE_SHOWN: "Debugger:EditorSourceShown", michael@0: SOURCE_ERROR_SHOWN: "Debugger:EditorSourceErrorShown", michael@0: michael@0: // When the editor has shown a source and set the line / column position michael@0: EDITOR_LOCATION_SET: "Debugger:EditorLocationSet", michael@0: michael@0: // When scopes, variables, properties and watch expressions are fetched and michael@0: // displayed in the variables view. michael@0: FETCHED_SCOPES: "Debugger:FetchedScopes", michael@0: FETCHED_VARIABLES: "Debugger:FetchedVariables", michael@0: FETCHED_PROPERTIES: "Debugger:FetchedProperties", michael@0: FETCHED_BUBBLE_PROPERTIES: "Debugger:FetchedBubbleProperties", michael@0: FETCHED_WATCH_EXPRESSIONS: "Debugger:FetchedWatchExpressions", michael@0: michael@0: // When a breakpoint has been added or removed on the debugger server. michael@0: BREAKPOINT_ADDED: "Debugger:BreakpointAdded", michael@0: BREAKPOINT_REMOVED: "Debugger:BreakpointRemoved", michael@0: michael@0: // When a breakpoint has been shown or hidden in the source editor. michael@0: BREAKPOINT_SHOWN: "Debugger:BreakpointShown", michael@0: BREAKPOINT_HIDDEN: "Debugger:BreakpointHidden", michael@0: michael@0: // When a conditional breakpoint's popup is showing or hiding. michael@0: CONDITIONAL_BREAKPOINT_POPUP_SHOWING: "Debugger:ConditionalBreakpointPopupShowing", michael@0: CONDITIONAL_BREAKPOINT_POPUP_HIDING: "Debugger:ConditionalBreakpointPopupHiding", michael@0: michael@0: // When event listeners are fetched or event breakpoints are updated. michael@0: EVENT_LISTENERS_FETCHED: "Debugger:EventListenersFetched", michael@0: EVENT_BREAKPOINTS_UPDATED: "Debugger:EventBreakpointsUpdated", michael@0: michael@0: // When a file search was performed. michael@0: FILE_SEARCH_MATCH_FOUND: "Debugger:FileSearch:MatchFound", michael@0: FILE_SEARCH_MATCH_NOT_FOUND: "Debugger:FileSearch:MatchNotFound", michael@0: michael@0: // When a function search was performed. michael@0: FUNCTION_SEARCH_MATCH_FOUND: "Debugger:FunctionSearch:MatchFound", michael@0: FUNCTION_SEARCH_MATCH_NOT_FOUND: "Debugger:FunctionSearch:MatchNotFound", michael@0: michael@0: // When a global text search was performed. michael@0: GLOBAL_SEARCH_MATCH_FOUND: "Debugger:GlobalSearch:MatchFound", michael@0: GLOBAL_SEARCH_MATCH_NOT_FOUND: "Debugger:GlobalSearch:MatchNotFound", michael@0: michael@0: // After the stackframes are cleared and debugger won't pause anymore. michael@0: AFTER_FRAMES_CLEARED: "Debugger:AfterFramesCleared", michael@0: michael@0: // When the options popup is showing or hiding. michael@0: OPTIONS_POPUP_SHOWING: "Debugger:OptionsPopupShowing", michael@0: OPTIONS_POPUP_HIDDEN: "Debugger:OptionsPopupHidden", michael@0: michael@0: // When the widgets layout has been changed. michael@0: LAYOUT_CHANGED: "Debugger:LayoutChanged" michael@0: }; michael@0: michael@0: // Descriptions for what a stack frame represents after the debugger pauses. michael@0: const FRAME_TYPE = { michael@0: NORMAL: 0, michael@0: CONDITIONAL_BREAKPOINT_EVAL: 1, michael@0: WATCH_EXPRESSIONS_EVAL: 2, michael@0: PUBLIC_CLIENT_EVAL: 3 michael@0: }; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/devtools/event-emitter.js"); michael@0: Cu.import("resource://gre/modules/Task.jsm"); michael@0: Cu.import("resource:///modules/devtools/SimpleListWidget.jsm"); michael@0: Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm"); michael@0: Cu.import("resource:///modules/devtools/SideMenuWidget.jsm"); michael@0: Cu.import("resource:///modules/devtools/VariablesView.jsm"); michael@0: Cu.import("resource:///modules/devtools/VariablesViewController.jsm"); michael@0: Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); michael@0: michael@0: const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; michael@0: const promise = require("devtools/toolkit/deprecated-sync-thenables"); michael@0: const Editor = require("devtools/sourceeditor/editor"); michael@0: const DebuggerEditor = require("devtools/sourceeditor/debugger.js"); michael@0: const {Tooltip} = require("devtools/shared/widgets/Tooltip"); michael@0: const FastListWidget = require("devtools/shared/widgets/FastListWidget"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Parser", michael@0: "resource:///modules/devtools/Parser.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "devtools", michael@0: "resource://gre/modules/devtools/Loader.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils", michael@0: "resource://gre/modules/devtools/DevToolsUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils", michael@0: "resource://gre/modules/ShortcutUtils.jsm"); michael@0: michael@0: Object.defineProperty(this, "NetworkHelper", { michael@0: get: function() { michael@0: return devtools.require("devtools/toolkit/webconsole/network-helper"); michael@0: }, michael@0: configurable: true, michael@0: enumerable: true michael@0: }); michael@0: michael@0: /** michael@0: * Object defining the debugger controller components. michael@0: */ michael@0: let DebuggerController = { michael@0: /** michael@0: * Initializes the debugger controller. michael@0: */ michael@0: initialize: function() { michael@0: dumpn("Initializing the DebuggerController"); michael@0: michael@0: this.startupDebugger = this.startupDebugger.bind(this); michael@0: this.shutdownDebugger = this.shutdownDebugger.bind(this); michael@0: this._onTabNavigated = this._onTabNavigated.bind(this); michael@0: this._onTabDetached = this._onTabDetached.bind(this); michael@0: }, michael@0: michael@0: /** michael@0: * Initializes the view. michael@0: * michael@0: * @return object michael@0: * A promise that is resolved when the debugger finishes startup. michael@0: */ michael@0: startupDebugger: function() { michael@0: if (this._startup) { michael@0: return this._startup; michael@0: } michael@0: michael@0: return this._startup = DebuggerView.initialize(); michael@0: }, michael@0: michael@0: /** michael@0: * Destroys the view and disconnects the debugger client from the server. michael@0: * michael@0: * @return object michael@0: * A promise that is resolved when the debugger finishes shutdown. michael@0: */ michael@0: shutdownDebugger: function() { michael@0: if (this._shutdown) { michael@0: return this._shutdown; michael@0: } michael@0: michael@0: return this._shutdown = DebuggerView.destroy().then(() => { michael@0: DebuggerView.destroy(); michael@0: this.SourceScripts.disconnect(); michael@0: this.StackFrames.disconnect(); michael@0: this.ThreadState.disconnect(); michael@0: this.Tracer.disconnect(); michael@0: this.disconnect(); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Initiates remote debugging based on the current target, wiring event michael@0: * handlers as necessary. michael@0: * michael@0: * @return object michael@0: * A promise that is resolved when the debugger finishes connecting. michael@0: */ michael@0: connect: function() { michael@0: if (this._connection) { michael@0: return this._connection; michael@0: } michael@0: michael@0: let startedDebugging = promise.defer(); michael@0: this._connection = startedDebugging.promise; michael@0: michael@0: let target = this._target; michael@0: let { client, form: { chromeDebugger, traceActor, addonActor } } = target; michael@0: target.on("close", this._onTabDetached); michael@0: target.on("navigate", this._onTabNavigated); michael@0: target.on("will-navigate", this._onTabNavigated); michael@0: this.client = client; michael@0: michael@0: if (addonActor) { michael@0: this._startAddonDebugging(addonActor, startedDebugging.resolve); michael@0: } else if (target.chrome) { michael@0: this._startChromeDebugging(chromeDebugger, startedDebugging.resolve); michael@0: } else { michael@0: this._startDebuggingTab(startedDebugging.resolve); michael@0: const startedTracing = promise.defer(); michael@0: if (Prefs.tracerEnabled && traceActor) { michael@0: this._startTracingTab(traceActor, startedTracing.resolve); michael@0: } else { michael@0: startedTracing.resolve(); michael@0: } michael@0: michael@0: return promise.all([startedDebugging.promise, startedTracing.promise]); michael@0: } michael@0: michael@0: return startedDebugging.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Disconnects the debugger client and removes event handlers as necessary. michael@0: */ michael@0: disconnect: function() { michael@0: // Return early if the client didn't even have a chance to instantiate. michael@0: if (!this.client) { michael@0: return; michael@0: } michael@0: michael@0: this._connection = null; michael@0: this.client = null; michael@0: this.activeThread = null; michael@0: }, michael@0: michael@0: /** michael@0: * Called for each location change in the debugged tab. michael@0: * michael@0: * @param string aType michael@0: * Packet type. michael@0: * @param object aPacket michael@0: * Packet received from the server. michael@0: */ michael@0: _onTabNavigated: function(aType, aPacket) { michael@0: switch (aType) { michael@0: case "will-navigate": { michael@0: // Reset UI. michael@0: DebuggerView.handleTabNavigation(); michael@0: michael@0: // Discard all the cached sources *before* the target starts navigating. michael@0: // Sources may be fetched during navigation, in which case we don't michael@0: // want to hang on to the old source contents. michael@0: DebuggerController.SourceScripts.clearCache(); michael@0: DebuggerController.Parser.clearCache(); michael@0: SourceUtils.clearCache(); michael@0: michael@0: // Prevent performing any actions that were scheduled before navigation. michael@0: clearNamedTimeout("new-source"); michael@0: clearNamedTimeout("event-breakpoints-update"); michael@0: clearNamedTimeout("event-listeners-fetch"); michael@0: break; michael@0: } michael@0: case "navigate": { michael@0: this.ThreadState.handleTabNavigation(); michael@0: this.StackFrames.handleTabNavigation(); michael@0: this.SourceScripts.handleTabNavigation(); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Called when the debugged tab is closed. michael@0: */ michael@0: _onTabDetached: function() { michael@0: this.shutdownDebugger(); michael@0: }, michael@0: michael@0: /** michael@0: * Warn if resuming execution produced a wrongOrder error. michael@0: */ michael@0: _ensureResumptionOrder: function(aResponse) { michael@0: if (aResponse.error == "wrongOrder") { michael@0: DebuggerView.Toolbar.showResumeWarning(aResponse.lastPausedUrl); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Sets up a debugging session. michael@0: * michael@0: * @param function aCallback michael@0: * A function to invoke once the client attaches to the active thread. michael@0: */ michael@0: _startDebuggingTab: function(aCallback) { michael@0: this._target.activeTab.attachThread({ michael@0: useSourceMaps: Prefs.sourceMapsEnabled michael@0: }, (aResponse, aThreadClient) => { michael@0: if (!aThreadClient) { michael@0: Cu.reportError("Couldn't attach to thread: " + aResponse.error); michael@0: return; michael@0: } michael@0: this.activeThread = aThreadClient; michael@0: michael@0: this.ThreadState.connect(); michael@0: this.StackFrames.connect(); michael@0: this.SourceScripts.connect(); michael@0: if (aThreadClient.paused) { michael@0: aThreadClient.resume(this._ensureResumptionOrder); michael@0: } michael@0: michael@0: if (aCallback) { michael@0: aCallback(); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Sets up an addon debugging session. michael@0: * michael@0: * @param object aAddonActor michael@0: * The actor for the addon that is being debugged. michael@0: * @param function aCallback michael@0: * A function to invoke once the client attaches to the active thread. michael@0: */ michael@0: _startAddonDebugging: function(aAddonActor, aCallback) { michael@0: this.client.attachAddon(aAddonActor, (aResponse) => { michael@0: return this._startChromeDebugging(aResponse.threadActor, aCallback); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Sets up a chrome debugging session. michael@0: * michael@0: * @param object aChromeDebugger michael@0: * The remote protocol grip of the chrome debugger. michael@0: * @param function aCallback michael@0: * A function to invoke once the client attaches to the active thread. michael@0: */ michael@0: _startChromeDebugging: function(aChromeDebugger, aCallback) { michael@0: this.client.attachThread(aChromeDebugger, (aResponse, aThreadClient) => { michael@0: if (!aThreadClient) { michael@0: Cu.reportError("Couldn't attach to thread: " + aResponse.error); michael@0: return; michael@0: } michael@0: this.activeThread = aThreadClient; michael@0: michael@0: this.ThreadState.connect(); michael@0: this.StackFrames.connect(); michael@0: this.SourceScripts.connect(); michael@0: if (aThreadClient.paused) { michael@0: aThreadClient.resume(this._ensureResumptionOrder); michael@0: } michael@0: michael@0: if (aCallback) { michael@0: aCallback(); michael@0: } michael@0: }, { useSourceMaps: Prefs.sourceMapsEnabled }); michael@0: }, michael@0: michael@0: /** michael@0: * Sets up an execution tracing session. michael@0: * michael@0: * @param object aTraceActor michael@0: * The remote protocol grip of the trace actor. michael@0: * @param function aCallback michael@0: * A function to invoke once the client attaches to the tracer. michael@0: */ michael@0: _startTracingTab: function(aTraceActor, aCallback) { michael@0: this.client.attachTracer(aTraceActor, (response, traceClient) => { michael@0: if (!traceClient) { michael@0: DevToolsUtils.reportException("DebuggerController._startTracingTab", michael@0: new Error("Failed to attach to tracing actor.")); michael@0: return; michael@0: } michael@0: michael@0: this.traceClient = traceClient; michael@0: this.Tracer.connect(); michael@0: michael@0: if (aCallback) { michael@0: aCallback(); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Detach and reattach to the thread actor with useSourceMaps true, blow michael@0: * away old sources and get them again. michael@0: */ michael@0: reconfigureThread: function(aUseSourceMaps) { michael@0: this.activeThread.reconfigure({ useSourceMaps: aUseSourceMaps }, aResponse => { michael@0: if (aResponse.error) { michael@0: let msg = "Couldn't reconfigure thread: " + aResponse.message; michael@0: Cu.reportError(msg); michael@0: dumpn(msg); michael@0: return; michael@0: } michael@0: michael@0: // Reset the view and fetch all the sources again. michael@0: DebuggerView.handleTabNavigation(); michael@0: this.SourceScripts.handleTabNavigation(); michael@0: michael@0: // Update the stack frame list. michael@0: if (this.activeThread.paused) { michael@0: this.activeThread._clearFrames(); michael@0: this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: _startup: null, michael@0: _shutdown: null, michael@0: _connection: null, michael@0: client: null, michael@0: activeThread: null michael@0: }; michael@0: michael@0: /** michael@0: * ThreadState keeps the UI up to date with the state of the michael@0: * thread (paused/attached/etc.). michael@0: */ michael@0: function ThreadState() { michael@0: this._update = this._update.bind(this); michael@0: } michael@0: michael@0: ThreadState.prototype = { michael@0: get activeThread() DebuggerController.activeThread, michael@0: michael@0: /** michael@0: * Connect to the current thread client. michael@0: */ michael@0: connect: function() { michael@0: dumpn("ThreadState is connecting..."); michael@0: this.activeThread.addListener("paused", this._update); michael@0: this.activeThread.addListener("resumed", this._update); michael@0: this.activeThread.pauseOnExceptions(Prefs.pauseOnExceptions, michael@0: Prefs.ignoreCaughtExceptions); michael@0: this.handleTabNavigation(); michael@0: }, michael@0: michael@0: /** michael@0: * Disconnect from the client. michael@0: */ michael@0: disconnect: function() { michael@0: if (!this.activeThread) { michael@0: return; michael@0: } michael@0: dumpn("ThreadState is disconnecting..."); michael@0: this.activeThread.removeListener("paused", this._update); michael@0: this.activeThread.removeListener("resumed", this._update); michael@0: }, michael@0: michael@0: /** michael@0: * Handles any initialization on a tab navigation event issued by the client. michael@0: */ michael@0: handleTabNavigation: function() { michael@0: if (!this.activeThread) { michael@0: return; michael@0: } michael@0: dumpn("Handling tab navigation in the ThreadState"); michael@0: this._update(); michael@0: }, michael@0: michael@0: /** michael@0: * Update the UI after a thread state change. michael@0: */ michael@0: _update: function(aEvent) { michael@0: DebuggerView.Toolbar.toggleResumeButtonState(this.activeThread.state); michael@0: michael@0: if (gTarget && (aEvent == "paused" || aEvent == "resumed")) { michael@0: gTarget.emit("thread-" + aEvent); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Keeps the stack frame list up-to-date, using the thread client's michael@0: * stack frame cache. michael@0: */ michael@0: function StackFrames() { michael@0: this._onPaused = this._onPaused.bind(this); michael@0: this._onResumed = this._onResumed.bind(this); michael@0: this._onFrames = this._onFrames.bind(this); michael@0: this._onFramesCleared = this._onFramesCleared.bind(this); michael@0: this._onBlackBoxChange = this._onBlackBoxChange.bind(this); michael@0: this._onPrettyPrintChange = this._onPrettyPrintChange.bind(this); michael@0: this._afterFramesCleared = this._afterFramesCleared.bind(this); michael@0: this.evaluate = this.evaluate.bind(this); michael@0: } michael@0: michael@0: StackFrames.prototype = { michael@0: get activeThread() DebuggerController.activeThread, michael@0: currentFrameDepth: -1, michael@0: _currentFrameDescription: FRAME_TYPE.NORMAL, michael@0: _syncedWatchExpressions: null, michael@0: _currentWatchExpressions: null, michael@0: _currentBreakpointLocation: null, michael@0: _currentEvaluation: null, michael@0: _currentException: null, michael@0: _currentReturnedValue: null, michael@0: michael@0: /** michael@0: * Connect to the current thread client. michael@0: */ michael@0: connect: function() { michael@0: dumpn("StackFrames is connecting..."); michael@0: this.activeThread.addListener("paused", this._onPaused); michael@0: this.activeThread.addListener("resumed", this._onResumed); michael@0: this.activeThread.addListener("framesadded", this._onFrames); michael@0: this.activeThread.addListener("framescleared", this._onFramesCleared); michael@0: this.activeThread.addListener("blackboxchange", this._onBlackBoxChange); michael@0: this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange); michael@0: this.handleTabNavigation(); michael@0: }, michael@0: michael@0: /** michael@0: * Disconnect from the client. michael@0: */ michael@0: disconnect: function() { michael@0: if (!this.activeThread) { michael@0: return; michael@0: } michael@0: dumpn("StackFrames is disconnecting..."); michael@0: this.activeThread.removeListener("paused", this._onPaused); michael@0: this.activeThread.removeListener("resumed", this._onResumed); michael@0: this.activeThread.removeListener("framesadded", this._onFrames); michael@0: this.activeThread.removeListener("framescleared", this._onFramesCleared); michael@0: this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange); michael@0: this.activeThread.removeListener("prettyprintchange", this._onPrettyPrintChange); michael@0: clearNamedTimeout("frames-cleared"); michael@0: }, michael@0: michael@0: /** michael@0: * Handles any initialization on a tab navigation event issued by the client. michael@0: */ michael@0: handleTabNavigation: function() { michael@0: dumpn("Handling tab navigation in the StackFrames"); michael@0: // Nothing to do here yet. michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the thread client's paused notification. michael@0: * michael@0: * @param string aEvent michael@0: * The name of the notification ("paused" in this case). michael@0: * @param object aPacket michael@0: * The response packet. michael@0: */ michael@0: _onPaused: function(aEvent, aPacket) { michael@0: switch (aPacket.why.type) { michael@0: // If paused by a breakpoint, store the breakpoint location. michael@0: case "breakpoint": michael@0: this._currentBreakpointLocation = aPacket.frame.where; michael@0: break; michael@0: // If paused by a client evaluation, store the evaluated value. michael@0: case "clientEvaluated": michael@0: this._currentEvaluation = aPacket.why.frameFinished; michael@0: break; michael@0: // If paused by an exception, store the exception value. michael@0: case "exception": michael@0: this._currentException = aPacket.why.exception; michael@0: break; michael@0: // If paused while stepping out of a frame, store the returned value or michael@0: // thrown exception. michael@0: case "resumeLimit": michael@0: if (!aPacket.why.frameFinished) { michael@0: break; michael@0: } else if (aPacket.why.frameFinished.throw) { michael@0: this._currentException = aPacket.why.frameFinished.throw; michael@0: } else if (aPacket.why.frameFinished.return) { michael@0: this._currentReturnedValue = aPacket.why.frameFinished.return; michael@0: } michael@0: break; michael@0: } michael@0: michael@0: this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE); michael@0: DebuggerView.editor.focus(); michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the thread client's resumed notification. michael@0: */ michael@0: _onResumed: function() { michael@0: // Prepare the watch expression evaluation string for the next pause. michael@0: if (this._currentFrameDescription != FRAME_TYPE.WATCH_EXPRESSIONS_EVAL) { michael@0: this._currentWatchExpressions = this._syncedWatchExpressions; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the thread client's framesadded notification. michael@0: */ michael@0: _onFrames: function() { michael@0: // Ignore useless notifications. michael@0: if (!this.activeThread || !this.activeThread.cachedFrames.length) { michael@0: return; michael@0: } michael@0: michael@0: let waitForNextPause = false; michael@0: let breakLocation = this._currentBreakpointLocation; michael@0: let watchExpressions = this._currentWatchExpressions; michael@0: let client = DebuggerController.activeThread.client; michael@0: michael@0: // We moved conditional breakpoint handling to the server, but michael@0: // need to support it in the client for a while until most of the michael@0: // server code in production is updated with it. bug 990137 is michael@0: // filed to mark this code to be removed. michael@0: if (!client.mainRoot.traits.conditionalBreakpoints) { michael@0: // Conditional breakpoints are { breakpoint, expression } tuples. The michael@0: // boolean evaluation of the expression decides if the active thread michael@0: // automatically resumes execution or not. michael@0: if (breakLocation) { michael@0: // Make sure a breakpoint actually exists at the specified url and line. michael@0: let breakpointPromise = DebuggerController.Breakpoints._getAdded(breakLocation); michael@0: if (breakpointPromise) { michael@0: breakpointPromise.then(({ conditionalExpression: e }) => { if (e) { michael@0: // Evaluating the current breakpoint's conditional expression will michael@0: // cause the stack frames to be cleared and active thread to pause, michael@0: // sending a 'clientEvaluated' packed and adding the frames again. michael@0: this.evaluate(e, { depth: 0, meta: FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL }); michael@0: waitForNextPause = true; michael@0: }}); michael@0: } michael@0: } michael@0: // We'll get our evaluation of the current breakpoint's conditional michael@0: // expression the next time the thread client pauses... michael@0: if (waitForNextPause) { michael@0: return; michael@0: } michael@0: if (this._currentFrameDescription == FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL) { michael@0: this._currentFrameDescription = FRAME_TYPE.NORMAL; michael@0: // If the breakpoint's conditional expression evaluation is falsy, michael@0: // automatically resume execution. michael@0: if (VariablesView.isFalsy({ value: this._currentEvaluation.return })) { michael@0: this.activeThread.resume(DebuggerController._ensureResumptionOrder); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // Watch expressions are evaluated in the context of the topmost frame, michael@0: // and the results are displayed in the variables view. michael@0: // TODO: handle all of this server-side: Bug 832470, comment 14. michael@0: if (watchExpressions) { michael@0: // Evaluation causes the stack frames to be cleared and active thread to michael@0: // pause, sending a 'clientEvaluated' packet and adding the frames again. michael@0: this.evaluate(watchExpressions, { depth: 0, meta: FRAME_TYPE.WATCH_EXPRESSIONS_EVAL }); michael@0: waitForNextPause = true; michael@0: } michael@0: // We'll get our evaluation of the current watch expressions the next time michael@0: // the thread client pauses... michael@0: if (waitForNextPause) { michael@0: return; michael@0: } michael@0: if (this._currentFrameDescription == FRAME_TYPE.WATCH_EXPRESSIONS_EVAL) { michael@0: this._currentFrameDescription = FRAME_TYPE.NORMAL; michael@0: // If an error was thrown during the evaluation of the watch expressions, michael@0: // then at least one expression evaluation could not be performed. So michael@0: // remove the most recent watch expression and try again. michael@0: if (this._currentEvaluation.throw) { michael@0: DebuggerView.WatchExpressions.removeAt(0); michael@0: DebuggerController.StackFrames.syncWatchExpressions(); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // Make sure the debugger view panes are visible, then refill the frames. michael@0: DebuggerView.showInstrumentsPane(); michael@0: this._refillFrames(); michael@0: michael@0: // No additional processing is necessary for this stack frame. michael@0: if (this._currentFrameDescription != FRAME_TYPE.NORMAL) { michael@0: this._currentFrameDescription = FRAME_TYPE.NORMAL; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Fill the StackFrames view with the frames we have in the cache, compressing michael@0: * frames which have black boxed sources into single frames. michael@0: */ michael@0: _refillFrames: function() { michael@0: // Make sure all the previous stackframes are removed before re-adding them. michael@0: DebuggerView.StackFrames.empty(); michael@0: michael@0: for (let frame of this.activeThread.cachedFrames) { michael@0: let { depth, where: { url, line }, source } = frame; michael@0: let isBlackBoxed = source ? this.activeThread.source(source).isBlackBoxed : false; michael@0: let location = NetworkHelper.convertToUnicode(unescape(url)); michael@0: let title = StackFrameUtils.getFrameTitle(frame); michael@0: DebuggerView.StackFrames.addFrame(title, location, line, depth, isBlackBoxed); michael@0: } michael@0: michael@0: DebuggerView.StackFrames.selectedDepth = Math.max(this.currentFrameDepth, 0); michael@0: DebuggerView.StackFrames.dirty = this.activeThread.moreFrames; michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the thread client's framescleared notification. michael@0: */ michael@0: _onFramesCleared: function() { michael@0: switch (this._currentFrameDescription) { michael@0: case FRAME_TYPE.NORMAL: michael@0: this._currentEvaluation = null; michael@0: this._currentException = null; michael@0: this._currentReturnedValue = null; michael@0: break; michael@0: case FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL: michael@0: this._currentBreakpointLocation = null; michael@0: break; michael@0: case FRAME_TYPE.WATCH_EXPRESSIONS_EVAL: michael@0: this._currentWatchExpressions = null; michael@0: break; michael@0: } michael@0: michael@0: // After each frame step (in, over, out), framescleared is fired, which michael@0: // forces the UI to be emptied and rebuilt on framesadded. Most of the times michael@0: // this is not necessary, and will result in a brief redraw flicker. michael@0: // To avoid it, invalidate the UI only after a short time if necessary. michael@0: setNamedTimeout("frames-cleared", FRAME_STEP_CLEAR_DELAY, this._afterFramesCleared); michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the debugger's blackboxchange notification. michael@0: */ michael@0: _onBlackBoxChange: function() { michael@0: if (this.activeThread.state == "paused") { michael@0: // Hack to avoid selecting the topmost frame after blackboxing a source. michael@0: this.currentFrameDepth = NaN; michael@0: this._refillFrames(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the debugger's prettyprintchange notification. michael@0: */ michael@0: _onPrettyPrintChange: function() { michael@0: // Makes sure the selected source remains selected michael@0: // after the fillFrames is called. michael@0: const source = DebuggerView.Sources.selectedValue; michael@0: if (this.activeThread.state == "paused") { michael@0: this.activeThread.fillFrames( michael@0: CALL_STACK_PAGE_SIZE, michael@0: () => DebuggerView.Sources.selectedValue = source); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Called soon after the thread client's framescleared notification. michael@0: */ michael@0: _afterFramesCleared: function() { michael@0: // Ignore useless notifications. michael@0: if (this.activeThread.cachedFrames.length) { michael@0: return; michael@0: } michael@0: DebuggerView.editor.clearDebugLocation(); michael@0: DebuggerView.StackFrames.empty(); michael@0: DebuggerView.Sources.unhighlightBreakpoint(); michael@0: DebuggerView.WatchExpressions.toggleContents(true); michael@0: DebuggerView.Variables.empty(0); michael@0: michael@0: window.emit(EVENTS.AFTER_FRAMES_CLEARED); michael@0: }, michael@0: michael@0: /** michael@0: * Marks the stack frame at the specified depth as selected and updates the michael@0: * properties view with the stack frame's data. michael@0: * michael@0: * @param number aDepth michael@0: * The depth of the frame in the stack. michael@0: */ michael@0: selectFrame: function(aDepth) { michael@0: // Make sure the frame at the specified depth exists first. michael@0: let frame = this.activeThread.cachedFrames[this.currentFrameDepth = aDepth]; michael@0: if (!frame) { michael@0: return; michael@0: } michael@0: michael@0: // Check if the frame does not represent the evaluation of debuggee code. michael@0: let { environment, where } = frame; michael@0: if (!environment) { michael@0: return; michael@0: } michael@0: michael@0: // Don't change the editor's location if the execution was paused by a michael@0: // public client evaluation. This is useful for adding overlays on michael@0: // top of the editor, like a variable inspection popup. michael@0: let isClientEval = this._currentFrameDescription == FRAME_TYPE.PUBLIC_CLIENT_EVAL; michael@0: let isPopupShown = DebuggerView.VariableBubble.contentsShown(); michael@0: if (!isClientEval && !isPopupShown) { michael@0: // Move the editor's caret to the proper url and line. michael@0: DebuggerView.setEditorLocation(where.url, where.line); michael@0: // Highlight the breakpoint at the specified url and line if it exists. michael@0: DebuggerView.Sources.highlightBreakpoint(where, { noEditorUpdate: true }); michael@0: } michael@0: michael@0: // Don't display the watch expressions textbox inputs in the pane. michael@0: DebuggerView.WatchExpressions.toggleContents(false); michael@0: michael@0: // Start recording any added variables or properties in any scope and michael@0: // clear existing scopes to create each one dynamically. michael@0: DebuggerView.Variables.empty(); michael@0: michael@0: // If watch expressions evaluation results are available, create a scope michael@0: // to contain all the values. michael@0: if (this._syncedWatchExpressions && aDepth == 0) { michael@0: let label = L10N.getStr("watchExpressionsScopeLabel"); michael@0: let scope = DebuggerView.Variables.addScope(label); michael@0: michael@0: // Customize the scope for holding watch expressions evaluations. michael@0: scope.descriptorTooltip = false; michael@0: scope.contextMenuId = "debuggerWatchExpressionsContextMenu"; michael@0: scope.separatorStr = L10N.getStr("watchExpressionsSeparatorLabel"); michael@0: scope.switch = DebuggerView.WatchExpressions.switchExpression; michael@0: scope.delete = DebuggerView.WatchExpressions.deleteExpression; michael@0: michael@0: // The evaluation hasn't thrown, so fetch and add the returned results. michael@0: this._fetchWatchExpressions(scope, this._currentEvaluation.return); michael@0: michael@0: // The watch expressions scope is always automatically expanded. michael@0: scope.expand(); michael@0: } michael@0: michael@0: do { michael@0: // Create a scope to contain all the inspected variables in the michael@0: // current environment. michael@0: let label = StackFrameUtils.getScopeLabel(environment); michael@0: let scope = DebuggerView.Variables.addScope(label); michael@0: let innermost = environment == frame.environment; michael@0: michael@0: // Handle special additions to the innermost scope. michael@0: if (innermost) { michael@0: this._insertScopeFrameReferences(scope, frame); michael@0: } michael@0: michael@0: // Handle the expansion of the scope, lazily populating it with the michael@0: // variables in the current environment. michael@0: DebuggerView.Variables.controller.addExpander(scope, environment); michael@0: michael@0: // The innermost scope is always automatically expanded, because it michael@0: // contains the variables in the current stack frame which are likely to michael@0: // be inspected. michael@0: if (innermost) { michael@0: scope.expand(); michael@0: } michael@0: } while ((environment = environment.parent)); michael@0: michael@0: // Signal that scope environments have been shown. michael@0: window.emit(EVENTS.FETCHED_SCOPES); michael@0: }, michael@0: michael@0: /** michael@0: * Loads more stack frames from the debugger server cache. michael@0: */ michael@0: addMoreFrames: function() { michael@0: this.activeThread.fillFrames( michael@0: this.activeThread.cachedFrames.length + CALL_STACK_PAGE_SIZE); michael@0: }, michael@0: michael@0: /** michael@0: * Evaluate an expression in the context of the selected frame. michael@0: * michael@0: * @param string aExpression michael@0: * The expression to evaluate. michael@0: * @param object aOptions [optional] michael@0: * Additional options for this client evaluation: michael@0: * - depth: the frame depth used for evaluation, 0 being the topmost. michael@0: * - meta: some meta-description for what this evaluation represents. michael@0: * @return object michael@0: * A promise that is resolved when the evaluation finishes, michael@0: * or rejected if there was no stack frame available or some michael@0: * other error occurred. michael@0: */ michael@0: evaluate: function(aExpression, aOptions = {}) { michael@0: let depth = "depth" in aOptions ? aOptions.depth : this.currentFrameDepth; michael@0: let frame = this.activeThread.cachedFrames[depth]; michael@0: if (frame == null) { michael@0: return promise.reject(new Error("No stack frame available.")); michael@0: } michael@0: michael@0: let deferred = promise.defer(); michael@0: michael@0: this.activeThread.addOneTimeListener("paused", (aEvent, aPacket) => { michael@0: let { type, frameFinished } = aPacket.why; michael@0: if (type == "clientEvaluated") { michael@0: if (!("terminated" in frameFinished)) { michael@0: deferred.resolve(frameFinished); michael@0: } else { michael@0: deferred.reject(new Error("The execution was abruptly terminated.")); michael@0: } michael@0: } else { michael@0: deferred.reject(new Error("Active thread paused unexpectedly.")); michael@0: } michael@0: }); michael@0: michael@0: let meta = "meta" in aOptions ? aOptions.meta : FRAME_TYPE.PUBLIC_CLIENT_EVAL; michael@0: this._currentFrameDescription = meta; michael@0: this.activeThread.eval(frame.actor, aExpression); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Add nodes for special frame references in the innermost scope. michael@0: * michael@0: * @param Scope aScope michael@0: * The scope where the references will be placed into. michael@0: * @param object aFrame michael@0: * The frame to get some references from. michael@0: */ michael@0: _insertScopeFrameReferences: function(aScope, aFrame) { michael@0: // Add any thrown exception. michael@0: if (this._currentException) { michael@0: let excRef = aScope.addItem("", { value: this._currentException }); michael@0: DebuggerView.Variables.controller.addExpander(excRef, this._currentException); michael@0: } michael@0: // Add any returned value. michael@0: if (this._currentReturnedValue) { michael@0: let retRef = aScope.addItem("", { value: this._currentReturnedValue }); michael@0: DebuggerView.Variables.controller.addExpander(retRef, this._currentReturnedValue); michael@0: } michael@0: // Add "this". michael@0: if (aFrame.this) { michael@0: let thisRef = aScope.addItem("this", { value: aFrame.this }); michael@0: DebuggerView.Variables.controller.addExpander(thisRef, aFrame.this); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Adds the watch expressions evaluation results to a scope in the view. michael@0: * michael@0: * @param Scope aScope michael@0: * The scope where the watch expressions will be placed into. michael@0: * @param object aExp michael@0: * The grip of the evaluation results. michael@0: */ michael@0: _fetchWatchExpressions: function(aScope, aExp) { michael@0: // Fetch the expressions only once. michael@0: if (aScope._fetched) { michael@0: return; michael@0: } michael@0: aScope._fetched = true; michael@0: michael@0: // Add nodes for every watch expression in scope. michael@0: this.activeThread.pauseGrip(aExp).getPrototypeAndProperties(aResponse => { michael@0: let ownProperties = aResponse.ownProperties; michael@0: let totalExpressions = DebuggerView.WatchExpressions.itemCount; michael@0: michael@0: for (let i = 0; i < totalExpressions; i++) { michael@0: let name = DebuggerView.WatchExpressions.getString(i); michael@0: let expVal = ownProperties[i].value; michael@0: let expRef = aScope.addItem(name, ownProperties[i]); michael@0: DebuggerView.Variables.controller.addExpander(expRef, expVal); michael@0: michael@0: // Revert some of the custom watch expressions scope presentation flags, michael@0: // so that they don't propagate to child items. michael@0: expRef.switch = null; michael@0: expRef.delete = null; michael@0: expRef.descriptorTooltip = true; michael@0: expRef.separatorStr = L10N.getStr("variablesSeparatorLabel"); michael@0: } michael@0: michael@0: // Signal that watch expressions have been fetched. michael@0: window.emit(EVENTS.FETCHED_WATCH_EXPRESSIONS); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Updates a list of watch expressions to evaluate on each pause. michael@0: * TODO: handle all of this server-side: Bug 832470, comment 14. michael@0: */ michael@0: syncWatchExpressions: function() { michael@0: let list = DebuggerView.WatchExpressions.getAllStrings(); michael@0: michael@0: // Sanity check all watch expressions before syncing them. To avoid michael@0: // having the whole watch expressions array throw because of a single michael@0: // faulty expression, simply convert it to a string describing the error. michael@0: // There's no other information necessary to be offered in such cases. michael@0: let sanitizedExpressions = list.map(aString => { michael@0: // Reflect.parse throws when it encounters a syntax error. michael@0: try { michael@0: Parser.reflectionAPI.parse(aString); michael@0: return aString; // Watch expression can be executed safely. michael@0: } catch (e) { michael@0: return "\"" + e.name + ": " + e.message + "\""; // Syntax error. michael@0: } michael@0: }); michael@0: michael@0: if (sanitizedExpressions.length) { michael@0: this._syncedWatchExpressions = michael@0: this._currentWatchExpressions = michael@0: "[" + michael@0: sanitizedExpressions.map(aString => michael@0: "eval(\"" + michael@0: "try {" + michael@0: // Make sure all quotes are escaped in the expression's syntax, michael@0: // and add a newline after the statement to avoid comments michael@0: // breaking the code integrity inside the eval block. michael@0: aString.replace(/"/g, "\\$&") + "\" + " + "'\\n'" + " + \"" + michael@0: "} catch (e) {" + michael@0: "e.name + ': ' + e.message;" + // TODO: Bug 812765, 812764. michael@0: "}" + michael@0: "\")" michael@0: ).join(",") + michael@0: "]"; michael@0: } else { michael@0: this._syncedWatchExpressions = michael@0: this._currentWatchExpressions = null; michael@0: } michael@0: michael@0: this.currentFrameDepth = -1; michael@0: this._onFrames(); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Keeps the source script list up-to-date, using the thread client's michael@0: * source script cache. michael@0: */ michael@0: function SourceScripts() { michael@0: this._onNewGlobal = this._onNewGlobal.bind(this); michael@0: this._onNewSource = this._onNewSource.bind(this); michael@0: this._onSourcesAdded = this._onSourcesAdded.bind(this); michael@0: this._onBlackBoxChange = this._onBlackBoxChange.bind(this); michael@0: this._onPrettyPrintChange = this._onPrettyPrintChange.bind(this); michael@0: } michael@0: michael@0: SourceScripts.prototype = { michael@0: get activeThread() DebuggerController.activeThread, michael@0: get debuggerClient() DebuggerController.client, michael@0: _cache: new Map(), michael@0: michael@0: /** michael@0: * Connect to the current thread client. michael@0: */ michael@0: connect: function() { michael@0: dumpn("SourceScripts is connecting..."); michael@0: this.debuggerClient.addListener("newGlobal", this._onNewGlobal); michael@0: this.debuggerClient.addListener("newSource", this._onNewSource); michael@0: this.activeThread.addListener("blackboxchange", this._onBlackBoxChange); michael@0: this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange); michael@0: this.handleTabNavigation(); michael@0: }, michael@0: michael@0: /** michael@0: * Disconnect from the client. michael@0: */ michael@0: disconnect: function() { michael@0: if (!this.activeThread) { michael@0: return; michael@0: } michael@0: dumpn("SourceScripts is disconnecting..."); michael@0: this.debuggerClient.removeListener("newGlobal", this._onNewGlobal); michael@0: this.debuggerClient.removeListener("newSource", this._onNewSource); michael@0: this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange); michael@0: this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange); michael@0: }, michael@0: michael@0: /** michael@0: * Clears all the cached source contents. michael@0: */ michael@0: clearCache: function() { michael@0: this._cache.clear(); michael@0: }, michael@0: michael@0: /** michael@0: * Handles any initialization on a tab navigation event issued by the client. michael@0: */ michael@0: handleTabNavigation: function() { michael@0: if (!this.activeThread) { michael@0: return; michael@0: } michael@0: dumpn("Handling tab navigation in the SourceScripts"); michael@0: michael@0: // Retrieve the list of script sources known to the server from before michael@0: // the client was ready to handle "newSource" notifications. michael@0: this.activeThread.getSources(this._onSourcesAdded); michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the debugger client's unsolicited newGlobal notification. michael@0: */ michael@0: _onNewGlobal: function(aNotification, aPacket) { michael@0: // TODO: bug 806775, update the globals list using aPacket.hostAnnotations michael@0: // from bug 801084. michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the debugger client's unsolicited newSource notification. michael@0: */ michael@0: _onNewSource: function(aNotification, aPacket) { michael@0: // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets. michael@0: if (NEW_SOURCE_IGNORED_URLS.indexOf(aPacket.source.url) != -1) { michael@0: return; michael@0: } michael@0: michael@0: // Add the source in the debugger view sources container. michael@0: DebuggerView.Sources.addSource(aPacket.source, { staged: false }); michael@0: michael@0: // Select this source if it's the preferred one. michael@0: let preferredValue = DebuggerView.Sources.preferredValue; michael@0: if (aPacket.source.url == preferredValue) { michael@0: DebuggerView.Sources.selectedValue = preferredValue; michael@0: } michael@0: // ..or the first entry if there's none selected yet after a while michael@0: else { michael@0: setNamedTimeout("new-source", NEW_SOURCE_DISPLAY_DELAY, () => { michael@0: // If after a certain delay the preferred source still wasn't received, michael@0: // just give up on waiting and display the first entry. michael@0: if (!DebuggerView.Sources.selectedValue) { michael@0: DebuggerView.Sources.selectedIndex = 0; michael@0: } michael@0: }); michael@0: } michael@0: michael@0: // If there are any stored breakpoints for this source, display them again, michael@0: // both in the editor and the breakpoints pane. michael@0: DebuggerController.Breakpoints.updateEditorBreakpoints(); michael@0: DebuggerController.Breakpoints.updatePaneBreakpoints(); michael@0: michael@0: // Make sure the events listeners are up to date. michael@0: if (DebuggerView.instrumentsPaneTab == "events-tab") { michael@0: DebuggerController.Breakpoints.DOM.scheduleEventListenersFetch(); michael@0: } michael@0: michael@0: // Signal that a new source has been added. michael@0: window.emit(EVENTS.NEW_SOURCE); michael@0: }, michael@0: michael@0: /** michael@0: * Callback for the debugger's active thread getSources() method. michael@0: */ michael@0: _onSourcesAdded: function(aResponse) { michael@0: if (aResponse.error) { michael@0: let msg = "Error getting sources: " + aResponse.message; michael@0: Cu.reportError(msg); michael@0: dumpn(msg); michael@0: return; michael@0: } michael@0: michael@0: if (aResponse.sources.length === 0) { michael@0: DebuggerView.Sources.emptyText = L10N.getStr("noSourcesText"); michael@0: window.emit(EVENTS.SOURCES_ADDED); michael@0: return; michael@0: } michael@0: michael@0: // Add all the sources in the debugger view sources container. michael@0: for (let source of aResponse.sources) { michael@0: // Ignore bogus scripts, e.g. generated from 'clientEvaluate' packets. michael@0: if (NEW_SOURCE_IGNORED_URLS.indexOf(source.url) == -1) { michael@0: DebuggerView.Sources.addSource(source, { staged: true }); michael@0: } michael@0: } michael@0: michael@0: // Flushes all the prepared sources into the sources container. michael@0: DebuggerView.Sources.commit({ sorted: true }); michael@0: michael@0: // Select the preferred source if it exists and was part of the response. michael@0: let preferredValue = DebuggerView.Sources.preferredValue; michael@0: if (DebuggerView.Sources.containsValue(preferredValue)) { michael@0: DebuggerView.Sources.selectedValue = preferredValue; michael@0: } michael@0: // ..or the first entry if there's no one selected yet. michael@0: else if (!DebuggerView.Sources.selectedValue) { michael@0: DebuggerView.Sources.selectedIndex = 0; michael@0: } michael@0: michael@0: // If there are any stored breakpoints for the sources, display them again, michael@0: // both in the editor and the breakpoints pane. michael@0: DebuggerController.Breakpoints.updateEditorBreakpoints(); michael@0: DebuggerController.Breakpoints.updatePaneBreakpoints(); michael@0: michael@0: // Signal that sources have been added. michael@0: window.emit(EVENTS.SOURCES_ADDED); michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the debugger client's 'blackboxchange' notification. michael@0: */ michael@0: _onBlackBoxChange: function (aEvent, { url, isBlackBoxed }) { michael@0: const item = DebuggerView.Sources.getItemByValue(url); michael@0: if (item) { michael@0: if (isBlackBoxed) { michael@0: item.target.classList.add("black-boxed"); michael@0: } else { michael@0: item.target.classList.remove("black-boxed"); michael@0: } michael@0: } michael@0: DebuggerView.Sources.updateToolbarButtonsState(); michael@0: DebuggerView.maybeShowBlackBoxMessage(); michael@0: }, michael@0: michael@0: /** michael@0: * Set the black boxed status of the given source. michael@0: * michael@0: * @param Object aSource michael@0: * The source form. michael@0: * @param bool aBlackBoxFlag michael@0: * True to black box the source, false to un-black box it. michael@0: * @returns Promise michael@0: * A promize that resolves to [aSource, isBlackBoxed] or rejects to michael@0: * [aSource, error]. michael@0: */ michael@0: setBlackBoxing: function(aSource, aBlackBoxFlag) { michael@0: const sourceClient = this.activeThread.source(aSource); michael@0: const deferred = promise.defer(); michael@0: michael@0: sourceClient[aBlackBoxFlag ? "blackBox" : "unblackBox"](aPacket => { michael@0: const { error, message } = aPacket; michael@0: if (error) { michael@0: let msg = "Couldn't toggle black boxing for " + aSource.url + ": " + message; michael@0: dumpn(msg); michael@0: Cu.reportError(msg); michael@0: deferred.reject([aSource, msg]); michael@0: } else { michael@0: deferred.resolve([aSource, sourceClient.isBlackBoxed]); michael@0: } michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Toggle the pretty printing of a source's text. All subsequent calls to michael@0: * |getText| will return the pretty-toggled text. Nothing will happen for michael@0: * non-javascript files. michael@0: * michael@0: * @param Object aSource michael@0: * The source form from the RDP. michael@0: * @returns Promise michael@0: * A promise that resolves to [aSource, prettyText] or rejects to michael@0: * [aSource, error]. michael@0: */ michael@0: togglePrettyPrint: function(aSource) { michael@0: // Only attempt to pretty print JavaScript sources. michael@0: if (!SourceUtils.isJavaScript(aSource.url, aSource.contentType)) { michael@0: return promise.reject([aSource, "Can't prettify non-javascript files."]); michael@0: } michael@0: michael@0: const sourceClient = this.activeThread.source(aSource); michael@0: const wantPretty = !sourceClient.isPrettyPrinted; michael@0: michael@0: // Only use the existing promise if it is pretty printed. michael@0: let textPromise = this._cache.get(aSource.url); michael@0: if (textPromise && textPromise.pretty === wantPretty) { michael@0: return textPromise; michael@0: } michael@0: michael@0: const deferred = promise.defer(); michael@0: deferred.promise.pretty = wantPretty; michael@0: this._cache.set(aSource.url, deferred.promise); michael@0: michael@0: const afterToggle = ({ error, message, source: text, contentType }) => { michael@0: if (error) { michael@0: // Revert the rejected promise from the cache, so that the original michael@0: // source's text may be shown when the source is selected. michael@0: this._cache.set(aSource.url, textPromise); michael@0: deferred.reject([aSource, message || error]); michael@0: return; michael@0: } michael@0: deferred.resolve([aSource, text, contentType]); michael@0: }; michael@0: michael@0: if (wantPretty) { michael@0: sourceClient.prettyPrint(Prefs.editorTabSize, afterToggle); michael@0: } else { michael@0: sourceClient.disablePrettyPrint(afterToggle); michael@0: } michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Handler for the debugger's prettyprintchange notification. michael@0: */ michael@0: _onPrettyPrintChange: function(aEvent, { url }) { michael@0: // Remove the cached source AST from the Parser, to avoid getting michael@0: // wrong locations when searching for functions. michael@0: DebuggerController.Parser.clearSource(url); michael@0: }, michael@0: michael@0: /** michael@0: * Gets a specified source's text. michael@0: * michael@0: * @param object aSource michael@0: * The source object coming from the active thread. michael@0: * @param function aOnTimeout [optional] michael@0: * Function called when the source text takes a long time to fetch, michael@0: * but not necessarily failing. Long fetch times don't cause the michael@0: * rejection of the returned promise. michael@0: * @param number aDelay [optional] michael@0: * The amount of time it takes to consider a source slow to fetch. michael@0: * If unspecified, it defaults to a predefined value. michael@0: * @return object michael@0: * A promise that is resolved after the source text has been fetched. michael@0: */ michael@0: getText: function(aSource, aOnTimeout, aDelay = FETCH_SOURCE_RESPONSE_DELAY) { michael@0: // Fetch the source text only once. michael@0: let textPromise = this._cache.get(aSource.url); michael@0: if (textPromise) { michael@0: return textPromise; michael@0: } michael@0: michael@0: let deferred = promise.defer(); michael@0: this._cache.set(aSource.url, deferred.promise); michael@0: michael@0: // If the source text takes a long time to fetch, invoke a callback. michael@0: if (aOnTimeout) { michael@0: var fetchTimeout = window.setTimeout(() => aOnTimeout(aSource), aDelay); michael@0: } michael@0: michael@0: // Get the source text from the active thread. michael@0: this.activeThread.source(aSource) michael@0: .source(({ error, message, source: text, contentType }) => { michael@0: if (aOnTimeout) { michael@0: window.clearTimeout(fetchTimeout); michael@0: } michael@0: if (error) { michael@0: deferred.reject([aSource, message || error]); michael@0: } else { michael@0: deferred.resolve([aSource, text, contentType]); michael@0: } michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Starts fetching all the sources, silently. michael@0: * michael@0: * @param array aUrls michael@0: * The urls for the sources to fetch. If fetching a source's text michael@0: * takes too long, it will be discarded. michael@0: * @return object michael@0: * A promise that is resolved after source texts have been fetched. michael@0: */ michael@0: getTextForSources: function(aUrls) { michael@0: let deferred = promise.defer(); michael@0: let pending = new Set(aUrls); michael@0: let fetched = []; michael@0: michael@0: // Can't use promise.all, because if one fetch operation is rejected, then michael@0: // everything is considered rejected, thus no other subsequent source will michael@0: // be getting fetched. We don't want that. Something like Q's allSettled michael@0: // would work like a charm here. michael@0: michael@0: // Try to fetch as many sources as possible. michael@0: for (let url of aUrls) { michael@0: let sourceItem = DebuggerView.Sources.getItemByValue(url); michael@0: let sourceForm = sourceItem.attachment.source; michael@0: this.getText(sourceForm, onTimeout).then(onFetch, onError); michael@0: } michael@0: michael@0: /* Called if fetching a source takes too long. */ michael@0: function onTimeout(aSource) { michael@0: onError([aSource]); michael@0: } michael@0: michael@0: /* Called if fetching a source finishes successfully. */ michael@0: function onFetch([aSource, aText, aContentType]) { michael@0: // If fetching the source has previously timed out, discard it this time. michael@0: if (!pending.has(aSource.url)) { michael@0: return; michael@0: } michael@0: pending.delete(aSource.url); michael@0: fetched.push([aSource.url, aText, aContentType]); michael@0: maybeFinish(); michael@0: } michael@0: michael@0: /* Called if fetching a source failed because of an error. */ michael@0: function onError([aSource, aError]) { michael@0: pending.delete(aSource.url); michael@0: maybeFinish(); michael@0: } michael@0: michael@0: /* Called every time something interesting happens while fetching sources. */ michael@0: function maybeFinish() { michael@0: if (pending.size == 0) { michael@0: // Sort the fetched sources alphabetically by their url. michael@0: deferred.resolve(fetched.sort(([aFirst], [aSecond]) => aFirst > aSecond)); michael@0: } michael@0: } michael@0: michael@0: return deferred.promise; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Tracer update the UI according to the messages exchanged with the tracer michael@0: * actor. michael@0: */ michael@0: function Tracer() { michael@0: this._trace = null; michael@0: this._idCounter = 0; michael@0: this.onTraces = this.onTraces.bind(this); michael@0: } michael@0: michael@0: Tracer.prototype = { michael@0: get client() { michael@0: return DebuggerController.client; michael@0: }, michael@0: michael@0: get traceClient() { michael@0: return DebuggerController.traceClient; michael@0: }, michael@0: michael@0: get tracing() { michael@0: return !!this._trace; michael@0: }, michael@0: michael@0: /** michael@0: * Hooks up the debugger controller with the tracer client. michael@0: */ michael@0: connect: function() { michael@0: this._stack = []; michael@0: this.client.addListener("traces", this.onTraces); michael@0: }, michael@0: michael@0: /** michael@0: * Disconnects the debugger controller from the tracer client. Any further michael@0: * communcation with the tracer actor will not have any effect on the UI. michael@0: */ michael@0: disconnect: function() { michael@0: this._stack = null; michael@0: this.client.removeListener("traces", this.onTraces); michael@0: }, michael@0: michael@0: /** michael@0: * Instructs the tracer actor to start tracing. michael@0: */ michael@0: startTracing: function(aCallback = () => {}) { michael@0: DebuggerView.Tracer.selectTab(); michael@0: if (this.tracing) { michael@0: return; michael@0: } michael@0: this._trace = "dbg.trace" + Math.random(); michael@0: this.traceClient.startTrace([ michael@0: "name", michael@0: "location", michael@0: "parameterNames", michael@0: "depth", michael@0: "arguments", michael@0: "return", michael@0: "throw", michael@0: "yield" michael@0: ], this._trace, (aResponse) => { michael@0: const { error } = aResponse; michael@0: if (error) { michael@0: DevToolsUtils.reportException("Tracer.prototype.startTracing", error); michael@0: this._trace = null; michael@0: } michael@0: michael@0: aCallback(aResponse); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Instructs the tracer actor to stop tracing. michael@0: */ michael@0: stopTracing: function(aCallback = () => {}) { michael@0: if (!this.tracing) { michael@0: return; michael@0: } michael@0: this.traceClient.stopTrace(this._trace, aResponse => { michael@0: const { error } = aResponse; michael@0: if (error) { michael@0: DevToolsUtils.reportException("Tracer.prototype.stopTracing", error); michael@0: } michael@0: michael@0: this._trace = null; michael@0: aCallback(aResponse); michael@0: }); michael@0: }, michael@0: michael@0: onTraces: function (aEvent, { traces }) { michael@0: const tracesLength = traces.length; michael@0: let tracesToShow; michael@0: if (tracesLength > TracerView.MAX_TRACES) { michael@0: tracesToShow = traces.slice(tracesLength - TracerView.MAX_TRACES, michael@0: tracesLength); michael@0: DebuggerView.Tracer.empty(); michael@0: this._stack.splice(0, this._stack.length); michael@0: } else { michael@0: tracesToShow = traces; michael@0: } michael@0: michael@0: for (let t of tracesToShow) { michael@0: if (t.type == "enteredFrame") { michael@0: this._onCall(t); michael@0: } else { michael@0: this._onReturn(t); michael@0: } michael@0: } michael@0: michael@0: DebuggerView.Tracer.commit(); michael@0: }, michael@0: michael@0: /** michael@0: * Callback for handling a new call frame. michael@0: */ michael@0: _onCall: function({ name, location, parameterNames, depth, arguments: args }) { michael@0: const item = { michael@0: name: name, michael@0: location: location, michael@0: id: this._idCounter++ michael@0: }; michael@0: michael@0: this._stack.push(item); michael@0: DebuggerView.Tracer.addTrace({ michael@0: type: "call", michael@0: name: name, michael@0: location: location, michael@0: depth: depth, michael@0: parameterNames: parameterNames, michael@0: arguments: args, michael@0: frameId: item.id michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Callback for handling an exited frame. michael@0: */ michael@0: _onReturn: function(aPacket) { michael@0: if (!this._stack.length) { michael@0: return; michael@0: } michael@0: michael@0: const { name, id, location } = this._stack.pop(); michael@0: DebuggerView.Tracer.addTrace({ michael@0: type: aPacket.why, michael@0: name: name, michael@0: location: location, michael@0: depth: aPacket.depth, michael@0: frameId: id, michael@0: returnVal: aPacket.return || aPacket.throw || aPacket.yield michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Create an object which has the same interface as a normal object client, michael@0: * but since we already have all the information for an object that we will michael@0: * ever get (the server doesn't create actors when tracing, just firehoses michael@0: * data and forgets about it) just return the data immdiately. michael@0: * michael@0: * @param Object aObject michael@0: * The tracer object "grip" (more like a limited snapshot). michael@0: * @returns Object michael@0: * The synchronous client object. michael@0: */ michael@0: syncGripClient: function(aObject) { michael@0: return { michael@0: get isFrozen() { return aObject.frozen; }, michael@0: get isSealed() { return aObject.sealed; }, michael@0: get isExtensible() { return aObject.extensible; }, michael@0: michael@0: get ownProperties() { return aObject.ownProperties; }, michael@0: get prototype() { return null; }, michael@0: michael@0: getParameterNames: callback => callback(aObject), michael@0: getPrototypeAndProperties: callback => callback(aObject), michael@0: getPrototype: callback => callback(aObject), michael@0: michael@0: getOwnPropertyNames: (callback) => { michael@0: callback({ michael@0: ownPropertyNames: aObject.ownProperties michael@0: ? Object.keys(aObject.ownProperties) michael@0: : [] michael@0: }); michael@0: }, michael@0: michael@0: getProperty: (property, callback) => { michael@0: callback({ michael@0: descriptor: aObject.ownProperties michael@0: ? aObject.ownProperties[property] michael@0: : null michael@0: }); michael@0: }, michael@0: michael@0: getDisplayString: callback => callback("[object " + aObject.class + "]"), michael@0: michael@0: getScope: callback => callback({ michael@0: error: "scopeNotAvailable", michael@0: message: "Cannot get scopes for traced objects" michael@0: }) michael@0: }; michael@0: }, michael@0: michael@0: /** michael@0: * Wraps object snapshots received from the tracer server so that we can michael@0: * differentiate them from long living object grips from the debugger server michael@0: * in the variables view. michael@0: * michael@0: * @param Object aObject michael@0: * The object snapshot from the tracer actor. michael@0: */ michael@0: WrappedObject: function(aObject) { michael@0: this.object = aObject; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Handles breaking on event listeners in the currently debugged target. michael@0: */ michael@0: function EventListeners() { michael@0: this._onEventListeners = this._onEventListeners.bind(this); michael@0: } michael@0: michael@0: EventListeners.prototype = { michael@0: /** michael@0: * A list of event names on which the debuggee will automatically pause michael@0: * when invoked. michael@0: */ michael@0: activeEventNames: [], michael@0: michael@0: /** michael@0: * Updates the list of events types with listeners that, when invoked, michael@0: * will automatically pause the debuggee. The respective events are michael@0: * retrieved from the UI. michael@0: */ michael@0: scheduleEventBreakpointsUpdate: function() { michael@0: // Make sure we're not sending a batch of closely repeated requests. michael@0: // This can easily happen when toggling all events of a certain type. michael@0: setNamedTimeout("event-breakpoints-update", 0, () => { michael@0: this.activeEventNames = DebuggerView.EventListeners.getCheckedEvents(); michael@0: gThreadClient.pauseOnDOMEvents(this.activeEventNames); michael@0: michael@0: // Notify that event breakpoints were added/removed on the server. michael@0: window.emit(EVENTS.EVENT_BREAKPOINTS_UPDATED); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Fetches the currently attached event listeners from the debugee. michael@0: */ michael@0: scheduleEventListenersFetch: function() { michael@0: let getListeners = aCallback => gThreadClient.eventListeners(aResponse => { michael@0: if (aResponse.error) { michael@0: let msg = "Error getting event listeners: " + aResponse.message; michael@0: DevToolsUtils.reportException("scheduleEventListenersFetch", msg); michael@0: return; michael@0: } michael@0: michael@0: let outstandingListenersDefinitionSite = aResponse.listeners.map(aListener => { michael@0: const deferred = promise.defer(); michael@0: michael@0: gThreadClient.pauseGrip(aListener.function).getDefinitionSite(aResponse => { michael@0: if (aResponse.error) { michael@0: const msg = "Error getting function definition site: " + aResponse.message; michael@0: DevToolsUtils.reportException("scheduleEventListenersFetch", msg); michael@0: } else { michael@0: aListener.function.url = aResponse.url; michael@0: } michael@0: michael@0: deferred.resolve(aListener); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: }); michael@0: michael@0: promise.all(outstandingListenersDefinitionSite).then(aListeners => { michael@0: this._onEventListeners(aListeners); michael@0: michael@0: // Notify that event listeners were fetched and shown in the view, michael@0: // and callback to resume the active thread if necessary. michael@0: window.emit(EVENTS.EVENT_LISTENERS_FETCHED); michael@0: aCallback && aCallback(); michael@0: }); michael@0: }); michael@0: michael@0: // Make sure we're not sending a batch of closely repeated requests. michael@0: // This can easily happen whenever new sources are fetched. michael@0: setNamedTimeout("event-listeners-fetch", FETCH_EVENT_LISTENERS_DELAY, () => { michael@0: if (gThreadClient.state != "paused") { michael@0: gThreadClient.interrupt(() => getListeners(() => gThreadClient.resume())); michael@0: } else { michael@0: getListeners(); michael@0: } michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Callback for a debugger's successful active thread eventListeners() call. michael@0: */ michael@0: _onEventListeners: function(aListeners) { michael@0: // Add all the listeners in the debugger view event linsteners container. michael@0: for (let listener of aListeners) { michael@0: DebuggerView.EventListeners.addListener(listener, { staged: true }); michael@0: } michael@0: michael@0: // Flushes all the prepared events into the event listeners container. michael@0: DebuggerView.EventListeners.commit(); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Handles all the breakpoints in the current debugger. michael@0: */ michael@0: function Breakpoints() { michael@0: this._onEditorBreakpointAdd = this._onEditorBreakpointAdd.bind(this); michael@0: this._onEditorBreakpointRemove = this._onEditorBreakpointRemove.bind(this); michael@0: this.addBreakpoint = this.addBreakpoint.bind(this); michael@0: this.removeBreakpoint = this.removeBreakpoint.bind(this); michael@0: } michael@0: michael@0: Breakpoints.prototype = { michael@0: /** michael@0: * A map of breakpoint promises as tracked by the debugger frontend. michael@0: * The keys consist of a string representation of the breakpoint location. michael@0: */ michael@0: _added: new Map(), michael@0: _removing: new Map(), michael@0: _disabled: new Map(), michael@0: michael@0: /** michael@0: * Adds the source editor breakpoint handlers. michael@0: * michael@0: * @return object michael@0: * A promise that is resolved when the breakpoints finishes initializing. michael@0: */ michael@0: initialize: function() { michael@0: DebuggerView.editor.on("breakpointAdded", this._onEditorBreakpointAdd); michael@0: DebuggerView.editor.on("breakpointRemoved", this._onEditorBreakpointRemove); michael@0: michael@0: // Initialization is synchronous, for now. michael@0: return promise.resolve(null); michael@0: }, michael@0: michael@0: /** michael@0: * Removes the source editor breakpoint handlers & all the added breakpoints. michael@0: * michael@0: * @return object michael@0: * A promise that is resolved when the breakpoints finishes destroying. michael@0: */ michael@0: destroy: function() { michael@0: DebuggerView.editor.off("breakpointAdded", this._onEditorBreakpointAdd); michael@0: DebuggerView.editor.off("breakpointRemoved", this._onEditorBreakpointRemove); michael@0: michael@0: return this.removeAllBreakpoints(); michael@0: }, michael@0: michael@0: /** michael@0: * Event handler for new breakpoints that come from the editor. michael@0: * michael@0: * @param number aLine michael@0: * Line number where breakpoint was set. michael@0: */ michael@0: _onEditorBreakpointAdd: function(_, aLine) { michael@0: let url = DebuggerView.Sources.selectedValue; michael@0: let location = { url: url, line: aLine + 1 }; michael@0: michael@0: // Initialize the breakpoint, but don't update the editor, since this michael@0: // callback is invoked because a breakpoint was added in the editor itself. michael@0: this.addBreakpoint(location, { noEditorUpdate: true }).then(aBreakpointClient => { michael@0: // If the breakpoint client has an "requestedLocation" attached, then michael@0: // the original requested placement for the breakpoint wasn't accepted. michael@0: // In this case, we need to update the editor with the new location. michael@0: if (aBreakpointClient.requestedLocation) { michael@0: DebuggerView.editor.removeBreakpoint(aBreakpointClient.requestedLocation.line - 1); michael@0: DebuggerView.editor.addBreakpoint(aBreakpointClient.location.line - 1); michael@0: } michael@0: // Notify that we've shown a breakpoint in the source editor. michael@0: window.emit(EVENTS.BREAKPOINT_SHOWN); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Event handler for breakpoints that are removed from the editor. michael@0: * michael@0: * @param number aLine michael@0: * Line number where breakpoint was removed. michael@0: */ michael@0: _onEditorBreakpointRemove: function(_, aLine) { michael@0: let url = DebuggerView.Sources.selectedValue; michael@0: let location = { url: url, line: aLine + 1 }; michael@0: michael@0: // Destroy the breakpoint, but don't update the editor, since this callback michael@0: // is invoked because a breakpoint was removed from the editor itself. michael@0: this.removeBreakpoint(location, { noEditorUpdate: true }).then(() => { michael@0: // Notify that we've hidden a breakpoint in the source editor. michael@0: window.emit(EVENTS.BREAKPOINT_HIDDEN); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Update the breakpoints in the editor view. This function takes the list of michael@0: * breakpoints in the debugger and adds them back into the editor view. michael@0: * This is invoked when the selected script is changed, or when new sources michael@0: * are received via the _onNewSource and _onSourcesAdded event listeners. michael@0: */ michael@0: updateEditorBreakpoints: function() { michael@0: for (let breakpointPromise of this._addedOrDisabled) { michael@0: breakpointPromise.then(aBreakpointClient => { michael@0: let currentSourceUrl = DebuggerView.Sources.selectedValue; michael@0: let breakpointUrl = aBreakpointClient.location.url; michael@0: michael@0: // Update the view only if the breakpoint is in the currently shown source. michael@0: if (currentSourceUrl == breakpointUrl) { michael@0: this._showBreakpoint(aBreakpointClient, { noPaneUpdate: true }); michael@0: } michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Update the breakpoints in the pane view. This function takes the list of michael@0: * breakpoints in the debugger and adds them back into the breakpoints pane. michael@0: * This is invoked when new sources are received via the _onNewSource and michael@0: * _onSourcesAdded event listeners. michael@0: */ michael@0: updatePaneBreakpoints: function() { michael@0: for (let breakpointPromise of this._addedOrDisabled) { michael@0: breakpointPromise.then(aBreakpointClient => { michael@0: let container = DebuggerView.Sources; michael@0: let breakpointUrl = aBreakpointClient.location.url; michael@0: michael@0: // Update the view only if the breakpoint exists in a known source. michael@0: if (container.containsValue(breakpointUrl)) { michael@0: this._showBreakpoint(aBreakpointClient, { noEditorUpdate: true }); michael@0: } michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Add a breakpoint. michael@0: * michael@0: * @param object aLocation michael@0: * The location where you want the breakpoint. michael@0: * This object must have two properties: michael@0: * - url: the breakpoint's source location. michael@0: * - line: the breakpoint's line number. michael@0: * It can also have the following optional properties: michael@0: * - condition: only pause if this condition evaluates truthy michael@0: * @param object aOptions [optional] michael@0: * Additional options or flags supported by this operation: michael@0: * - openPopup: tells if the expression popup should be shown. michael@0: * - noEditorUpdate: tells if you want to skip editor updates. michael@0: * - noPaneUpdate: tells if you want to skip breakpoint pane updates. michael@0: * @return object michael@0: * A promise that is resolved after the breakpoint is added, or michael@0: * rejected if there was an error. michael@0: */ michael@0: addBreakpoint: Task.async(function*(aLocation, aOptions = {}) { michael@0: // Make sure a proper location is available. michael@0: if (!aLocation) { michael@0: throw new Error("Invalid breakpoint location."); michael@0: } michael@0: let addedPromise, removingPromise; michael@0: michael@0: // If the breakpoint was already added, or is currently being added at the michael@0: // specified location, then return that promise immediately. michael@0: if ((addedPromise = this._getAdded(aLocation))) { michael@0: return addedPromise; michael@0: } michael@0: michael@0: // If the breakpoint is currently being removed from the specified location, michael@0: // then wait for that to finish. michael@0: if ((removingPromise = this._getRemoving(aLocation))) { michael@0: yield removingPromise; michael@0: } michael@0: michael@0: let deferred = promise.defer(); michael@0: michael@0: // Remember the breakpoint initialization promise in the store. michael@0: let identifier = this.getIdentifier(aLocation); michael@0: this._added.set(identifier, deferred.promise); michael@0: michael@0: // Try adding the breakpoint. michael@0: gThreadClient.setBreakpoint(aLocation, Task.async(function*(aResponse, aBreakpointClient) { michael@0: // If the breakpoint response has an "actualLocation" attached, then michael@0: // the original requested placement for the breakpoint wasn't accepted. michael@0: if (aResponse.actualLocation) { michael@0: // Remember the initialization promise for the new location instead. michael@0: let oldIdentifier = identifier; michael@0: let newIdentifier = identifier = this.getIdentifier(aResponse.actualLocation); michael@0: this._added.delete(oldIdentifier); michael@0: this._added.set(newIdentifier, deferred.promise); michael@0: } michael@0: michael@0: // By default, new breakpoints are always enabled. Disabled breakpoints michael@0: // are, in fact, removed from the server but preserved in the frontend, michael@0: // so that they may not be forgotten across target navigations. michael@0: let disabledPromise = this._disabled.get(identifier); michael@0: if (disabledPromise) { michael@0: let aPrevBreakpointClient = yield disabledPromise; michael@0: let condition = aPrevBreakpointClient.getCondition(); michael@0: this._disabled.delete(identifier); michael@0: michael@0: if (condition) { michael@0: aBreakpointClient = yield aBreakpointClient.setCondition( michael@0: gThreadClient, michael@0: condition michael@0: ); michael@0: } michael@0: } michael@0: michael@0: if (aResponse.actualLocation) { michael@0: // Store the originally requested location in case it's ever needed michael@0: // and update the breakpoint client with the actual location. michael@0: aBreakpointClient.requestedLocation = aLocation; michael@0: aBreakpointClient.location = aResponse.actualLocation; michael@0: } michael@0: michael@0: // Preserve information about the breakpoint's line text, to display it michael@0: // in the sources pane without requiring fetching the source (for example, michael@0: // after the target navigated). Note that this will get out of sync michael@0: // if the source text contents change. michael@0: let line = aBreakpointClient.location.line - 1; michael@0: aBreakpointClient.text = DebuggerView.editor.getText(line).trim(); michael@0: michael@0: // Show the breakpoint in the editor and breakpoints pane, and resolve. michael@0: this._showBreakpoint(aBreakpointClient, aOptions); michael@0: michael@0: // Notify that we've added a breakpoint. michael@0: window.emit(EVENTS.BREAKPOINT_ADDED, aBreakpointClient); michael@0: deferred.resolve(aBreakpointClient); michael@0: }.bind(this))); michael@0: michael@0: return deferred.promise; michael@0: }), michael@0: michael@0: /** michael@0: * Remove a breakpoint. michael@0: * michael@0: * @param object aLocation michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: * @param object aOptions [optional] michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: * @return object michael@0: * A promise that is resolved after the breakpoint is removed, or michael@0: * rejected if there was an error. michael@0: */ michael@0: removeBreakpoint: function(aLocation, aOptions = {}) { michael@0: // Make sure a proper location is available. michael@0: if (!aLocation) { michael@0: return promise.reject(new Error("Invalid breakpoint location.")); michael@0: } michael@0: michael@0: // If the breakpoint was already removed, or has never even been added, michael@0: // then return a resolved promise immediately. michael@0: let addedPromise = this._getAdded(aLocation); michael@0: if (!addedPromise) { michael@0: return promise.resolve(aLocation); michael@0: } michael@0: michael@0: // If the breakpoint is currently being removed from the specified location, michael@0: // then return that promise immediately. michael@0: let removingPromise = this._getRemoving(aLocation); michael@0: if (removingPromise) { michael@0: return removingPromise; michael@0: } michael@0: michael@0: let deferred = promise.defer(); michael@0: michael@0: // Remember the breakpoint removal promise in the store. michael@0: let identifier = this.getIdentifier(aLocation); michael@0: this._removing.set(identifier, deferred.promise); michael@0: michael@0: // Retrieve the corresponding breakpoint client first. michael@0: addedPromise.then(aBreakpointClient => { michael@0: // Try removing the breakpoint. michael@0: aBreakpointClient.remove(aResponse => { michael@0: // If there was an error removing the breakpoint, reject the promise michael@0: // and forget about it that the breakpoint may be re-removed later. michael@0: if (aResponse.error) { michael@0: deferred.reject(aResponse); michael@0: return void this._removing.delete(identifier); michael@0: } michael@0: michael@0: // When a breakpoint is removed, the frontend may wish to preserve some michael@0: // details about it, so that it can be easily re-added later. In such michael@0: // cases, breakpoints are marked and stored as disabled, so that they michael@0: // may not be forgotten across target navigations. michael@0: if (aOptions.rememberDisabled) { michael@0: aBreakpointClient.disabled = true; michael@0: this._disabled.set(identifier, promise.resolve(aBreakpointClient)); michael@0: } michael@0: michael@0: // Forget both the initialization and removal promises from the store. michael@0: this._added.delete(identifier); michael@0: this._removing.delete(identifier); michael@0: michael@0: // Hide the breakpoint from the editor and breakpoints pane, and resolve. michael@0: this._hideBreakpoint(aLocation, aOptions); michael@0: michael@0: // Notify that we've removed a breakpoint. michael@0: window.emit(EVENTS.BREAKPOINT_REMOVED, aLocation); michael@0: deferred.resolve(aLocation); michael@0: }); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: /** michael@0: * Removes all the currently enabled breakpoints. michael@0: * michael@0: * @return object michael@0: * A promise that is resolved after all breakpoints are removed, or michael@0: * rejected if there was an error. michael@0: */ michael@0: removeAllBreakpoints: function() { michael@0: /* Gets an array of all the existing breakpoints promises. */ michael@0: let getActiveBreakpoints = (aPromises, aStore = []) => { michael@0: for (let [, breakpointPromise] of aPromises) { michael@0: aStore.push(breakpointPromise); michael@0: } michael@0: return aStore; michael@0: } michael@0: michael@0: /* Gets an array of all the removed breakpoints promises. */ michael@0: let getRemovedBreakpoints = (aClients, aStore = []) => { michael@0: for (let breakpointClient of aClients) { michael@0: aStore.push(this.removeBreakpoint(breakpointClient.location)); michael@0: } michael@0: return aStore; michael@0: } michael@0: michael@0: // First, populate an array of all the currently added breakpoints promises. michael@0: // Then, once all the breakpoints clients are retrieved, populate an array michael@0: // of all the removed breakpoints promises and wait for their fulfillment. michael@0: return promise.all(getActiveBreakpoints(this._added)).then(aBreakpointClients => { michael@0: return promise.all(getRemovedBreakpoints(aBreakpointClients)); michael@0: }); michael@0: }, michael@0: michael@0: /** michael@0: * Update the condition of a breakpoint. michael@0: * michael@0: * @param object aLocation michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: * @param string aClients michael@0: * The condition to set on the breakpoint michael@0: * @return object michael@0: * A promise that will be resolved with the breakpoint client michael@0: */ michael@0: updateCondition: function(aLocation, aCondition) { michael@0: let addedPromise = this._getAdded(aLocation); michael@0: if (!addedPromise) { michael@0: return promise.reject(new Error('breakpoint does not exist ' + michael@0: 'in specified location')); michael@0: } michael@0: michael@0: var promise = addedPromise.then(aBreakpointClient => { michael@0: return aBreakpointClient.setCondition(gThreadClient, aCondition); michael@0: }); michael@0: michael@0: // `setCondition` returns a new breakpoint that has the condition, michael@0: // so we need to update the store michael@0: this._added.set(this.getIdentifier(aLocation), promise); michael@0: return promise; michael@0: }, michael@0: michael@0: /** michael@0: * Update the editor and breakpoints pane to show a specified breakpoint. michael@0: * michael@0: * @param object aBreakpointData michael@0: * Information about the breakpoint to be shown. michael@0: * This object must have the following properties: michael@0: * - location: the breakpoint's source location and line number michael@0: * - disabled: the breakpoint's disabled state, boolean michael@0: * - text: the breakpoint's line text to be displayed michael@0: * @param object aOptions [optional] michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: */ michael@0: _showBreakpoint: function(aBreakpointData, aOptions = {}) { michael@0: let currentSourceUrl = DebuggerView.Sources.selectedValue; michael@0: let location = aBreakpointData.location; michael@0: michael@0: // Update the editor if required. michael@0: if (!aOptions.noEditorUpdate && !aBreakpointData.disabled) { michael@0: if (location.url == currentSourceUrl) { michael@0: DebuggerView.editor.addBreakpoint(location.line - 1); michael@0: } michael@0: } michael@0: michael@0: // Update the breakpoints pane if required. michael@0: if (!aOptions.noPaneUpdate) { michael@0: DebuggerView.Sources.addBreakpoint(aBreakpointData, aOptions); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Update the editor and breakpoints pane to hide a specified breakpoint. michael@0: * michael@0: * @param object aLocation michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: * @param object aOptions [optional] michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: */ michael@0: _hideBreakpoint: function(aLocation, aOptions = {}) { michael@0: let currentSourceUrl = DebuggerView.Sources.selectedValue; michael@0: michael@0: // Update the editor if required. michael@0: if (!aOptions.noEditorUpdate) { michael@0: if (aLocation.url == currentSourceUrl) { michael@0: DebuggerView.editor.removeBreakpoint(aLocation.line - 1); michael@0: } michael@0: } michael@0: michael@0: // Update the breakpoints pane if required. michael@0: if (!aOptions.noPaneUpdate) { michael@0: DebuggerView.Sources.removeBreakpoint(aLocation); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Get a Promise for the BreakpointActor client object which is already added michael@0: * or currently being added at the given location. michael@0: * michael@0: * @param object aLocation michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: * @return object | null michael@0: * A promise that is resolved after the breakpoint is added, or michael@0: * null if no breakpoint was found. michael@0: */ michael@0: _getAdded: function(aLocation) { michael@0: return this._added.get(this.getIdentifier(aLocation)); michael@0: }, michael@0: michael@0: /** michael@0: * Get a Promise for the BreakpointActor client object which is currently michael@0: * being removed from the given location. michael@0: * michael@0: * @param object aLocation michael@0: * @see DebuggerController.Breakpoints.addBreakpoint michael@0: * @return object | null michael@0: * A promise that is resolved after the breakpoint is removed, or michael@0: * null if no breakpoint was found. michael@0: */ michael@0: _getRemoving: function(aLocation) { michael@0: return this._removing.get(this.getIdentifier(aLocation)); michael@0: }, michael@0: michael@0: /** michael@0: * Get an identifier string for a given location. Breakpoint promises are michael@0: * identified in the store by a string representation of their location. michael@0: * michael@0: * @param object aLocation michael@0: * The location to serialize to a string. michael@0: * @return string michael@0: * The identifier string. michael@0: */ michael@0: getIdentifier: function(aLocation) { michael@0: return aLocation.url + ":" + aLocation.line; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Gets all Promises for the BreakpointActor client objects that are michael@0: * either enabled (added to the server) or disabled (removed from the server, michael@0: * but for which some details are preserved). michael@0: */ michael@0: Object.defineProperty(Breakpoints.prototype, "_addedOrDisabled", { michael@0: get: function* () { michael@0: yield* this._added.values(); michael@0: yield* this._disabled.values(); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Localization convenience methods. michael@0: */ michael@0: let L10N = new ViewHelpers.L10N(DBG_STRINGS_URI); michael@0: michael@0: /** michael@0: * Shortcuts for accessing various debugger preferences. michael@0: */ michael@0: let Prefs = new ViewHelpers.Prefs("devtools", { michael@0: sourcesWidth: ["Int", "debugger.ui.panes-sources-width"], michael@0: instrumentsWidth: ["Int", "debugger.ui.panes-instruments-width"], michael@0: panesVisibleOnStartup: ["Bool", "debugger.ui.panes-visible-on-startup"], michael@0: variablesSortingEnabled: ["Bool", "debugger.ui.variables-sorting-enabled"], michael@0: variablesOnlyEnumVisible: ["Bool", "debugger.ui.variables-only-enum-visible"], michael@0: variablesSearchboxVisible: ["Bool", "debugger.ui.variables-searchbox-visible"], michael@0: pauseOnExceptions: ["Bool", "debugger.pause-on-exceptions"], michael@0: ignoreCaughtExceptions: ["Bool", "debugger.ignore-caught-exceptions"], michael@0: sourceMapsEnabled: ["Bool", "debugger.source-maps-enabled"], michael@0: prettyPrintEnabled: ["Bool", "debugger.pretty-print-enabled"], michael@0: autoPrettyPrint: ["Bool", "debugger.auto-pretty-print"], michael@0: tracerEnabled: ["Bool", "debugger.tracer"], michael@0: editorTabSize: ["Int", "editor.tabsize"] michael@0: }); michael@0: michael@0: /** michael@0: * Convenient way of emitting events from the panel window. michael@0: */ michael@0: EventEmitter.decorate(this); michael@0: michael@0: /** michael@0: * Preliminary setup for the DebuggerController object. michael@0: */ michael@0: DebuggerController.initialize(); michael@0: DebuggerController.Parser = new Parser(); michael@0: DebuggerController.ThreadState = new ThreadState(); michael@0: DebuggerController.StackFrames = new StackFrames(); michael@0: DebuggerController.SourceScripts = new SourceScripts(); michael@0: DebuggerController.Breakpoints = new Breakpoints(); michael@0: DebuggerController.Breakpoints.DOM = new EventListeners(); michael@0: DebuggerController.Tracer = new Tracer(); michael@0: michael@0: /** michael@0: * Export some properties to the global scope for easier access. michael@0: */ michael@0: Object.defineProperties(window, { michael@0: "gTarget": { michael@0: get: function() DebuggerController._target michael@0: }, michael@0: "gHostType": { michael@0: get: function() DebuggerView._hostType michael@0: }, michael@0: "gClient": { michael@0: get: function() DebuggerController.client michael@0: }, michael@0: "gThreadClient": { michael@0: get: function() DebuggerController.activeThread michael@0: }, michael@0: "gCallStackPageSize": { michael@0: get: function() CALL_STACK_PAGE_SIZE michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Helper method for debugging. michael@0: * @param string michael@0: */ michael@0: function dumpn(str) { michael@0: if (wantLogging) { michael@0: dump("DBG-FRONTEND: " + str + "\n"); michael@0: } michael@0: } michael@0: michael@0: let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");