summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/debugger-controller.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/debugger/debugger-controller.js')
-rw-r--r--devtools/client/debugger/debugger-controller.js1276
1 files changed, 1276 insertions, 0 deletions
diff --git a/devtools/client/debugger/debugger-controller.js b/devtools/client/debugger/debugger-controller.js
new file mode 100644
index 000000000..ce6a467bc
--- /dev/null
+++ b/devtools/client/debugger/debugger-controller.js
@@ -0,0 +1,1276 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+const DBG_STRINGS_URI = "devtools/client/locales/debugger.properties";
+const NEW_SOURCE_IGNORED_URLS = ["debugger eval code", "XStringBundle"];
+const NEW_SOURCE_DISPLAY_DELAY = 200; // ms
+const FETCH_SOURCE_RESPONSE_DELAY = 200; // ms
+const FRAME_STEP_CLEAR_DELAY = 100; // ms
+const CALL_STACK_PAGE_SIZE = 25; // frames
+
+// The panel's window global is an EventEmitter firing the following events:
+const EVENTS = {
+ // When the debugger's source editor instance finishes loading or unloading.
+ EDITOR_LOADED: "Debugger:EditorLoaded",
+ EDITOR_UNLOADED: "Debugger:EditorUnloaded",
+
+ // When new sources are received from the debugger server.
+ NEW_SOURCE: "Debugger:NewSource",
+ SOURCES_ADDED: "Debugger:SourcesAdded",
+
+ // When a source is shown in the source editor.
+ SOURCE_SHOWN: "Debugger:EditorSourceShown",
+ SOURCE_ERROR_SHOWN: "Debugger:EditorSourceErrorShown",
+
+ // When the editor has shown a source and set the line / column position
+ EDITOR_LOCATION_SET: "Debugger:EditorLocationSet",
+
+ // When scopes, variables, properties and watch expressions are fetched and
+ // displayed in the variables view.
+ FETCHED_SCOPES: "Debugger:FetchedScopes",
+ FETCHED_VARIABLES: "Debugger:FetchedVariables",
+ FETCHED_PROPERTIES: "Debugger:FetchedProperties",
+ FETCHED_BUBBLE_PROPERTIES: "Debugger:FetchedBubbleProperties",
+ FETCHED_WATCH_EXPRESSIONS: "Debugger:FetchedWatchExpressions",
+
+ // When a breakpoint has been added or removed on the debugger server.
+ BREAKPOINT_ADDED: "Debugger:BreakpointAdded",
+ BREAKPOINT_REMOVED: "Debugger:BreakpointRemoved",
+ BREAKPOINT_CLICKED: "Debugger:BreakpointClicked",
+
+ // When a breakpoint has been shown or hidden in the source editor
+ // or the pane.
+ BREAKPOINT_SHOWN_IN_EDITOR: "Debugger:BreakpointShownInEditor",
+ BREAKPOINT_SHOWN_IN_PANE: "Debugger:BreakpointShownInPane",
+ BREAKPOINT_HIDDEN_IN_EDITOR: "Debugger:BreakpointHiddenInEditor",
+ BREAKPOINT_HIDDEN_IN_PANE: "Debugger:BreakpointHiddenInPane",
+
+ // When a conditional breakpoint's popup is shown/hidden.
+ CONDITIONAL_BREAKPOINT_POPUP_SHOWN: "Debugger:ConditionalBreakpointPopupShown",
+ CONDITIONAL_BREAKPOINT_POPUP_HIDDEN: "Debugger:ConditionalBreakpointPopupHidden",
+
+ // When event listeners are fetched or event breakpoints are updated.
+ EVENT_LISTENERS_FETCHED: "Debugger:EventListenersFetched",
+ EVENT_BREAKPOINTS_UPDATED: "Debugger:EventBreakpointsUpdated",
+
+ // When a file search was performed.
+ FILE_SEARCH_MATCH_FOUND: "Debugger:FileSearch:MatchFound",
+ FILE_SEARCH_MATCH_NOT_FOUND: "Debugger:FileSearch:MatchNotFound",
+
+ // When a function search was performed.
+ FUNCTION_SEARCH_MATCH_FOUND: "Debugger:FunctionSearch:MatchFound",
+ FUNCTION_SEARCH_MATCH_NOT_FOUND: "Debugger:FunctionSearch:MatchNotFound",
+
+ // When a global text search was performed.
+ GLOBAL_SEARCH_MATCH_FOUND: "Debugger:GlobalSearch:MatchFound",
+ GLOBAL_SEARCH_MATCH_NOT_FOUND: "Debugger:GlobalSearch:MatchNotFound",
+
+ // After the the StackFrames object has been filled with frames
+ AFTER_FRAMES_REFILLED: "Debugger:AfterFramesRefilled",
+
+ // After the stackframes are cleared and debugger won't pause anymore.
+ AFTER_FRAMES_CLEARED: "Debugger:AfterFramesCleared",
+
+ // When the options popup is showing or hiding.
+ OPTIONS_POPUP_SHOWING: "Debugger:OptionsPopupShowing",
+ OPTIONS_POPUP_HIDDEN: "Debugger:OptionsPopupHidden",
+
+ // When the widgets layout has been changed.
+ LAYOUT_CHANGED: "Debugger:LayoutChanged",
+
+ // When a worker has been selected.
+ WORKER_SELECTED: "Debugger::WorkerSelected"
+};
+
+// Descriptions for what a stack frame represents after the debugger pauses.
+const FRAME_TYPE = {
+ NORMAL: 0,
+ CONDITIONAL_BREAKPOINT_EVAL: 1,
+ WATCH_EXPRESSIONS_EVAL: 2,
+ PUBLIC_CLIENT_EVAL: 3
+};
+
+const { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
+const { require } = BrowserLoader({
+ baseURI: "resource://devtools/client/debugger/",
+ window,
+});
+const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineConstant(this, "require", require);
+const { SimpleListWidget } = require("resource://devtools/client/shared/widgets/SimpleListWidget.jsm");
+const { BreadcrumbsWidget } = require("resource://devtools/client/shared/widgets/BreadcrumbsWidget.jsm");
+const { SideMenuWidget } = require("resource://devtools/client/shared/widgets/SideMenuWidget.jsm");
+const { VariablesView } = require("resource://devtools/client/shared/widgets/VariablesView.jsm");
+const { VariablesViewController, StackFrameUtils } = require("resource://devtools/client/shared/widgets/VariablesViewController.jsm");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { gDevTools } = require("devtools/client/framework/devtools");
+const { ViewHelpers, Heritage, WidgetMethods, setNamedTimeout,
+ clearNamedTimeout } = require("devtools/client/shared/widgets/view-helpers");
+
+// React
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
+
+// Used to create the Redux store
+const createStore = require("devtools/client/shared/redux/create-store")({
+ getTargetClient: () => DebuggerController.client,
+ log: false
+});
+const {
+ makeStateBroadcaster,
+ enhanceStoreWithBroadcaster,
+ combineBroadcastingReducers
+} = require("devtools/client/shared/redux/non-react-subscriber");
+const { bindActionCreators } = require("devtools/client/shared/vendor/redux");
+const reducers = require("./content/reducers/index");
+const { onReducerEvents } = require("./content/utils");
+
+const waitUntilService = require("devtools/client/shared/redux/middleware/wait-service");
+var services = {
+ WAIT_UNTIL: waitUntilService.NAME
+};
+
+var Services = require("Services");
+var {TargetFactory} = require("devtools/client/framework/target");
+var {Toolbox} = require("devtools/client/framework/toolbox");
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var promise = require("devtools/shared/deprecated-sync-thenables");
+var Editor = require("devtools/client/sourceeditor/editor");
+var DebuggerEditor = require("devtools/client/sourceeditor/debugger");
+var Tooltip = require("devtools/client/shared/widgets/tooltip/Tooltip");
+var FastListWidget = require("devtools/client/shared/widgets/FastListWidget");
+var {LocalizationHelper, ELLIPSIS} = require("devtools/shared/l10n");
+var {PrefsHelper} = require("devtools/client/shared/prefs");
+var {Task} = require("devtools/shared/task");
+
+XPCOMUtils.defineConstant(this, "EVENTS", EVENTS);
+
+XPCOMUtils.defineLazyModuleGetter(this, "Parser",
+ "resource://devtools/shared/Parser.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
+ "resource://gre/modules/ShortcutUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper",
+ "@mozilla.org/widget/clipboardhelper;1", "nsIClipboardHelper");
+
+Object.defineProperty(this, "NetworkHelper", {
+ get: function () {
+ return require("devtools/shared/webconsole/network-helper");
+ },
+ configurable: true,
+ enumerable: true
+});
+
+/**
+ * Localization convenience methods.
+ */
+var L10N = new LocalizationHelper(DBG_STRINGS_URI);
+
+/**
+ * Object defining the debugger controller components.
+ */
+var DebuggerController = {
+ /**
+ * Initializes the debugger controller.
+ */
+ initialize: function () {
+ dumpn("Initializing the DebuggerController");
+
+ this.startupDebugger = this.startupDebugger.bind(this);
+ this.shutdownDebugger = this.shutdownDebugger.bind(this);
+ this._onNavigate = this._onNavigate.bind(this);
+ this._onWillNavigate = this._onWillNavigate.bind(this);
+ this._onTabDetached = this._onTabDetached.bind(this);
+
+ const broadcaster = makeStateBroadcaster(() => !!this.activeThread);
+ const reducer = combineBroadcastingReducers(
+ reducers,
+ broadcaster.emitChange
+ );
+ // TODO: Bug 1228867, clean this up and probably abstract it out
+ // better.
+ //
+ // We only want to process async event that are appropriate for
+ // this page. The devtools are open across page reloads, so async
+ // requests from the last page might bleed through if reloading
+ // fast enough. We check to make sure the async action is part of
+ // a current request, and ignore it if not.
+ let store = createStore((state, action) => {
+ if (action.seqId &&
+ (action.status === "done" || action.status === "error") &&
+ state && state.asyncRequests.indexOf(action.seqId) === -1) {
+ return state;
+ }
+ return reducer(state, action);
+ });
+ store = enhanceStoreWithBroadcaster(store, broadcaster);
+
+ // This controller right now acts as the store that's globally
+ // available, so just copy the Redux API onto it.
+ Object.keys(store).forEach(name => {
+ this[name] = store[name];
+ });
+ },
+
+ /**
+ * Initializes the view.
+ *
+ * @return object
+ * A promise that is resolved when the debugger finishes startup.
+ */
+ startupDebugger: Task.async(function* () {
+ if (this._startup) {
+ return;
+ }
+
+ yield DebuggerView.initialize(this._target.isWorkerTarget);
+ this._startup = true;
+ }),
+
+ /**
+ * Destroys the view and disconnects the debugger client from the server.
+ *
+ * @return object
+ * A promise that is resolved when the debugger finishes shutdown.
+ */
+ shutdownDebugger: Task.async(function* () {
+ if (this._shutdown) {
+ return;
+ }
+
+ DebuggerView.destroy();
+ this.StackFrames.disconnect();
+ this.ThreadState.disconnect();
+ if (this._target.isTabActor) {
+ this.Workers.disconnect();
+ }
+
+ this.disconnect();
+
+ this._shutdown = true;
+ }),
+
+ /**
+ * Initiates remote debugging based on the current target, wiring event
+ * handlers as necessary.
+ *
+ * @return object
+ * A promise that is resolved when the debugger finishes connecting.
+ */
+ connect: Task.async(function* () {
+ let target = this._target;
+
+ let { client } = target;
+ target.on("close", this._onTabDetached);
+ target.on("navigate", this._onNavigate);
+ target.on("will-navigate", this._onWillNavigate);
+ this.client = client;
+ this.activeThread = this._toolbox.threadClient;
+
+ // Disable asm.js so that we can set breakpoints and other things
+ // on asm.js scripts
+ yield this.reconfigureThread({ observeAsmJS: true });
+ yield this.connectThread();
+
+ // We need to call this to sync the state of the resume
+ // button in the toolbar with the state of the thread.
+ this.ThreadState._update();
+
+ this._hideUnsupportedFeatures();
+ }),
+
+ connectThread: function () {
+ const { newSource, fetchEventListeners } = bindActionCreators(actions, this.dispatch);
+
+ // TODO: bug 806775, update the globals list using aPacket.hostAnnotations
+ // from bug 801084.
+ // this.client.addListener("newGlobal", ...);
+
+ this.activeThread.addListener("newSource", (event, packet) => {
+ newSource(packet.source);
+
+ // Make sure the events listeners are up to date.
+ if (DebuggerView.instrumentsPaneTab == "events-tab") {
+ fetchEventListeners();
+ }
+ });
+
+ if (this._target.isTabActor) {
+ this.Workers.connect();
+ }
+ this.ThreadState.connect();
+ this.StackFrames.connect();
+
+ // Load all of the sources. Note that the server will actually
+ // emit individual `newSource` notifications, which trigger
+ // separate actions, so this won't do anything other than force
+ // the server to traverse sources.
+ this.dispatch(actions.loadSources()).then(() => {
+ // If the engine is already paused, update the UI to represent the
+ // paused state
+ if (this.activeThread) {
+ const pausedPacket = this.activeThread.getLastPausePacket();
+ DebuggerView.Toolbar.toggleResumeButtonState(
+ this.activeThread.state,
+ !!pausedPacket
+ );
+ if (pausedPacket) {
+ this.StackFrames._onPaused("paused", pausedPacket);
+ }
+ }
+ });
+ },
+
+ /**
+ * Disconnects the debugger client and removes event handlers as necessary.
+ */
+ disconnect: function () {
+ // Return early if the client didn't even have a chance to instantiate.
+ if (!this.client) {
+ return;
+ }
+
+ this.client.removeListener("newGlobal");
+ this.activeThread.removeListener("newSource");
+ this.activeThread.removeListener("blackboxchange");
+
+ this._connected = false;
+ this.client = null;
+ this.activeThread = null;
+ },
+
+ _hideUnsupportedFeatures: function () {
+ if (this.client.mainRoot.traits.noPrettyPrinting) {
+ DebuggerView.Sources.hidePrettyPrinting();
+ }
+
+ if (this.client.mainRoot.traits.noBlackBoxing) {
+ DebuggerView.Sources.hideBlackBoxing();
+ }
+ },
+
+ _onWillNavigate: function (opts = {}) {
+ // Reset UI.
+ DebuggerView.handleTabNavigation();
+ if (!opts.noUnload) {
+ this.dispatch(actions.unload());
+ }
+
+ // Discard all the cached parsed sources *before* the target
+ // starts navigating. Sources may be fetched during navigation, in
+ // which case we don't want to hang on to the old source contents.
+ DebuggerController.Parser.clearCache();
+ SourceUtils.clearCache();
+
+ // Prevent performing any actions that were scheduled before
+ // navigation.
+ clearNamedTimeout("new-source");
+ clearNamedTimeout("event-breakpoints-update");
+ clearNamedTimeout("event-listeners-fetch");
+ },
+
+ _onNavigate: function () {
+ this.ThreadState.handleTabNavigation();
+ this.StackFrames.handleTabNavigation();
+ },
+
+ /**
+ * Called when the debugged tab is closed.
+ */
+ _onTabDetached: function () {
+ this.shutdownDebugger();
+ },
+
+ /**
+ * Warn if resuming execution produced a wrongOrder error.
+ */
+ _ensureResumptionOrder: function (aResponse) {
+ if (aResponse.error == "wrongOrder") {
+ DebuggerView.Toolbar.showResumeWarning(aResponse.lastPausedUrl);
+ }
+ },
+
+ /**
+ * Detach and reattach to the thread actor with useSourceMaps true, blow
+ * away old sources and get them again.
+ */
+ reconfigureThread: function (opts) {
+ const deferred = promise.defer();
+ this.activeThread.reconfigure(
+ opts,
+ aResponse => {
+ if (aResponse.error) {
+ deferred.reject(aResponse.error);
+ return;
+ }
+
+ if (("useSourceMaps" in opts) || ("autoBlackBox" in opts)) {
+ // Reset the view and fetch all the sources again.
+ DebuggerView.handleTabNavigation();
+ this.dispatch(actions.unload());
+ this.dispatch(actions.loadSources());
+
+ // Update the stack frame list.
+ if (this.activeThread.paused) {
+ this.activeThread._clearFrames();
+ this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
+ }
+ }
+
+ deferred.resolve();
+ }
+ );
+ return deferred.promise;
+ },
+
+ waitForSourcesLoaded: function () {
+ const deferred = promise.defer();
+ this.dispatch({
+ type: services.WAIT_UNTIL,
+ predicate: action => (action.type === constants.LOAD_SOURCES &&
+ action.status === "done"),
+ run: deferred.resolve
+ });
+ return deferred.promise;
+ },
+
+ waitForSourceShown: function (name) {
+ const deferred = promise.defer();
+ window.on(EVENTS.SOURCE_SHOWN, function onShown(_, source) {
+ if (source.url.includes(name)) {
+ window.off(EVENTS.SOURCE_SHOWN, onShown);
+ deferred.resolve();
+ }
+ });
+ return deferred.promise;
+ },
+
+ _startup: false,
+ _shutdown: false,
+ _connected: false,
+ client: null,
+ activeThread: null
+};
+
+function Workers() {
+ this._workerForms = Object.create(null);
+ this._onWorkerListChanged = this._onWorkerListChanged.bind(this);
+ this._onWorkerSelect = this._onWorkerSelect.bind(this);
+}
+
+Workers.prototype = {
+ get _tabClient() {
+ return DebuggerController._target.activeTab;
+ },
+
+ connect: function () {
+ if (!Prefs.workersEnabled) {
+ return;
+ }
+
+ this._updateWorkerList();
+ this._tabClient.addListener("workerListChanged", this._onWorkerListChanged);
+ },
+
+ disconnect: function () {
+ this._tabClient.removeListener("workerListChanged", this._onWorkerListChanged);
+ },
+
+ _updateWorkerList: function () {
+ if (!this._tabClient.listWorkers) {
+ return;
+ }
+
+ this._tabClient.listWorkers((response) => {
+ let workerForms = Object.create(null);
+ for (let worker of response.workers) {
+ workerForms[worker.actor] = worker;
+ }
+
+ for (let workerActor in this._workerForms) {
+ if (!(workerActor in workerForms)) {
+ DebuggerView.Workers.removeWorker(this._workerForms[workerActor]);
+ delete this._workerForms[workerActor];
+ }
+ }
+
+ for (let workerActor in workerForms) {
+ if (!(workerActor in this._workerForms)) {
+ let workerForm = workerForms[workerActor];
+ this._workerForms[workerActor] = workerForm;
+ DebuggerView.Workers.addWorker(workerForm);
+ }
+ }
+ });
+ },
+
+ _onWorkerListChanged: function () {
+ this._updateWorkerList();
+ },
+
+ _onWorkerSelect: function (workerForm) {
+ DebuggerController.client.attachWorker(workerForm.actor, (response, workerClient) => {
+ let toolbox = gDevTools.showToolbox(TargetFactory.forWorker(workerClient),
+ "jsdebugger", Toolbox.HostType.WINDOW);
+ window.emit(EVENTS.WORKER_SELECTED, toolbox);
+ });
+ }
+};
+
+/**
+ * ThreadState keeps the UI up to date with the state of the
+ * thread (paused/attached/etc.).
+ */
+function ThreadState() {
+ this._update = this._update.bind(this);
+ this.interruptedByResumeButton = false;
+}
+
+ThreadState.prototype = {
+ get activeThread() {
+ return DebuggerController.activeThread;
+ },
+
+ /**
+ * Connect to the current thread client.
+ */
+ connect: function () {
+ dumpn("ThreadState is connecting...");
+ this.activeThread.addListener("paused", this._update);
+ this.activeThread.addListener("resumed", this._update);
+ },
+
+ /**
+ * Disconnect from the client.
+ */
+ disconnect: function () {
+ if (!this.activeThread) {
+ return;
+ }
+ dumpn("ThreadState is disconnecting...");
+ this.activeThread.removeListener("paused", this._update);
+ this.activeThread.removeListener("resumed", this._update);
+ },
+
+ /**
+ * Handles any initialization on a tab navigation event issued by the client.
+ */
+ handleTabNavigation: function () {
+ if (!this.activeThread) {
+ return;
+ }
+ dumpn("Handling tab navigation in the ThreadState");
+ this._update();
+ },
+
+ /**
+ * Update the UI after a thread state change.
+ */
+ _update: function (aEvent, aPacket) {
+ if (aEvent == "paused") {
+ if (aPacket.why.type == "interrupted" &&
+ this.interruptedByResumeButton) {
+ // Interrupt requests suppressed by default, but if this is an
+ // explicit interrupt by the pause button we want to emit it.
+ gTarget.emit("thread-paused", aPacket);
+ } else if (aPacket.why.type == "breakpointConditionThrown" && aPacket.why.message) {
+ let where = aPacket.frame.where;
+ let aLocation = {
+ line: where.line,
+ column: where.column,
+ actor: where.source ? where.source.actor : null
+ };
+ DebuggerView.Sources.showBreakpointConditionThrownMessage(
+ aLocation,
+ aPacket.why.message
+ );
+ }
+ }
+
+ this.interruptedByResumeButton = false;
+ DebuggerView.Toolbar.toggleResumeButtonState(
+ this.activeThread.state,
+ aPacket ? aPacket.frame : false
+ );
+ }
+};
+
+/**
+ * Keeps the stack frame list up-to-date, using the thread client's
+ * stack frame cache.
+ */
+function StackFrames() {
+ this._onPaused = this._onPaused.bind(this);
+ this._onResumed = this._onResumed.bind(this);
+ this._onFrames = this._onFrames.bind(this);
+ this._onFramesCleared = this._onFramesCleared.bind(this);
+ this._onBlackBoxChange = this._onBlackBoxChange.bind(this);
+ this._onPrettyPrintChange = this._onPrettyPrintChange.bind(this);
+ this._afterFramesCleared = this._afterFramesCleared.bind(this);
+ this.evaluate = this.evaluate.bind(this);
+}
+
+StackFrames.prototype = {
+ get activeThread() {
+ return DebuggerController.activeThread;
+ },
+
+ currentFrameDepth: -1,
+ _currentFrameDescription: FRAME_TYPE.NORMAL,
+ _syncedWatchExpressions: null,
+ _currentWatchExpressions: null,
+ _currentBreakpointLocation: null,
+ _currentEvaluation: null,
+ _currentException: null,
+ _currentReturnedValue: null,
+
+ /**
+ * Connect to the current thread client.
+ */
+ connect: function () {
+ dumpn("StackFrames is connecting...");
+ this.activeThread.addListener("paused", this._onPaused);
+ this.activeThread.addListener("resumed", this._onResumed);
+ this.activeThread.addListener("framesadded", this._onFrames);
+ this.activeThread.addListener("framescleared", this._onFramesCleared);
+ this.activeThread.addListener("blackboxchange", this._onBlackBoxChange);
+ this.activeThread.addListener("prettyprintchange", this._onPrettyPrintChange);
+ this.handleTabNavigation();
+ },
+
+ /**
+ * Disconnect from the client.
+ */
+ disconnect: function () {
+ if (!this.activeThread) {
+ return;
+ }
+ dumpn("StackFrames is disconnecting...");
+ this.activeThread.removeListener("paused", this._onPaused);
+ this.activeThread.removeListener("resumed", this._onResumed);
+ this.activeThread.removeListener("framesadded", this._onFrames);
+ this.activeThread.removeListener("framescleared", this._onFramesCleared);
+ this.activeThread.removeListener("blackboxchange", this._onBlackBoxChange);
+ this.activeThread.removeListener("prettyprintchange", this._onPrettyPrintChange);
+ clearNamedTimeout("frames-cleared");
+ },
+
+ /**
+ * Handles any initialization on a tab navigation event issued by the client.
+ */
+ handleTabNavigation: function () {
+ dumpn("Handling tab navigation in the StackFrames");
+ // Nothing to do here yet.
+ },
+
+ /**
+ * Handler for the thread client's paused notification.
+ *
+ * @param string aEvent
+ * The name of the notification ("paused" in this case).
+ * @param object aPacket
+ * The response packet.
+ */
+ _onPaused: function (aEvent, aPacket) {
+ switch (aPacket.why.type) {
+ // If paused by a breakpoint, store the breakpoint location.
+ case "breakpoint":
+ this._currentBreakpointLocation = aPacket.frame.where;
+ break;
+ case "breakpointConditionThrown":
+ this._currentBreakpointLocation = aPacket.frame.where;
+ this._conditionThrowMessage = aPacket.why.message;
+ break;
+ // If paused by a client evaluation, store the evaluated value.
+ case "clientEvaluated":
+ this._currentEvaluation = aPacket.why.frameFinished;
+ break;
+ // If paused by an exception, store the exception value.
+ case "exception":
+ this._currentException = aPacket.why.exception;
+ break;
+ // If paused while stepping out of a frame, store the returned value or
+ // thrown exception.
+ case "resumeLimit":
+ if (!aPacket.why.frameFinished) {
+ break;
+ } else if (aPacket.why.frameFinished.throw) {
+ this._currentException = aPacket.why.frameFinished.throw;
+ } else if (aPacket.why.frameFinished.return) {
+ this._currentReturnedValue = aPacket.why.frameFinished.return;
+ }
+ break;
+ // If paused by an explicit interrupt, which are generated by the slow
+ // script dialog and internal events such as setting breakpoints, ignore
+ // the event to avoid UI flicker.
+ case "interrupted":
+ if (!aPacket.why.onNext) {
+ return;
+ }
+ break;
+ }
+
+ this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
+ // Focus the editor, but don't steal focus from the split console.
+ if (!DebuggerController._toolbox.isSplitConsoleFocused()) {
+ DebuggerView.editor.focus();
+ }
+ },
+
+ /**
+ * Handler for the thread client's resumed notification.
+ */
+ _onResumed: function () {
+ // Prepare the watch expression evaluation string for the next pause.
+ if (this._currentFrameDescription != FRAME_TYPE.WATCH_EXPRESSIONS_EVAL) {
+ this._currentWatchExpressions = this._syncedWatchExpressions;
+ }
+ },
+
+ /**
+ * Handler for the thread client's framesadded notification.
+ */
+ _onFrames: Task.async(function* () {
+ // Ignore useless notifications.
+ if (!this.activeThread || !this.activeThread.cachedFrames.length) {
+ return;
+ }
+ if (this._currentFrameDescription != FRAME_TYPE.NORMAL &&
+ this._currentFrameDescription != FRAME_TYPE.PUBLIC_CLIENT_EVAL) {
+ return;
+ }
+
+ // TODO: remove all of this deprecated code: Bug 990137.
+ yield this._handleConditionalBreakpoint();
+
+ // TODO: handle all of this server-side: Bug 832470, comment 14.
+ yield this._handleWatchExpressions();
+
+ // Make sure the debugger view panes are visible, then refill the frames.
+ DebuggerView.showInstrumentsPane();
+ this._refillFrames();
+
+ // No additional processing is necessary for this stack frame.
+ if (this._currentFrameDescription != FRAME_TYPE.NORMAL) {
+ this._currentFrameDescription = FRAME_TYPE.NORMAL;
+ }
+ }),
+
+ /**
+ * Fill the StackFrames view with the frames we have in the cache, compressing
+ * frames which have black boxed sources into single frames.
+ */
+ _refillFrames: function () {
+ // Make sure all the previous stackframes are removed before re-adding them.
+ DebuggerView.StackFrames.empty();
+
+ for (let frame of this.activeThread.cachedFrames) {
+ let { depth, source, where: { line, column } } = frame;
+
+ let isBlackBoxed = source ? this.activeThread.source(source).isBlackBoxed : false;
+ DebuggerView.StackFrames.addFrame(frame, line, column, depth, isBlackBoxed);
+ }
+
+ DebuggerView.StackFrames.selectedDepth = Math.max(this.currentFrameDepth, 0);
+ DebuggerView.StackFrames.dirty = this.activeThread.moreFrames;
+
+ DebuggerView.StackFrames.addCopyContextMenu();
+
+ window.emit(EVENTS.AFTER_FRAMES_REFILLED);
+ },
+
+ /**
+ * Handler for the thread client's framescleared notification.
+ */
+ _onFramesCleared: function () {
+ switch (this._currentFrameDescription) {
+ case FRAME_TYPE.NORMAL:
+ this._currentEvaluation = null;
+ this._currentException = null;
+ this._currentReturnedValue = null;
+ break;
+ case FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL:
+ this._currentBreakpointLocation = null;
+ break;
+ case FRAME_TYPE.WATCH_EXPRESSIONS_EVAL:
+ this._currentWatchExpressions = null;
+ break;
+ }
+
+ // After each frame step (in, over, out), framescleared is fired, which
+ // forces the UI to be emptied and rebuilt on framesadded. Most of the times
+ // this is not necessary, and will result in a brief redraw flicker.
+ // To avoid it, invalidate the UI only after a short time if necessary.
+ setNamedTimeout("frames-cleared", FRAME_STEP_CLEAR_DELAY, this._afterFramesCleared);
+ },
+
+ /**
+ * Handler for the debugger's blackboxchange notification.
+ */
+ _onBlackBoxChange: function () {
+ if (this.activeThread.state == "paused") {
+ // Hack to avoid selecting the topmost frame after blackboxing a source.
+ this.currentFrameDepth = NaN;
+ this._refillFrames();
+ }
+ },
+
+ /**
+ * Handler for the debugger's prettyprintchange notification.
+ */
+ _onPrettyPrintChange: function () {
+ if (this.activeThread.state != "paused") {
+ return;
+ }
+ // Makes sure the selected source remains selected
+ // after the fillFrames is called.
+ const source = DebuggerView.Sources.selectedValue;
+
+ this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE, () => {
+ DebuggerView.Sources.selectedValue = source;
+ });
+ },
+
+ /**
+ * Called soon after the thread client's framescleared notification.
+ */
+ _afterFramesCleared: function () {
+ // Ignore useless notifications.
+ if (this.activeThread.cachedFrames.length) {
+ return;
+ }
+ DebuggerView.editor.clearDebugLocation();
+ DebuggerView.StackFrames.empty();
+ DebuggerView.Sources.unhighlightBreakpoint();
+ DebuggerView.WatchExpressions.toggleContents(true);
+ DebuggerView.Variables.empty(0);
+
+ window.emit(EVENTS.AFTER_FRAMES_CLEARED);
+ },
+
+ /**
+ * Marks the stack frame at the specified depth as selected and updates the
+ * properties view with the stack frame's data.
+ *
+ * @param number aDepth
+ * The depth of the frame in the stack.
+ */
+ selectFrame: function (aDepth) {
+ // Make sure the frame at the specified depth exists first.
+ let frame = this.activeThread.cachedFrames[this.currentFrameDepth = aDepth];
+ if (!frame) {
+ return;
+ }
+
+ // Check if the frame does not represent the evaluation of debuggee code.
+ let { environment, where, source } = frame;
+ if (!environment) {
+ return;
+ }
+
+ // Don't change the editor's location if the execution was paused by a
+ // public client evaluation. This is useful for adding overlays on
+ // top of the editor, like a variable inspection popup.
+ let isClientEval = this._currentFrameDescription == FRAME_TYPE.PUBLIC_CLIENT_EVAL;
+ let isPopupShown = DebuggerView.VariableBubble.contentsShown();
+ if (!isClientEval && !isPopupShown) {
+ // Move the editor's caret to the proper url and line.
+ DebuggerView.setEditorLocation(source.actor, where.line);
+ } else {
+ // Highlight the line where the execution is paused in the editor.
+ DebuggerView.setEditorLocation(source.actor, where.line, { noCaret: true });
+ }
+
+ // Highlight the breakpoint at the line and column if it exists.
+ DebuggerView.Sources.highlightBreakpointAtCursor();
+
+ // Don't display the watch expressions textbox inputs in the pane.
+ DebuggerView.WatchExpressions.toggleContents(false);
+
+ // Start recording any added variables or properties in any scope and
+ // clear existing scopes to create each one dynamically.
+ DebuggerView.Variables.empty();
+
+ // If watch expressions evaluation results are available, create a scope
+ // to contain all the values.
+ if (this._syncedWatchExpressions && aDepth == 0) {
+ let label = L10N.getStr("watchExpressionsScopeLabel");
+ let scope = DebuggerView.Variables.addScope(label,
+ "variables-view-watch-expressions");
+
+ // Customize the scope for holding watch expressions evaluations.
+ scope.descriptorTooltip = false;
+ scope.contextMenuId = "debuggerWatchExpressionsContextMenu";
+ scope.separatorStr = L10N.getStr("watchExpressionsSeparatorLabel2");
+ scope.switch = DebuggerView.WatchExpressions.switchExpression;
+ scope.delete = DebuggerView.WatchExpressions.deleteExpression;
+
+ // The evaluation hasn't thrown, so fetch and add the returned results.
+ this._fetchWatchExpressions(scope, this._currentEvaluation.return);
+
+ // The watch expressions scope is always automatically expanded.
+ scope.expand();
+ }
+
+ do {
+ // Create a scope to contain all the inspected variables in the
+ // current environment.
+ let label = StackFrameUtils.getScopeLabel(environment);
+ let scope = DebuggerView.Variables.addScope(label);
+ let innermost = environment == frame.environment;
+
+ // Handle special additions to the innermost scope.
+ if (innermost) {
+ this._insertScopeFrameReferences(scope, frame);
+ }
+
+ // Handle the expansion of the scope, lazily populating it with the
+ // variables in the current environment.
+ DebuggerView.Variables.controller.addExpander(scope, environment);
+
+ // The innermost scope is always automatically expanded, because it
+ // contains the variables in the current stack frame which are likely to
+ // be inspected. The previously expanded scopes are also reexpanded here.
+ if (innermost || DebuggerView.Variables.wasExpanded(scope)) {
+ scope.expand();
+ }
+ } while ((environment = environment.parent));
+
+ // Signal that scope environments have been shown.
+ window.emit(EVENTS.FETCHED_SCOPES);
+ },
+
+ /**
+ * Loads more stack frames from the debugger server cache.
+ */
+ addMoreFrames: function () {
+ this.activeThread.fillFrames(
+ this.activeThread.cachedFrames.length + CALL_STACK_PAGE_SIZE);
+ },
+
+ /**
+ * Evaluate an expression in the context of the selected frame.
+ *
+ * @param string aExpression
+ * The expression to evaluate.
+ * @param object aOptions [optional]
+ * Additional options for this client evaluation:
+ * - depth: the frame depth used for evaluation, 0 being the topmost.
+ * - meta: some meta-description for what this evaluation represents.
+ * @return object
+ * A promise that is resolved when the evaluation finishes,
+ * or rejected if there was no stack frame available or some
+ * other error occurred.
+ */
+ evaluate: function (aExpression, aOptions = {}) {
+ let depth = "depth" in aOptions ? aOptions.depth : this.currentFrameDepth;
+ let frame = this.activeThread.cachedFrames[depth];
+ if (frame == null) {
+ return promise.reject(new Error("No stack frame available."));
+ }
+
+ let deferred = promise.defer();
+
+ this.activeThread.addOneTimeListener("paused", (aEvent, aPacket) => {
+ let { type, frameFinished } = aPacket.why;
+ if (type == "clientEvaluated") {
+ deferred.resolve(frameFinished);
+ } else {
+ deferred.reject(new Error("Active thread paused unexpectedly."));
+ }
+ });
+
+ let meta = "meta" in aOptions ? aOptions.meta : FRAME_TYPE.PUBLIC_CLIENT_EVAL;
+ this._currentFrameDescription = meta;
+ this.activeThread.eval(frame.actor, aExpression);
+
+ return deferred.promise;
+ },
+
+ /**
+ * Add nodes for special frame references in the innermost scope.
+ *
+ * @param Scope aScope
+ * The scope where the references will be placed into.
+ * @param object aFrame
+ * The frame to get some references from.
+ */
+ _insertScopeFrameReferences: function (aScope, aFrame) {
+ // Add any thrown exception.
+ if (this._currentException) {
+ let excRef = aScope.addItem("<exception>", { value: this._currentException },
+ { internalItem: true });
+ DebuggerView.Variables.controller.addExpander(excRef, this._currentException);
+ }
+ // Add any returned value.
+ if (this._currentReturnedValue) {
+ let retRef = aScope.addItem("<return>",
+ { value: this._currentReturnedValue },
+ { internalItem: true });
+ DebuggerView.Variables.controller.addExpander(retRef, this._currentReturnedValue);
+ }
+ // Add "this".
+ if (aFrame.this) {
+ let thisRef = aScope.addItem("this", { value: aFrame.this });
+ DebuggerView.Variables.controller.addExpander(thisRef, aFrame.this);
+ }
+ },
+
+ /**
+ * Handles conditional breakpoints when the debugger pauses and the
+ * stackframes are received.
+ *
+ * We moved conditional breakpoint handling to the server, but
+ * need to support it in the client for a while until most of the
+ * server code in production is updated with it.
+ * TODO: remove all of this deprecated code: Bug 990137.
+ *
+ * @return object
+ * A promise that is resolved after a potential breakpoint's
+ * conditional expression is evaluated. If there's no breakpoint
+ * where the debugger is paused, the promise is resolved immediately.
+ */
+ _handleConditionalBreakpoint: Task.async(function* () {
+ if (gClient.mainRoot.traits.conditionalBreakpoints) {
+ return;
+ }
+ let breakLocation = this._currentBreakpointLocation;
+ if (!breakLocation) {
+ return;
+ }
+
+ let bp = queries.getBreakpoint(DebuggerController.getState(), {
+ actor: breakLocation.source.actor,
+ line: breakLocation.line
+ });
+ let conditionalExpression = bp.condition;
+ if (!conditionalExpression) {
+ return;
+ }
+
+ // Evaluating the current breakpoint's conditional expression will
+ // cause the stack frames to be cleared and active thread to pause,
+ // sending a 'clientEvaluated' packed and adding the frames again.
+ let evaluationOptions = { depth: 0, meta: FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL };
+ yield this.evaluate(conditionalExpression, evaluationOptions);
+ this._currentFrameDescription = FRAME_TYPE.NORMAL;
+
+ // If the breakpoint's conditional expression evaluation is falsy
+ // and there is no exception, automatically resume execution.
+ if (!this._currentEvaluation.throw &&
+ VariablesView.isFalsy({ value: this._currentEvaluation.return })) {
+ this.activeThread.resume(DebuggerController._ensureResumptionOrder);
+ }
+ }),
+
+ /**
+ * Handles watch expressions when the debugger pauses and the stackframes
+ * are received.
+ *
+ * @return object
+ * A promise that is resolved after the potential watch expressions
+ * are evaluated. If there are no watch expressions where the debugger
+ * is paused, the promise is resolved immediately.
+ */
+ _handleWatchExpressions: Task.async(function* () {
+ // Ignore useless notifications.
+ if (!this.activeThread || !this.activeThread.cachedFrames.length) {
+ return;
+ }
+
+ let watchExpressions = this._currentWatchExpressions;
+ if (!watchExpressions) {
+ return;
+ }
+
+ // Evaluation causes the stack frames to be cleared and active thread to
+ // pause, sending a 'clientEvaluated' packet and adding the frames again.
+ let evaluationOptions = { depth: 0, meta: FRAME_TYPE.WATCH_EXPRESSIONS_EVAL };
+ yield this.evaluate(watchExpressions, evaluationOptions);
+ this._currentFrameDescription = FRAME_TYPE.NORMAL;
+
+ // If an error was thrown during the evaluation of the watch expressions
+ // or the evaluation was terminated from the slow script dialog, then at
+ // least one expression evaluation could not be performed. So remove the
+ // most recent watch expression and try again.
+ if (this._currentEvaluation.throw || this._currentEvaluation.terminated) {
+ DebuggerView.WatchExpressions.removeAt(0);
+ yield DebuggerController.StackFrames.syncWatchExpressions();
+ }
+ }),
+
+ /**
+ * Adds the watch expressions evaluation results to a scope in the view.
+ *
+ * @param Scope aScope
+ * The scope where the watch expressions will be placed into.
+ * @param object aExp
+ * The grip of the evaluation results.
+ */
+ _fetchWatchExpressions: function (aScope, aExp) {
+ // Fetch the expressions only once.
+ if (aScope._fetched) {
+ return;
+ }
+ aScope._fetched = true;
+
+ // Add nodes for every watch expression in scope.
+ this.activeThread.pauseGrip(aExp).getPrototypeAndProperties(aResponse => {
+ let ownProperties = aResponse.ownProperties;
+ let totalExpressions = DebuggerView.WatchExpressions.itemCount;
+
+ for (let i = 0; i < totalExpressions; i++) {
+ let name = DebuggerView.WatchExpressions.getString(i);
+ let expVal = ownProperties[i].value;
+ let expRef = aScope.addItem(name, ownProperties[i]);
+ DebuggerView.Variables.controller.addExpander(expRef, expVal);
+
+ // Revert some of the custom watch expressions scope presentation flags,
+ // so that they don't propagate to child items.
+ expRef.switch = null;
+ expRef.delete = null;
+ expRef.descriptorTooltip = true;
+ expRef.separatorStr = L10N.getStr("variablesSeparatorLabel");
+ }
+
+ // Signal that watch expressions have been fetched.
+ window.emit(EVENTS.FETCHED_WATCH_EXPRESSIONS);
+ });
+ },
+
+ /**
+ * Updates a list of watch expressions to evaluate on each pause.
+ * TODO: handle all of this server-side: Bug 832470, comment 14.
+ */
+ syncWatchExpressions: function () {
+ let list = DebuggerView.WatchExpressions.getAllStrings();
+
+ // Sanity check all watch expressions before syncing them. To avoid
+ // having the whole watch expressions array throw because of a single
+ // faulty expression, simply convert it to a string describing the error.
+ // There's no other information necessary to be offered in such cases.
+ let sanitizedExpressions = list.map(aString => {
+ // Reflect.parse throws when it encounters a syntax error.
+ try {
+ Parser.reflectionAPI.parse(aString);
+ return aString; // Watch expression can be executed safely.
+ } catch (e) {
+ return "\"" + e.name + ": " + e.message + "\""; // Syntax error.
+ }
+ });
+
+ if (!sanitizedExpressions.length) {
+ this._currentWatchExpressions = null;
+ this._syncedWatchExpressions = null;
+ } else {
+ this._syncedWatchExpressions =
+ this._currentWatchExpressions = "[" +
+ sanitizedExpressions.map(aString =>
+ "eval(\"" +
+ "try {" +
+ // Make sure all quotes are escaped in the expression's syntax,
+ // and add a newline after the statement to avoid comments
+ // breaking the code integrity inside the eval block.
+ aString.replace(/\\/g, "\\\\").replace(/"/g, "\\$&") + "\" + " + "'\\n'" + " + \"" +
+ "} catch (e) {" +
+ "e.name + ': ' + e.message;" + // TODO: Bug 812765, 812764.
+ "}" +
+ "\")"
+ ).join(",") +
+ "]";
+ }
+
+ this.currentFrameDepth = -1;
+ return this._onFrames();
+ }
+};
+
+/**
+ * Shortcuts for accessing various debugger preferences.
+ */
+var Prefs = new PrefsHelper("devtools", {
+ workersAndSourcesWidth: ["Int", "debugger.ui.panes-workers-and-sources-width"],
+ instrumentsWidth: ["Int", "debugger.ui.panes-instruments-width"],
+ panesVisibleOnStartup: ["Bool", "debugger.ui.panes-visible-on-startup"],
+ variablesSortingEnabled: ["Bool", "debugger.ui.variables-sorting-enabled"],
+ variablesOnlyEnumVisible: ["Bool", "debugger.ui.variables-only-enum-visible"],
+ variablesSearchboxVisible: ["Bool", "debugger.ui.variables-searchbox-visible"],
+ pauseOnExceptions: ["Bool", "debugger.pause-on-exceptions"],
+ ignoreCaughtExceptions: ["Bool", "debugger.ignore-caught-exceptions"],
+ sourceMapsEnabled: ["Bool", "debugger.source-maps-enabled"],
+ prettyPrintEnabled: ["Bool", "debugger.pretty-print-enabled"],
+ autoPrettyPrint: ["Bool", "debugger.auto-pretty-print"],
+ workersEnabled: ["Bool", "debugger.workers"],
+ editorTabSize: ["Int", "editor.tabsize"],
+ autoBlackBox: ["Bool", "debugger.auto-black-box"],
+});
+
+/**
+ * Convenient way of emitting events from the panel window.
+ */
+EventEmitter.decorate(this);
+
+/**
+ * Preliminary setup for the DebuggerController object.
+ */
+DebuggerController.initialize();
+DebuggerController.Parser = new Parser();
+DebuggerController.Workers = new Workers();
+DebuggerController.ThreadState = new ThreadState();
+DebuggerController.StackFrames = new StackFrames();
+
+/**
+ * Export some properties to the global scope for easier access.
+ */
+Object.defineProperties(window, {
+ "gTarget": {
+ get: function () {
+ return DebuggerController._target;
+ },
+ configurable: true
+ },
+ "gHostType": {
+ get: function () {
+ return DebuggerView._hostType;
+ },
+ configurable: true
+ },
+ "gClient": {
+ get: function () {
+ return DebuggerController.client;
+ },
+ configurable: true
+ },
+ "gThreadClient": {
+ get: function () {
+ return DebuggerController.activeThread;
+ },
+ configurable: true
+ },
+ "gCallStackPageSize": {
+ get: function () {
+ return CALL_STACK_PAGE_SIZE;
+ },
+ configurable: true
+ }
+});
+
+/**
+ * Helper method for debugging.
+ * @param string
+ */
+function dumpn(str) {
+ if (wantLogging) {
+ dump("DBG-FRONTEND: " + str + "\n");
+ }
+}
+
+var wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");