gecko/browser/devtools/debugger/debugger-controller.js

1481 lines
48 KiB
JavaScript

/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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";
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
const NEW_SCRIPT_DISPLAY_DELAY = 200; // ms
const FRAME_STEP_CLEAR_DELAY = 100; // ms
const CALL_STACK_PAGE_SIZE = 25; // frames
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
Cu.import("resource:///modules/source-editor.jsm");
Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
Cu.import("resource:///modules/devtools/VariablesView.jsm");
/**
* Object defining the debugger controller components.
*/
let DebuggerController = {
/**
* Initializes the debugger controller.
*/
initialize: function DC_initialize() {
dumpn("Initializing the DebuggerController");
this._startupDebugger = this._startupDebugger.bind(this);
this._shutdownDebugger = this._shutdownDebugger.bind(this);
this._onTabNavigated = this._onTabNavigated.bind(this);
this._onTabDetached = this._onTabDetached.bind(this);
window.addEventListener("load", this._startupDebugger, true);
window.addEventListener("unload", this._shutdownDebugger, true);
},
/**
* Initializes the view and connects a debugger client to the server.
*/
_startupDebugger: function DC__startupDebugger() {
if (this._isInitialized) {
return;
}
this._isInitialized = true;
window.removeEventListener("load", this._startupDebugger, true);
DebuggerView.initialize(function() {
DebuggerView._isInitialized = true;
window.dispatchEvent("Debugger:Loaded");
this._connect();
}.bind(this));
},
/**
* Destroys the view and disconnects the debugger client from the server.
*/
_shutdownDebugger: function DC__shutdownDebugger() {
if (this._isDestroyed || !DebuggerView._isInitialized) {
return;
}
this._isDestroyed = true;
window.removeEventListener("unload", this._shutdownDebugger, true);
DebuggerView.destroy(function() {
DebuggerView._isDestroyed = true;
this.SourceScripts.disconnect();
this.StackFrames.disconnect();
this.ThreadState.disconnect();
this._disconnect();
window.dispatchEvent("Debugger:Unloaded");
window._isChromeDebugger && this._quitApp();
}.bind(this));
},
/**
* Prepares the hostname and port number for a remote debugger connection
* and handles connection retries and timeouts.
*
* @return boolean
* True if connection should proceed normally, false otherwise.
*/
_prepareConnection: function DC__prepareConnection() {
// If we exceeded the total number of connection retries, bail.
if (this._remoteConnectionTry === Prefs.remoteConnectionRetries) {
Services.prompt.alert(null,
L10N.getStr("remoteDebuggerPromptTitle"),
L10N.getStr("remoteDebuggerConnectionFailedMessage"));
// If the connection was not established before a certain number of
// retries, close the remote debugger.
this._shutdownDebugger();
return false;
}
// TODO: This is ugly, need to rethink the design for the UI in #751677.
if (!Prefs.remoteAutoConnect) {
let prompt = new RemoteDebuggerPrompt();
let result = prompt.show(!!this._remoteConnectionTimeout);
// If the connection was not established before the user canceled the
// prompt, close the remote debugger.
if (!result) {
this._shutdownDebugger();
return false;
}
Prefs.remoteHost = prompt.remote.host;
Prefs.remotePort = prompt.remote.port;
Prefs.remoteAutoConnect = prompt.remote.auto;
}
// If this debugger is connecting remotely to a server, we need to check
// after a while if the connection actually succeeded.
this._remoteConnectionTry = ++this._remoteConnectionTry || 1;
this._remoteConnectionTimeout = window.setTimeout(function() {
// If we couldn't connect to any server yet, try again...
if (!this.activeThread) {
this._onRemoteConnectionTimeout();
this._connect();
}
}.bind(this), Prefs.remoteTimeout);
// Proceed with the connection normally.
return true;
},
/**
* Called when a remote connection timeout occurs.
*/
_onRemoteConnectionTimeout: function DC__onRemoteConnectionTimeout() {
Cu.reportError("Couldn't connect to " +
Prefs.remoteHost + ":" + Prefs.remotePort);
},
/**
* Initializes a debugger client and connects it to the debugger server,
* wiring event handlers as necessary.
*/
_connect: function DC__connect() {
if (window._isRemoteDebugger && !this._prepareConnection()) {
return;
}
let transport = (window._isChromeDebugger || window._isRemoteDebugger)
? debuggerSocketConnect(Prefs.remoteHost, Prefs.remotePort)
: DebuggerServer.connectPipe();
let client = this.client = new DebuggerClient(transport);
client.addListener("tabNavigated", this._onTabNavigated);
client.addListener("tabDetached", this._onTabDetached);
client.connect(function(aType, aTraits) {
client.listTabs(function(aResponse) {
if (window._isChromeDebugger) {
let dbg = aResponse.chromeDebugger;
this._startChromeDebugging(client, dbg);
} else {
let tab = aResponse.tabs[aResponse.selected];
this._startDebuggingTab(client, tab);
}
window.dispatchEvent("Debugger:Connected");
}.bind(this));
}.bind(this));
},
/**
* Disconnects the debugger client and removes event handlers as necessary.
*/
_disconnect: function DC__disconnect() {
// Return early if the client didn't even have a chance to instantiate.
if (!this.client) {
return;
}
this.client.removeListener("tabNavigated", this._onTabNavigated);
this.client.removeListener("tabDetached", this._onTabDetached);
this.client.close();
this.client = null;
this.tabClient = null;
this.activeThread = null;
},
/**
* Called for each location change in the debugged tab.
*/
_onTabNavigated: function DC__onTabNavigated() {
DebuggerView._handleTabNavigation();
this.ThreadState._handleTabNavigation();
this.StackFrames._handleTabNavigation();
this.SourceScripts._handleTabNavigation();
},
/**
* Called when the debugged tab is closed.
*/
_onTabDetached: function DC__onTabDetached() {
this._shutdownDebugger();
},
/**
* Sets up a debugging session.
*
* @param DebuggerClient aClient
* The debugger client.
* @param object aTabGrip
* The remote protocol grip of the tab.
*/
_startDebuggingTab: function DC__startDebuggingTab(aClient, aTabGrip) {
if (!aClient) {
Cu.reportError("No client found!");
return;
}
this.client = aClient;
aClient.attachTab(aTabGrip.actor, function(aResponse, aTabClient) {
if (!aTabClient) {
Cu.reportError("No tab client found!");
return;
}
this.tabClient = aTabClient;
aClient.attachThread(aResponse.threadActor, function(aResponse, aThreadClient) {
if (!aThreadClient) {
Cu.reportError("Couldn't attach to thread: " + aResponse.error);
return;
}
this.activeThread = aThreadClient;
this.ThreadState.connect();
this.StackFrames.connect();
this.SourceScripts.connect();
aThreadClient.resume();
}.bind(this));
}.bind(this));
},
/**
* Sets up a chrome debugging session.
*
* @param DebuggerClient aClient
* The debugger client.
* @param object aChromeDebugger
* The remote protocol grip of the chrome debugger.
*/
_startChromeDebugging: function DC__startChromeDebugging(aClient, aChromeDebugger) {
if (!aClient) {
Cu.reportError("No client found!");
return;
}
this.client = aClient;
aClient.attachThread(aChromeDebugger, function(aResponse, aThreadClient) {
if (!aThreadClient) {
Cu.reportError("Couldn't attach to thread: " + aResponse.error);
return;
}
this.activeThread = aThreadClient;
this.ThreadState.connect();
this.StackFrames.connect();
this.SourceScripts.connect();
aThreadClient.resume();
}.bind(this));
},
/**
* Attempts to quit the current process if allowed.
*/
_quitApp: function DC__quitApp() {
let canceled = Cc["@mozilla.org/supports-PRBool;1"]
.createInstance(Ci.nsISupportsPRBool);
Services.obs.notifyObservers(canceled, "quit-application-requested", null);
// Somebody canceled our quit request.
if (canceled.data) {
return;
}
Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit);
},
/**
* Convenience method, dispatching a custom event.
*
* @param string aType
* The name of the event.
* @param any aDetail
* The data passed when initializing the event.
*/
dispatchEvent: function DC_dispatchEvent(aType, aDetail) {
let evt = document.createEvent("CustomEvent");
evt.initCustomEvent(aType, true, false, aDetail);
document.documentElement.dispatchEvent(evt);
}
};
/**
* ThreadState keeps the UI up to date with the state of the
* thread (paused/attached/etc.).
*/
function ThreadState() {
this._update = this._update.bind(this);
}
ThreadState.prototype = {
get activeThread() DebuggerController.activeThread,
/**
* Connect to the current thread client.
*/
connect: function TS_connect() {
dumpn("ThreadState is connecting...");
this.activeThread.addListener("paused", this._update);
this.activeThread.addListener("resumed", this._update);
this.activeThread.addListener("detached", this._update);
this._handleTabNavigation();
},
/**
* Disconnect from the client.
*/
disconnect: function TS_disconnect() {
if (!this.activeThread) {
return;
}
dumpn("ThreadState is disconnecting...");
this.activeThread.removeListener("paused", this._update);
this.activeThread.removeListener("resumed", this._update);
this.activeThread.removeListener("detached", this._update);
},
/**
* Handles any initialization on a tab navigation event issued by the client.
*/
_handleTabNavigation: function TS__handleTabNavigation() {
if (!this.activeThread) {
return;
}
dumpn("Handling tab navigation in the ThreadState");
this._update(this.activeThread.state);
},
/**
* Update the UI after a thread state change.
*/
_update: function TS__update(aEvent) {
DebuggerView.Toolbar.toggleResumeButtonState(this.activeThread.state);
}
};
/**
* 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._afterFramesCleared = this._afterFramesCleared.bind(this);
this.evaluate = this.evaluate.bind(this);
}
StackFrames.prototype = {
get activeThread() DebuggerController.activeThread,
autoScopeExpand: false,
currentFrame: null,
currentException: null,
/**
* Connect to the current thread client.
*/
connect: function SF_connect() {
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._handleTabNavigation();
},
/**
* Disconnect from the client.
*/
disconnect: function SF_disconnect() {
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);
},
/**
* Handles any initialization on a tab navigation event issued by the client.
*/
_handleTabNavigation: function SF__handleTabNavigation() {
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 SF__onPaused(aEvent, aPacket) {
// In case the pause was caused by an exception, store the exception value.
if (aPacket.why.type == "exception") {
this.currentException = aPacket.why.exception;
}
this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE);
DebuggerView.editor.focus();
},
/**
* Handler for the thread client's resumed notification.
*/
_onResumed: function SF__onResumed() {
DebuggerView.editor.setDebugLocation(-1);
},
/**
* Handler for the thread client's framesadded notification.
*/
_onFrames: function SF__onFrames() {
// Ignore useless notifications.
if (!this.activeThread.cachedFrames.length) {
return;
}
DebuggerView.StackFrames.empty();
for (let frame of this.activeThread.cachedFrames) {
this._addFrame(frame);
}
if (!this.currentFrame) {
this.selectFrame(0);
}
if (this.activeThread.moreFrames) {
DebuggerView.StackFrames.dirty = true;
}
},
/**
* Handler for the thread client's framescleared notification.
*/
_onFramesCleared: function SF__onFramesCleared() {
this.currentFrame = null;
this.currentException = null;
// 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.
window.setTimeout(this._afterFramesCleared, FRAME_STEP_CLEAR_DELAY);
},
/**
* Called soon after the thread client's framescleared notification.
*/
_afterFramesCleared: function SF__afterFramesCleared() {
// Ignore useless notifications.
if (this.activeThread.cachedFrames.length) {
return;
}
DebuggerView.StackFrames.empty();
DebuggerView.Variables.empty(0);
DebuggerView.Breakpoints.unhighlightBreakpoint();
window.dispatchEvent("Debugger:AfterFramesCleared");
},
/**
* 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 SF_selectFrame(aDepth) {
let frame = this.activeThread.cachedFrames[this.currentFrame = aDepth];
if (!frame) {
return;
}
let environment = frame.environment;
let { url, line } = frame.where;
// Check if the frame does not represent the evaluation of debuggee code.
if (!environment) {
return;
}
// Move the editor's caret to the proper url and line.
DebuggerView.updateEditor(url, line);
// Highlight the stack frame at the specified depth.
DebuggerView.StackFrames.highlightFrame(aDepth);
// Highlight the breakpoint at the specified url and line if it exists.
DebuggerView.Breakpoints.highlightBreakpoint(url, line);
// Start recording any added variables or properties in any scope.
DebuggerView.Variables.createHierarchy();
// Clear existing scopes and create each one dynamically.
DebuggerView.Variables.empty();
do {
// Create a scope to contain all the inspected variables.
let label = this._getScopeLabel(environment);
let scope = DebuggerView.Variables.addScope(label);
// Special additions to the innermost scope.
if (environment == frame.environment) {
this._insertScopeFrameReferences(scope, frame);
this._fetchScopeVariables(scope, environment);
// Always expand the innermost scope by default.
scope.expand();
}
// Lazily add nodes for every other environment scope.
else {
this._addScopeExpander(scope, environment);
this.autoScopeExpand && scope.expand();
}
} while (environment = environment.parent);
// Signal that variables have been fetched.
window.dispatchEvent("Debugger:FetchedVariables");
DebuggerView.Variables.commitHierarchy();
},
/**
* Adds an 'onexpand' callback for a scope, lazily handling
* the addition of new variables.
*
* @param Scope aScope
* The scope where the variables will be placed into.
* @param object aEnv
* The scope's environment.
*/
_addScopeExpander: function SF__addScopeExpander(aScope, aEnv) {
let callback = this._fetchScopeVariables.bind(this, aScope, aEnv);
// It's a good idea to be prepared in case of an expansion.
aScope.onmouseover = callback;
// Make sure that variables are always available on expansion.
aScope.onexpand = callback;
},
/**
* Adds an 'onexpand' callback for a variable, lazily handling
* the addition of new properties.
*
* @param Variable aVar
* The variable where the properties will be placed into.
* @param any aGrip
* The grip of the variable.
*/
_addVarExpander: function SF__addVarExpander(aVar, aGrip) {
// No need for expansion for primitive values.
if (VariablesView.isPrimitive({ value: aGrip })) {
return;
}
let callback = this._fetchVarProperties.bind(this, aVar, aGrip);
// Some variables are likely to contain a very large number of properties.
// It's a good idea to be prepared in case of an expansion.
if (aVar.name == "window" || aVar.name == "this") {
aVar.onmouseover = callback;
}
// Make sure that properties are always available on expansion.
aVar.onexpand = callback;
},
/**
* Adds variables to a scope in the view. Triggered when a scope is
* expanded or is hovered. It does not expand the scope.
*
* @param Scope aScope
* The scope where the variables will be placed into.
* @param object aEnv
* The scope's environment.
*/
_fetchScopeVariables: function SF__fetchScopeVariables(aScope, aEnv) {
// Retrieve the variables only once.
if (aScope.fetched) {
return;
}
aScope.fetched = true;
switch (aEnv.type) {
case "with":
case "object":
// Add nodes for every variable in scope.
this.activeThread.pauseGrip(aEnv.object).getPrototypeAndProperties(function(aResponse) {
this._insertScopeVariables(aResponse.ownProperties, aScope);
// Signal that variables have been fetched.
window.dispatchEvent("Debugger:FetchedVariables");
DebuggerView.Variables.commitHierarchy();
}.bind(this));
break;
case "block":
case "function":
// Add nodes for every argument and every other variable in scope.
this._insertScopeArguments(aEnv.bindings.arguments, aScope);
this._insertScopeVariables(aEnv.bindings.variables, aScope);
break;
default:
Cu.reportError("Unknown Debugger.Environment type: " + aEnv.type);
break;
}
},
/**
* 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 SF__insertScopeFrameReferences(aScope, aFrame) {
// Add any thrown exception.
if (this.currentException) {
let excRef = aScope.addVar("<exception>", { value: this.currentException });
this._addVarExpander(excRef, this.currentException);
}
// Add "this".
if (aFrame.this) {
let thisRef = aScope.addVar("this", { value: aFrame.this });
this._addVarExpander(thisRef, aFrame.this);
}
},
/**
* Add nodes for every argument in scope.
*
* @param object aArguments
* The map of names to arguments, as specified in the protocol.
* @param Scope aScope
* The scope where the nodes will be placed into.
*/
_insertScopeArguments: function SF__insertScopeArguments(aArguments, aScope) {
if (!aArguments) {
return;
}
for (let argument of aArguments) {
let name = Object.getOwnPropertyNames(argument)[0];
let argRef = aScope.addVar(name, argument[name]);
let argVal = argument[name].value;
this._addVarExpander(argRef, argVal);
}
},
/**
* Add nodes for every variable in scope.
*
* @param object aVariables
* The map of names to variables, as specified in the protocol.
* @param Scope aScope
* The scope where the nodes will be placed into.
*/
_insertScopeVariables: function SF__insertScopeVariables(aVariables, aScope) {
if (!aVariables) {
return;
}
let variableNames = Object.keys(aVariables);
// Sort all of the variables before adding them if preferred.
if (Prefs.variablesSortingEnabled) {
variableNames.sort();
}
// Add the sorted variables to the specified scope.
for (let name of variableNames) {
let varRef = aScope.addVar(name, aVariables[name]);
let varVal = aVariables[name].value;
this._addVarExpander(varRef, varVal);
}
},
/**
* Adds properties to a variable in the view. Triggered when a variable is
* expanded or certain variables are hovered. It does not expand the variable.
*
* @param Variable aVar
* The variable where the properties will be placed into.
* @param any aGrip
* The grip of the variable.
*/
_fetchVarProperties: function SF__fetchVarProperties(aVar, aGrip) {
// Retrieve the properties only once.
if (aVar.fetched) {
return;
}
aVar.fetched = true;
this.activeThread.pauseGrip(aGrip).getPrototypeAndProperties(function(aResponse) {
let { ownProperties, prototype } = aResponse;
// Add all the variable properties.
if (ownProperties) {
aVar.addProperties(ownProperties);
// Expansion handlers must be set after the properties are added.
for (let name in ownProperties) {
this._addVarExpander(aVar.get(name), ownProperties[name].value);
}
}
// Add the variable's __proto__.
if (prototype.type != "null") {
aVar.addProperty("__proto__", { value: prototype });
// Expansion handlers must be set after the properties are added.
this._addVarExpander(aVar.get("__proto__"), prototype);
}
aVar._retrieved = true;
// Signal that properties have been fetched.
window.dispatchEvent("Debugger:FetchedProperties");
DebuggerView.Variables.commitHierarchy();
}.bind(this));
},
/**
* Constructs a scope label based on its environment.
*
* @param object aEnv
* The scope's environment.
* @return string
* The scope's label.
*/
_getScopeLabel: function SV__getScopeLabel(aEnv) {
let name = "";
// Name the outermost scope Global.
if (!aEnv.parent) {
name = L10N.getStr("globalScopeLabel");
}
// Otherwise construct the scope name.
else {
name = aEnv.type.charAt(0).toUpperCase() + aEnv.type.slice(1);
}
let label = L10N.getFormatStr("scopeLabel", [name]);
switch (aEnv.type) {
case "with":
case "object":
label += " [" + aEnv.object.class + "]";
break;
case "function":
label += " [" + aEnv.functionName + "]";
break;
}
return label;
},
/**
* Adds the specified stack frame to the list.
*
* @param object aFrame
* The new frame to add.
*/
_addFrame: function SF__addFrame(aFrame) {
let depth = aFrame.depth;
let { url, line } = aFrame.where;
let startText = StackFrameUtils.getFrameTitle(aFrame);
let endText = SourceUtils.getSourceLabel(url) + ":" + line;
DebuggerView.StackFrames.addFrame(startText, endText, depth, {
attachment: aFrame
});
},
/**
* Loads more stack frames from the debugger server cache.
*/
addMoreFrames: function SF_addMoreFrames() {
this.activeThread.fillFrames(
this.activeThread.cachedFrames.length + CALL_STACK_PAGE_SIZE);
},
/**
* Evaluate an expression in the context of the selected frame. This is used
* for modifying the value of variables or properties in scope.
*
* @param string aExpression
* The expression to evaluate.
*/
evaluate: function SF_evaluate(aExpression) {
let frame = this.activeThread.cachedFrames[this.currentFrame];
this.activeThread.eval(frame.actor, aExpression);
}
};
/**
* Keeps the source script list up-to-date, using the thread client's
* source script cache.
*
* FIXME: Currently, "sources" are actually "scripts", this should change in
* Bug 795368 - Add "sources" and "newSource" packets to the RDP, and use them
* instead of "scripts" and "newScript".
*/
function SourceScripts() {
this._onNewScript = this._onNewScript.bind(this);
this._onNewGlobal = this._onNewGlobal.bind(this);
this._onScriptsAdded = this._onScriptsAdded.bind(this);
}
SourceScripts.prototype = {
get activeThread() DebuggerController.activeThread,
get debuggerClient() DebuggerController.client,
/**
* Connect to the current thread client.
*/
connect: function SS_connect() {
dumpn("SourceScripts is connecting...");
this.debuggerClient.addListener("newScript", this._onNewScript);
this.debuggerClient.addListener("newGlobal", this._onNewGlobal);
this._handleTabNavigation();
},
/**
* Disconnect from the client.
*/
disconnect: function SS_disconnect() {
if (!this.activeThread) {
return;
}
dumpn("SourceScripts is disconnecting...");
this.debuggerClient.removeListener("newScript", this._onNewScript);
this.debuggerClient.removeListener("newGlobal", this._onNewGlobal);
},
/**
* Handles any initialization on a tab navigation event issued by the client.
*/
_handleTabNavigation: function SS__handleTabNavigation() {
if (!this.activeThread) {
return;
}
dumpn("Handling tab navigation in the SourceScripts");
// Retrieve the list of script sources known to the server from before
// the client was ready to handle "newScript" notifications.
this.activeThread.getScripts(this._onScriptsAdded);
},
/**
* Handler for the debugger client's unsolicited newScript notification.
*/
_onNewScript: function SS__onNewScript(aNotification, aPacket) {
// Ignore scripts generated from 'clientEvaluate' packets.
if (aPacket.url == "debugger eval code") {
return;
}
// Add the source in the debugger view sources container.
this._addSource({
url: aPacket.url,
startLine: aPacket.startLine,
source: aPacket.source
}, {
forced: true
});
let container = DebuggerView.Sources;
let preferredValue = container.preferredValue;
// Select this source if it's the preferred one.
if (aPacket.url == preferredValue) {
container.selectedValue = preferredValue;
}
// ..or the first entry if there's none selected yet after a while
else {
window.setTimeout(function() {
// If after a certain delay the preferred source still wasn't received,
// just give up on waiting and display the first entry.
if (!container.selectedValue) {
container.selectedIndex = 0;
}
}, NEW_SCRIPT_DISPLAY_DELAY);
}
// If there are any stored breakpoints for this source, display them again,
// both in the editor and the breakpoints pane.
DebuggerController.Breakpoints.updateEditorBreakpoints();
DebuggerController.Breakpoints.updatePaneBreakpoints();
// Signal that a new script has been added.
window.dispatchEvent("Debugger:AfterNewScript");
},
/**
* Handler for the debugger client's unsolicited newGlobal notification.
*/
_onNewGlobal: function SS__onNewGlobal(aNotification, aPacket) {
// TODO: bug 806775, update the globals list using aPacket.hostAnnotations
// from bug 801084.
},
/**
* Callback for the debugger's active thread getScripts() method.
*/
_onScriptsAdded: function SS__onScriptsAdded(aResponse) {
// Add all the sources in the debugger view sources container.
for (let script of aResponse.scripts) {
this._addSource(script);
}
let container = DebuggerView.Sources;
let preferredValue = container.preferredValue;
// Flushes all the prepared sources into the sources container.
container.commit();
// Select the preferred source if it exists and was part of the response.
if (container.containsValue(preferredValue)) {
container.selectedValue = preferredValue;
}
// ..or the first entry if there's no one selected yet.
else if (!container.selectedValue) {
container.selectedIndex = 0;
}
// If there are any stored breakpoints for the sources, display them again,
// both in the editor and the breakpoints pane.
DebuggerController.Breakpoints.updateEditorBreakpoints();
DebuggerController.Breakpoints.updatePaneBreakpoints();
// Signal that scripts have been added.
window.dispatchEvent("Debugger:AfterScriptsAdded");
},
/**
* Add the specified source to the debugger view sources list.
*
* @param object aScript
* The source object coming from the active thread.
* @param object aOptions [optional]
* Additional options for adding the source. Supported options:
* - forced: force the source to be immediately added
*/
_addSource: function SS__addSource(aSource, aOptions = {}) {
let url = aSource.url;
let label = SourceUtils.getSourceLabel(url);
DebuggerView.Sources.push(label, url, {
forced: aOptions.forced,
attachment: aSource
});
},
/**
* Gets a specified source's text.
*
* @param object aSource
* The source object coming from the active thread.
* @param function aCallback
* Function called after the source text has been loaded.
*/
getText: function SS_getText(aSource, aCallback) {
// If already loaded, return the source text immediately.
if (aSource.loaded) {
aCallback(aSource.url, aSource.text);
return;
}
// Get the source text from the active thread.
this.activeThread.source(aSource.source).source(function(aResponse) {
if (aResponse.error) {
Cu.reportError("Error loading " + aUrl);
return;
}
aCallback(aSource.url, aResponse.source);
});
}
};
/**
* Handles all the breakpoints in the current debugger.
*/
function Breakpoints() {
this._onEditorBreakpointChange = this._onEditorBreakpointChange.bind(this);
this._onEditorBreakpointAdd = this._onEditorBreakpointAdd.bind(this);
this._onEditorBreakpointRemove = this._onEditorBreakpointRemove.bind(this);
this.addBreakpoint = this.addBreakpoint.bind(this);
this.removeBreakpoint = this.removeBreakpoint.bind(this);
this.getBreakpoint = this.getBreakpoint.bind(this);
}
Breakpoints.prototype = {
get activeThread() DebuggerController.ThreadState.activeThread,
get editor() DebuggerView.editor,
/**
* The list of breakpoints in the debugger as tracked by the current
* debugger instance. This is an object where the values are BreakpointActor
* objects received from the client, while the keys are actor names, for
* example "conn0.breakpoint3".
*/
store: {},
/**
* Skip editor breakpoint change events.
*
* This property tells the source editor event handler to skip handling of
* the BREAKPOINT_CHANGE events. This is used when the debugger adds/removes
* breakpoints from the editor. Typically, the BREAKPOINT_CHANGE event handler
* adds/removes events from the debugger, but when breakpoints are added from
* the public debugger API, we need to do things in reverse.
*
* This implementation relies on the fact that the source editor fires the
* BREAKPOINT_CHANGE events synchronously.
*/
_skipEditorBreakpointCallbacks: false,
/**
* Adds the source editor breakpoint handlers.
*/
initialize: function BP_initialize() {
this.editor.addEventListener(
SourceEditor.EVENTS.BREAKPOINT_CHANGE, this._onEditorBreakpointChange);
},
/**
* Removes the source editor breakpoint handlers & all the added breakpoints.
*/
destroy: function BP_destroy() {
this.editor.removeEventListener(
SourceEditor.EVENTS.BREAKPOINT_CHANGE, this._onEditorBreakpointChange);
for each (let breakpointClient in this.store) {
this.removeBreakpoint(breakpointClient);
}
},
/**
* Event handler for breakpoint changes that happen in the editor. This
* function syncs the breakpoints in the editor to those in the debugger.
*
* @param object aEvent
* The SourceEditor.EVENTS.BREAKPOINT_CHANGE event object.
*/
_onEditorBreakpointChange: function BP__onEditorBreakpointChange(aEvent) {
if (this._skipEditorBreakpointCallbacks) {
return;
}
this._skipEditorBreakpointCallbacks = true;
aEvent.added.forEach(this._onEditorBreakpointAdd, this);
aEvent.removed.forEach(this._onEditorBreakpointRemove, this);
this._skipEditorBreakpointCallbacks = false;
},
/**
* Event handler for new breakpoints that come from the editor.
*
* @param object aEditorBreakpoint
* The breakpoint object coming from the editor.
*/
_onEditorBreakpointAdd: function BP__onEditorBreakpointAdd(aEditorBreakpoint) {
let url = DebuggerView.Sources.selectedValue;
let line = aEditorBreakpoint.line + 1;
this.addBreakpoint({ url: url, line: line }, function(aBreakpointClient) {
// If the breakpoint client has an "actualLocation" attached, then
// the original requested placement for the breakpoint wasn't accepted.
// In this case, we need to update the editor with the new location.
if (aBreakpointClient.actualLocation) {
this.editor.removeBreakpoint(line - 1);
this.editor.addBreakpoint(aBreakpointClient.actualLocation.line - 1);
}
}.bind(this));
},
/**
* Event handler for breakpoints that are removed from the editor.
*
* @param object aEditorBreakpoint
* The breakpoint object that was removed from the editor.
*/
_onEditorBreakpointRemove: function BP__onEditorBreakpointRemove(aEditorBreakpoint) {
let url = DebuggerView.Sources.selectedValue;
let line = aEditorBreakpoint.line + 1;
this.removeBreakpoint(this.getBreakpoint(url, line));
},
/**
* Update the breakpoints in the editor view. This function takes the list of
* breakpoints in the debugger and adds them back into the editor view.
* This is invoked when the selected script is changed.
*/
updateEditorBreakpoints: function BP_updateEditorBreakpoints() {
for each (let breakpointClient in this.store) {
if (DebuggerView.Sources.selectedValue == breakpointClient.location.url) {
this._showBreakpoint(breakpointClient, { noPaneUpdate: true });
}
}
},
/**
* Update the breakpoints in the pane view. This function takes the list of
* breakpoints in the debugger and adds them back into the breakpoints pane.
* This is invoked when scripts are added.
*/
updatePaneBreakpoints: function BP_updatePaneBreakpoints() {
for each (let breakpointClient in this.store) {
if (DebuggerView.Sources.containsValue(breakpointClient.location.url)) {
this._showBreakpoint(breakpointClient, { noEditorUpdate: true });
}
}
},
/**
* Add a breakpoint.
*
* @param object aLocation
* The location where you want the breakpoint. This object must have
* two properties:
* - url: the url of the source.
* - line: the line number (starting from 1).
* @param function aCallback [optional]
* Optional function to invoke once the breakpoint is added. The
* callback is invoked with two arguments:
* - aBreakpointClient: the BreakpointActor client object
* - aResponseError: if there was any error
* @param object aFlags [optional]
* An object containing some of the following boolean properties:
* - noEditorUpdate: tells if you want to skip editor updates
* - noPaneUpdate: tells if you want to skip breakpoint pane updates
*/
addBreakpoint:
function BP_addBreakpoint(aLocation, aCallback, aFlags = {}) {
let breakpointClient = this.getBreakpoint(aLocation.url, aLocation.line);
// If the breakpoint was already added, callback immediately.
if (breakpointClient) {
aCallback && aCallback(breakpointClient);
return;
}
this.activeThread.setBreakpoint(aLocation, function(aResponse, aBreakpointClient) {
let { url, line } = aResponse.actualLocation || aLocation;
// Prevent this new breakpoint from being repositioned on top of an
// already existing one.
if (this.getBreakpoint(url, line)) {
this._hideBreakpoint(aBreakpointClient);
aBreakpointClient.remove();
return;
}
// If the breakpoint response has an "actualLocation" attached, then
// the original requested placement for the breakpoint wasn't accepted.
if (aResponse.actualLocation) {
// Store the originally requested location in case it's ever needed.
aBreakpointClient.requestedLocation = {
url: aBreakpointClient.location.url,
line: aBreakpointClient.location.line
};
// Store the response actual location to be used.
aBreakpointClient.actualLocation = aResponse.actualLocation;
// Update the breakpoint client with the actual location.
aBreakpointClient.location.url = aResponse.actualLocation.url;
aBreakpointClient.location.line = aResponse.actualLocation.line;
}
// Remember the breakpoint client in the store.
this.store[aBreakpointClient.actor] = aBreakpointClient;
// Preserve some information about the breakpoint's source url and line
// to display in the breakpoints pane.
aBreakpointClient.lineText = DebuggerView.getEditorLine(line - 1);
aBreakpointClient.lineInfo = SourceUtils.getSourceLabel(url) + ":" + line;
// Show the breakpoint in the editor and breakpoints pane.
this._showBreakpoint(aBreakpointClient, aFlags);
// We're done here.
aCallback && aCallback(aBreakpointClient, aResponse.error);
}.bind(this));
},
/**
* Remove a breakpoint.
*
* @param object aBreakpointClient
* The BreakpointActor client object to remove.
* @param function aCallback [optional]
* Optional function to invoke once the breakpoint is removed. The
* callback is invoked with one argument
* - aBreakpointClient: the breakpoint location (url and line)
* @param object aFlags [optional]
* An object containing some of the following boolean properties:
* - noEditorUpdate: tells if you want to skip editor updates
* - noPaneUpdate: tells if you want to skip breakpoint pane updates
*/
removeBreakpoint:
function BP_removeBreakpoint(aBreakpointClient, aCallback, aFlags = {}) {
let breakpointActor = (aBreakpointClient || {}).actor;
// If the breakpoint was already removed, callback immediately.
if (!this.store[breakpointActor]) {
aCallback && aCallback(aBreakpointClient.location);
return;
}
aBreakpointClient.remove(function() {
// Delete the breakpoint client from the store.
delete this.store[breakpointActor];
// Hide the breakpoint from the editor and breakpoints pane.
this._hideBreakpoint(aBreakpointClient, aFlags);
// We're done here.
aCallback && aCallback(aBreakpointClient.location);
}.bind(this));
},
/**
* Update the editor and breakpoints pane to show a specified breakpoint.
*
* @param object aBreakpointClient
* The BreakpointActor client object to show.
* @param object aFlags [optional]
* An object containing some of the following boolean properties:
* - noEditorUpdate: tells if you want to skip editor updates
* - noPaneUpdate: tells if you want to skip breakpoint pane updates
*/
_showBreakpoint: function BP__showBreakpoint(aBreakpointClient, aFlags = {}) {
let currentSourceUrl = DebuggerView.Sources.selectedValue;
let { url, line } = aBreakpointClient.location;
if (!aFlags.noEditorUpdate) {
if (url == currentSourceUrl) {
this._skipEditorBreakpointCallbacks = true;
this.editor.addBreakpoint(line - 1);
this._skipEditorBreakpointCallbacks = false;
}
}
if (!aFlags.noPaneUpdate) {
let { lineText, lineInfo } = aBreakpointClient;
let actor = aBreakpointClient.actor;
DebuggerView.Breakpoints.addBreakpoint(lineInfo, lineText, url, line, actor);
}
},
/**
* Update the editor and breakpoints pane to hide a specified breakpoint.
*
* @param object aBreakpointClient
* The BreakpointActor client object to hide.
* @param object aFlags [optional]
* An object containing some of the following boolean properties:
* - noEditorUpdate: tells if you want to skip editor updates
* - noPaneUpdate: tells if you want to skip breakpoint pane updates
*/
_hideBreakpoint: function BP__hideBreakpoint(aBreakpointClient, aFlags = {}) {
let currentSourceUrl = DebuggerView.Sources.selectedValue;
let { url, line } = aBreakpointClient.location;
if (!aFlags.noEditorUpdate) {
if (url == currentSourceUrl) {
this._skipEditorBreakpointCallbacks = true;
this.editor.removeBreakpoint(line - 1);
this._skipEditorBreakpointCallbacks = false;
}
}
if (!aFlags.noPaneUpdate) {
DebuggerView.Breakpoints.removeBreakpoint(url, line);
}
},
/**
* Get the breakpoint object at the given location.
*
* @param string aUrl
* The URL of where the breakpoint is.
* @param number aLine
* The line number where the breakpoint is.
* @return object
* The BreakpointActor object.
*/
getBreakpoint: function BP_getBreakpoint(aUrl, aLine) {
for each (let breakpointClient in this.store) {
if (breakpointClient.location.url == aUrl &&
breakpointClient.location.line == aLine) {
return breakpointClient;
}
}
return null;
}
};
/**
* Localization convenience methods.
*/
let L10N = {
/**
* L10N shortcut function.
*
* @param string aName
* @return string
*/
getStr: function L10N_getStr(aName) {
return this.stringBundle.GetStringFromName(aName);
},
/**
* L10N shortcut function.
*
* @param string aName
* @param array aArray
* @return string
*/
getFormatStr: function L10N_getFormatStr(aName, aArray) {
return this.stringBundle.formatStringFromName(aName, aArray, aArray.length);
}
};
XPCOMUtils.defineLazyGetter(L10N, "stringBundle", function() {
return Services.strings.createBundle(DBG_STRINGS_URI);
});
XPCOMUtils.defineLazyGetter(L10N, "ellipsis", function() {
return Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
});
/**
* Shortcuts for accessing various debugger preferences.
*/
let Prefs = {
/**
* Helper method for getting a pref value.
*
* @param string aType
* @param string aPrefName
* @return any
*/
_get: function P__get(aType, aPrefName) {
if (this[aPrefName] === undefined) {
this[aPrefName] = Services.prefs["get" + aType + "Pref"](aPrefName);
}
return this[aPrefName];
},
/**
* Helper method for setting a pref value.
*
* @param string aType
* @param string aPrefName
* @param any aValue
*/
_set: function P__set(aType, aPrefName, aValue) {
Services.prefs["set" + aType + "Pref"](aPrefName, aValue);
this[aPrefName] = aValue;
},
/**
* Maps a property name to a pref, defining lazy getters and setters.
*
* @param string aType
* @param string aPropertyName
* @param string aPrefName
*/
map: function P_map(aType, aPropertyName, aPrefName) {
Object.defineProperty(this, aPropertyName, {
get: function() this._get(aType, aPrefName),
set: function(aValue) this._set(aType, aPrefName, aValue)
});
}
};
Prefs.map("Int", "height", "devtools.debugger.ui.height");
Prefs.map("Int", "windowX", "devtools.debugger.ui.win-x");
Prefs.map("Int", "windowY", "devtools.debugger.ui.win-y");
Prefs.map("Int", "windowWidth", "devtools.debugger.ui.win-width");
Prefs.map("Int", "windowHeight", "devtools.debugger.ui.win-height");
Prefs.map("Int", "stackframesWidth", "devtools.debugger.ui.stackframes-width");
Prefs.map("Int", "variablesWidth", "devtools.debugger.ui.variables-width");
Prefs.map("Bool", "panesVisibleOnStartup", "devtools.debugger.ui.panes-visible-on-startup");
Prefs.map("Bool", "variablesSortingEnabled", "devtools.debugger.ui.variables-sorting-enabled");
Prefs.map("Bool", "variablesNonEnumVisible", "devtools.debugger.ui.variables-non-enum-visible");
Prefs.map("Bool", "variablesSearchboxVisible", "devtools.debugger.ui.variables-searchbox-visible");
Prefs.map("Char", "remoteHost", "devtools.debugger.remote-host");
Prefs.map("Int", "remotePort", "devtools.debugger.remote-port");
Prefs.map("Bool", "remoteAutoConnect", "devtools.debugger.remote-autoconnect");
Prefs.map("Int", "remoteConnectionRetries", "devtools.debugger.remote-connection-retries");
Prefs.map("Int", "remoteTimeout", "devtools.debugger.remote-timeout");
/**
* Returns true if this is a remote debugger instance.
* @return boolean
*/
XPCOMUtils.defineLazyGetter(window, "_isRemoteDebugger", function() {
// We're inside a single top level XUL window, not an iframe container.
return !(window.frameElement instanceof XULElement) &&
!!window._remoteFlag;
});
/**
* Returns true if this is a chrome debugger instance.
* @return boolean
*/
XPCOMUtils.defineLazyGetter(window, "_isChromeDebugger", function() {
// We're inside a single top level XUL window, but not a remote debugger.
return !(window.frameElement instanceof XULElement) &&
!window._remoteFlag;
});
/**
* Preliminary setup for the DebuggerController object.
*/
DebuggerController.initialize();
DebuggerController.ThreadState = new ThreadState();
DebuggerController.StackFrames = new StackFrames();
DebuggerController.SourceScripts = new SourceScripts();
DebuggerController.Breakpoints = new Breakpoints();
/**
* Export some properties to the global scope for easier access.
*/
Object.defineProperties(window, {
"gClient": {
get: function() DebuggerController.client
},
"gTabClient": {
get: function() DebuggerController.tabClient
},
"gThreadClient": {
get: function() DebuggerController.activeThread
},
"gThreadState": {
get: function() DebuggerController.ThreadState
},
"gStackFrames": {
get: function() DebuggerController.StackFrames
},
"gSourceScripts": {
get: function() DebuggerController.SourceScripts
},
"gBreakpoints": {
get: function() DebuggerController.Breakpoints
},
"gCallStackPageSize": {
get: function() CALL_STACK_PAGE_SIZE,
},
"dispatchEvent": {
get: function() DebuggerController.dispatchEvent,
},
"editor": {
get: function() DebuggerView.editor
}
});
/**
* Helper method for debugging.
* @param string
*/
function dumpn(str) {
if (wantLogging) {
dump("DBG-FRONTEND: " + str + "\n");
}
}
let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");