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

1662 lines
50 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 FRAME_STEP_CACHE_DURATION = 100; // ms
const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties";
const SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 1048576; // 1 MB in bytes
Cu.import("resource:///modules/source-editor.jsm");
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import('resource://gre/modules/Services.jsm');
/**
* Controls the debugger view by handling the source scripts, the current
* thread state and thread stack frame cache.
*/
let DebuggerController = {
/**
* Makes a few preliminary changes and bindings to the controller.
*/
init: function() {
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("DOMContentLoaded", this._startupDebugger, true);
window.addEventListener("unload", this._shutdownDebugger, true);
},
/**
* Initializes the debugger view and connects a debugger client to the server.
*/
_startupDebugger: function DC__startupDebugger() {
if (this._isInitialized) {
return;
}
this._isInitialized = true;
window.removeEventListener("DOMContentLoaded", this._startupDebugger, true);
DebuggerView.initializePanes();
DebuggerView.initializeEditor();
DebuggerView.StackFrames.initialize();
DebuggerView.Properties.initialize();
DebuggerView.Scripts.initialize();
DebuggerView.showCloseButton(!this._isRemoteDebugger && !this._isChromeDebugger);
this.dispatchEvent("Debugger:Loaded");
this._connect();
},
/**
* Destroys the debugger view, disconnects the debugger client and cleans up
* any active listeners.
*/
_shutdownDebugger: function DC__shutdownDebugger() {
if (this._isDestroyed) {
return;
}
this._isDestroyed = true;
window.removeEventListener("unload", this._shutdownDebugger, true);
DebuggerView.destroyPanes();
DebuggerView.destroyEditor();
DebuggerView.Scripts.destroy();
DebuggerView.StackFrames.destroy();
DebuggerView.Properties.destroy();
DebuggerController.Breakpoints.destroy();
DebuggerController.SourceScripts.disconnect();
DebuggerController.StackFrames.disconnect();
DebuggerController.ThreadState.disconnect();
this.dispatchEvent("Debugger:Unloaded");
this._disconnect();
this._isChromeDebugger && this._quitApp();
},
/**
* 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
*/
_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"));
this.dispatchEvent("Debugger:Close");
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 && !DebuggerController.activeThread) {
this.dispatchEvent("Debugger:Close");
return false;
}
Prefs.remoteHost = prompt.uri.host;
Prefs.remotePort = prompt.uri.port;
}
// 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 (!DebuggerController.activeThread) {
DebuggerController._onRemoteConnectionTimeout();
DebuggerController._connect();
}
}, Prefs.remoteTimeout);
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 (this._isRemoteDebugger) {
if (!this._prepareConnection()) {
return;
}
}
let transport = (this._isChromeDebugger || this._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) {
let tab = aResponse.tabs[aResponse.selected];
this._startDebuggingTab(client, tab);
this.dispatchEvent("Debugger:Connecting");
}.bind(this));
}.bind(this));
},
/**
* Closes the debugger client and removes event handlers as necessary.
*/
_disconnect: function DC__disconnect() {
this.client.removeListener("tabNavigated", this._onTabNavigated);
this.client.removeListener("tabDetached", this._onTabDetached);
this.client.close();
this.client = null;
this.tabClient = null;
this.activeThread = null;
},
/**
* Starts debugging the current tab. This function is called on each location
* change in this tab.
*/
_onTabNavigated: function DC__onTabNavigated(aNotification, aPacket) {
let client = this.client;
client.activeThread.detach(function() {
client.activeTab.detach(function() {
client.listTabs(function(aResponse) {
let tab = aResponse.tabs[aResponse.selected];
this._startDebuggingTab(client, tab);
this.dispatchEvent("Debugger:Connecting");
}.bind(this));
}.bind(this));
}.bind(this));
},
/**
* Stops debugging the current tab.
*/
_onTabDetached: function DC__onTabDetached() {
this.dispatchEvent("Debugger:Close");
},
/**
* 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;
DebuggerController.ThreadState.connect(function() {
DebuggerController.StackFrames.connect(function() {
DebuggerController.SourceScripts.connect(function() {
aThreadClient.resume();
});
});
});
}.bind(this));
}.bind(this));
},
/**
* Returns true if this is a remote debugger instance.
* @return boolean
*/
get _isRemoteDebugger() {
return window._remoteFlag;
},
/**
* Returns true if this is a chrome debugger instance.
* @return boolean
*/
get _isChromeDebugger() {
return !window.parent.content && !this._isRemoteDebugger;
},
/**
* 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 string 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 = {
/**
* Gets the current thread the client has connected to.
*/
get activeThread() {
return DebuggerController.activeThread;
},
/**
* Connect to the current thread client.
*
* @param function aCallback
* The next function in the initialization sequence.
*/
connect: function TS_connect(aCallback) {
this.activeThread.addListener("paused", this._update);
this.activeThread.addListener("resumed", this._update);
this.activeThread.addListener("detached", this._update);
this._update();
aCallback && aCallback();
},
/**
* Disconnect from the client.
*/
disconnect: function TS_disconnect() {
if (!this.activeThread) {
return;
}
this.activeThread.removeListener("paused", this._update);
this.activeThread.removeListener("resumed", this._update);
this.activeThread.removeListener("detached", this._update);
},
/**
* Update the UI after a thread state change.
*/
_update: function TS__update(aEvent) {
DebuggerView.StackFrames.updateState(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._onResume = this._onResume.bind(this);
this._onFrames = this._onFrames.bind(this);
this._onFramesCleared = this._onFramesCleared.bind(this);
this._afterFramesCleared = this._afterFramesCleared.bind(this);
}
StackFrames.prototype = {
/**
* The maximum number of frames allowed to be loaded at a time.
*/
pageSize: 25,
/**
* The currently selected frame depth.
*/
selectedFrame: null,
/**
* A flag that defines whether the debuggee will pause whenever an exception
* is thrown.
*/
pauseOnExceptions: false,
/**
* Gets the current thread the client has connected to.
*/
get activeThread() {
return DebuggerController.activeThread;
},
/**
* Watch the given thread client.
*
* @param function aCallback
* The next function in the initialization sequence.
*/
connect: function SF_connect(aCallback) {
window.addEventListener("Debugger:FetchedVariables", this._onFetchedVars, false);
this._onFramesCleared();
this.activeThread.addListener("paused", this._onPaused);
this.activeThread.addListener("resumed", this._onResume);
this.activeThread.addListener("framesadded", this._onFrames);
this.activeThread.addListener("framescleared", this._onFramesCleared);
this.updatePauseOnExceptions(this.pauseOnExceptions);
aCallback && aCallback();
},
/**
* Disconnect from the client.
*/
disconnect: function SF_disconnect() {
window.removeEventListener("Debugger:FetchedVariables", this._onFetchedVars, false);
if (!this.activeThread) {
return;
}
this.activeThread.removeListener("paused", this._onPaused);
this.activeThread.removeListener("resumed", this._onResume);
this.activeThread.removeListener("framesadded", this._onFrames);
this.activeThread.removeListener("framescleared", this._onFramesCleared);
},
/**
* 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.exception = aPacket.why.exception;
}
this.activeThread.fillFrames(this.pageSize);
},
/**
* Handler for the thread client's resumed notification.
*/
_onResume: function SF__onResume() {
DebuggerView.editor.setDebugLocation(-1);
},
/**
* Handler for the thread client's framesadded notification.
*/
_onFrames: function SF__onFrames() {
if (!this.activeThread.cachedFrames.length) {
DebuggerView.StackFrames.emptyText();
DebuggerView.Properties.emptyText();
return;
}
DebuggerView.StackFrames.empty();
DebuggerView.Properties.empty();
for each (let frame in this.activeThread.cachedFrames) {
this._addFrame(frame);
}
if (!this.selectedFrame) {
this.selectFrame(0);
}
if (this.activeThread.moreFrames) {
DebuggerView.StackFrames.dirty = true;
}
},
/**
* Handler for the thread client's framescleared notification.
*/
_onFramesCleared: function SF__onFramesCleared() {
this.selectedFrame = null;
this.exception = 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_CACHE_DURATION);
},
/**
* Called soon after the thread client's framescleared notification.
*/
_afterFramesCleared: function SF__afterFramesCleared() {
if (!this.activeThread.cachedFrames.length) {
DebuggerView.StackFrames.emptyText();
DebuggerView.Properties.emptyText();
DebuggerController.dispatchEvent("Debugger:AfterFramesCleared");
}
},
/**
* Update the source editor's current debug location based on the selected
* frame and script.
*/
updateEditorLocation: function SF_updateEditorLocation() {
let frame = this.activeThread.cachedFrames[this.selectedFrame];
if (!frame) {
return;
}
let url = frame.where.url;
let line = frame.where.line;
let editor = DebuggerView.editor;
// Move the editor's caret to the proper line.
if (DebuggerView.Scripts.isSelected(url) && line) {
editor.setDebugLocation(line - 1);
} else {
editor.setDebugLocation(-1);
}
},
/**
* Inform the debugger client whether the debuggee should be paused whenever
* an exception is thrown.
*
* @param boolean aFlag
* The new value of the flag: true for pausing, false otherwise.
*/
updatePauseOnExceptions: function SF_updatePauseOnExceptions(aFlag) {
this.pauseOnExceptions = aFlag;
this.activeThread.pauseOnExceptions(this.pauseOnExceptions);
},
/**
* Marks the stack frame in 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) {
// Deselect any previously highlighted frame.
if (this.selectedFrame !== null) {
DebuggerView.StackFrames.unhighlightFrame(this.selectedFrame);
}
// Highlight the current frame.
this.selectedFrame = aDepth;
DebuggerView.StackFrames.highlightFrame(this.selectedFrame);
let frame = this.activeThread.cachedFrames[aDepth];
if (!frame) {
return;
}
let url = frame.where.url;
let line = frame.where.line;
let editor = DebuggerView.editor;
// Move the editor's caret to the proper line.
if (DebuggerView.Scripts.isSelected(url) && line) {
editor.setCaretPosition(line - 1);
editor.setDebugLocation(line - 1);
}
else if (DebuggerView.Scripts.contains(url)) {
DebuggerView.Scripts.selectScript(url);
editor.setCaretPosition(line - 1);
}
else {
editor.setDebugLocation(-1);
}
// Start recording any added variables or properties in any scope.
DebuggerView.Properties.createHierarchyStore();
// Clear existing scopes and create each one dynamically.
DebuggerView.Properties.empty();
if (frame.environment) {
let env = frame.environment;
do {
// Construct the scope name.
let name = env.type.charAt(0).toUpperCase() + env.type.slice(1);
// Call the outermost scope Global.
if (!env.parent) {
name = L10N.getStr("globalScopeLabel");
}
let label = L10N.getFormatStr("scopeLabel", [name]);
switch (env.type) {
case "with":
case "object":
label += " [" + env.object.class + "]";
break;
case "function":
if (env.functionName) {
label += " [" + env.functionName + "]";
}
break;
default:
break;
}
let scope = DebuggerView.Properties.addScope(label);
// Special additions to the innermost scope.
if (env == frame.environment) {
// Add any thrown exception.
if (aDepth == 0 && this.exception) {
let excVar = scope.addVar("<exception>");
if (typeof this.exception == "object") {
excVar.setGrip({
type: this.exception.type,
class: this.exception.class
});
this._addExpander(excVar, this.exception);
} else {
excVar.setGrip(this.exception);
}
}
// Add "this".
if (frame.this) {
let thisVar = scope.addVar("this");
thisVar.setGrip({
type: frame.this.type,
class: frame.this.class
});
this._addExpander(thisVar, frame.this);
}
// Expand the innermost scope by default.
scope.expand(true);
scope.addToHierarchy();
}
switch (env.type) {
case "with":
case "object":
let objClient = this.activeThread.pauseGrip(env.object);
objClient.getPrototypeAndProperties(function SF_getProps(aResponse) {
this._addScopeVariables(aResponse.ownProperties, scope);
// Signal that variables have been fetched.
DebuggerController.dispatchEvent("Debugger:FetchedVariables");
}.bind(this));
break;
case "block":
case "function":
// Add nodes for every argument.
let variables = env.bindings.arguments;
for each (let variable in variables) {
let name = Object.getOwnPropertyNames(variable)[0];
let paramVar = scope.addVar(name, variable[name]);
let paramVal = variable[name].value;
paramVar.setGrip(paramVal);
this._addExpander(paramVar, paramVal);
}
// Add nodes for every other variable in scope.
this._addScopeVariables(env.bindings.variables, scope);
break;
default:
Cu.reportError("Unknown Debugger.Environment type: " + env.type);
break;
}
} while (env = env.parent);
}
// Signal that variables have been fetched.
DebuggerController.dispatchEvent("Debugger:FetchedVariables");
},
/**
* Called afters variables have been fetched after a frame was selected.
*/
_onFetchedVars: function SF__onFetchedVars() {
DebuggerView.Properties.commitHierarchy();
},
/**
* Add nodes for every variable in scope.
*
* @param object aVariables
* The map of names to variables, as specified in the Remote
* Debugging Protocol.
* @param object aScope
* The scope where the nodes will be placed into.
*/
_addScopeVariables: function SF_addScopeVariables(aVariables, aScope) {
// Sort all of the variables before adding them, for better UX.
let variables = {};
for each (let prop in Object.keys(aVariables).sort()) {
variables[prop] = aVariables[prop];
}
// Add the sorted variables to the specified scope.
for (let variable in variables) {
let paramVar = aScope.addVar(variable, variables[variable]);
let paramVal = variables[variable].value;
paramVar.setGrip(paramVal);
this._addExpander(paramVar, paramVal);
}
},
/**
* Adds an 'onexpand' callback for a variable, lazily handling the addition of
* new properties.
*/
_addExpander: function SF__addExpander(aVar, aObject) {
// No need for expansion for null and undefined values.
if (!aVar || !aObject || typeof aObject !== "object" ||
aObject.type !== "object") {
return;
}
// Force the twisty to show up.
aVar.forceShowArrow();
aVar.onexpand = this._addVarProperties.bind(this, aVar, aObject);
},
/**
* Adds properties to a variable in the view. Triggered when a variable is
* expanded.
*/
_addVarProperties: function SF__addVarProperties(aVar, aObject) {
// Retrieve the properties only once.
if (aVar.fetched) {
return;
}
let objClient = this.activeThread.pauseGrip(aObject);
objClient.getPrototypeAndProperties(function SF_onProtoAndProps(aResponse) {
// Sort all of the properties before adding them, for better UX.
let properties = {};
for each (let prop in Object.keys(aResponse.ownProperties).sort()) {
properties[prop] = aResponse.ownProperties[prop];
}
aVar.addProperties(properties);
// Expansion handlers must be set after the properties are added.
for (let prop in aResponse.ownProperties) {
this._addExpander(aVar[prop], aResponse.ownProperties[prop].value);
}
// Add __proto__.
if (aResponse.prototype.type !== "null") {
let properties = { "__proto__ ": { value: aResponse.prototype } };
aVar.addProperties(properties);
// Expansion handlers must be set after the properties are added.
this._addExpander(aVar["__proto__ "], aResponse.prototype);
}
aVar.fetched = true;
}.bind(this));
},
/**
* Adds the specified stack frame to the list.
*
* @param Debugger.Frame aFrame
* The new frame to add.
*/
_addFrame: function SF__addFrame(aFrame) {
let depth = aFrame.depth;
let label = DebuggerController.SourceScripts._getScriptLabel(aFrame.where.url);
let startText = this._getFrameTitle(aFrame);
let endText = label + ":" + aFrame.where.line;
let frame = DebuggerView.StackFrames.addFrame(depth, startText, endText);
if (frame) {
frame.debuggerFrame = aFrame;
}
},
/**
* Loads more stack frames from the debugger server cache.
*/
addMoreFrames: function SF_addMoreFrames() {
this.activeThread.fillFrames(
this.activeThread.cachedFrames.length + this.pageSize);
},
/**
* Create a textual representation for the stack frame specified, for
* displaying in the stack frame list.
*
* @param Debugger.Frame aFrame
* The stack frame to label.
*/
_getFrameTitle: function SF__getFrameTitle(aFrame) {
if (aFrame.type == "call") {
return aFrame["calleeName"] ? aFrame["calleeName"] : "(anonymous)";
}
return "(" + aFrame.type + ")";
},
/**
* Evaluate an expression in the context of the selected frame. This is used
* for modifying the value of variables in scope.
*
* @param string aExpression
* The expression to evaluate.
*/
evaluate: function SF_evaluate(aExpression) {
let frame = this.activeThread.cachedFrames[this.selectedFrame];
this.activeThread.eval(frame.actor, aExpression);
}
};
/**
* Keeps the source script list up-to-date, using the thread client's
* source script cache.
*/
function SourceScripts() {
this._onNewScript = this._onNewScript.bind(this);
this._onScriptsAdded = this._onScriptsAdded.bind(this);
this._onScriptsCleared = this._onScriptsCleared.bind(this);
this._onShowScript = this._onShowScript.bind(this);
this._onLoadSource = this._onLoadSource.bind(this);
this._onLoadSourceFinished = this._onLoadSourceFinished.bind(this);
}
SourceScripts.prototype = {
/**
* A cache containing simplified labels from script urls.
*/
_labelsCache: {},
/**
* Gets the current thread the client has connected to.
*/
get activeThread() {
return DebuggerController.activeThread;
},
/**
* Gets the current debugger client.
*/
get debuggerClient() {
return DebuggerController.client;
},
/**
* Watch the given thread client.
*
* @param function aCallback
* The next function in the initialization sequence.
*/
connect: function SS_connect(aCallback) {
window.addEventListener("Debugger:LoadSource", this._onLoadSource, false);
this.debuggerClient.addListener("newScript", this._onNewScript);
this.activeThread.addListener("scriptsadded", this._onScriptsAdded);
this.activeThread.addListener("scriptscleared", this._onScriptsCleared);
this._clearLabelsCache();
this._onScriptsCleared();
// Retrieve the list of scripts known to the server from before the client
// was ready to handle new script notifications.
this.activeThread.fillScripts();
aCallback && aCallback();
},
/**
* Disconnect from the client.
*/
disconnect: function TS_disconnect() {
window.removeEventListener("Debugger:LoadSource", this._onLoadSource, false);
if (!this.activeThread) {
return;
}
this.debuggerClient.removeListener("newScript", this._onNewScript);
this.activeThread.removeListener("scriptsadded", this._onScriptsAdded);
this.activeThread.removeListener("scriptscleared", this._onScriptsCleared);
},
/**
* 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;
}
this._addScript({ url: aPacket.url, startLine: aPacket.startLine }, true);
// If there are any stored breakpoints for this script, display them again.
for each (let bp in DebuggerController.Breakpoints.store) {
if (bp.location.url == aPacket.url) {
DebuggerController.Breakpoints.displayBreakpoint(bp.location);
}
}
},
/**
* Handler for the thread client's scriptsadded notification.
*/
_onScriptsAdded: function SS__onScriptsAdded() {
for each (let script in this.activeThread.cachedScripts) {
this._addScript(script, false);
}
DebuggerView.Scripts.commitScripts();
},
/**
* Handler for the thread client's scriptscleared notification.
*/
_onScriptsCleared: function SS__onScriptsCleared() {
DebuggerView.Scripts.empty();
},
/**
* Sets the proper editor mode (JS or HTML) according to the specified
* content type, or by determining the type from the URL.
*
* @param string aUrl
* The script URL.
* @param string aContentType [optional]
* The script content type.
*/
_setEditorMode: function SS__setEditorMode(aUrl, aContentType) {
if (aContentType) {
if (/javascript/.test(aContentType)) {
DebuggerView.editor.setMode(SourceEditor.MODES.JAVASCRIPT);
} else {
DebuggerView.editor.setMode(SourceEditor.MODES.HTML);
}
return;
}
// Use JS mode for files with .js and .jsm extensions.
if (/\.jsm?$/.test(this.trimUrlQuery(aUrl))) {
DebuggerView.editor.setMode(SourceEditor.MODES.JAVASCRIPT);
} else {
DebuggerView.editor.setMode(SourceEditor.MODES.HTML);
}
},
/**
* Trims the query part or reference identifier of a url string, if necessary.
*
* @param string aUrl
* The script url.
* @return string
* The url with the trimmed query.
*/
trimUrlQuery: function SS_trimUrlQuery(aUrl) {
let length = aUrl.length;
let q1 = aUrl.indexOf('?');
let q2 = aUrl.indexOf('&');
let q3 = aUrl.indexOf('#');
let q = Math.min(q1 !== -1 ? q1 : length,
q2 !== -1 ? q2 : length,
q3 !== -1 ? q3 : length);
return aUrl.slice(0, q);
},
/**
* Trims as much as possible from a URL, while keeping the result unique
* in the Debugger View scripts container.
*
* @param string | nsIURL aUrl
* The script URL.
* @param string aLabel [optional]
* The resulting label at each step.
* @param number aSeq [optional]
* The current iteration step.
* @return string
* The resulting label at the final step.
*/
_trimURL: function SS__trimURL(aUrl, aLabel, aSeq) {
if (!(aUrl instanceof Ci.nsIURL)) {
try {
// Use an nsIURL to parse all the url path parts.
aUrl = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
} catch (e) {
// This doesn't look like a url, or nsIURL can't handle it.
return aUrl;
}
}
if (!aSeq) {
let name = aUrl.fileName;
if (name) {
// This is a regular file url, get only the file name (contains the
// base name and extension if available).
// If this url contains an invalid query, unfortunately nsIURL thinks
// it's part of the file extension. It must be removed.
aLabel = aUrl.fileName.replace(/\&.*/, "");
} else {
// This is not a file url, hence there is no base name, nor extension.
// Proceed using other available information.
aLabel = "";
}
aSeq = 1;
}
// If we have a label and it doesn't start with a query...
if (aLabel && aLabel.indexOf("?") !== 0) {
if (DebuggerView.Scripts.containsIgnoringQuery(aUrl.spec)) {
// A page may contain multiple requests to the same url but with different
// queries. It would be redundant to show each one.
return aLabel;
}
if (!DebuggerView.Scripts.containsLabel(aLabel)) {
// We found the shortest unique label for the url.
return aLabel;
}
}
// Append the url query.
if (aSeq === 1) {
let query = aUrl.query;
if (query) {
return this._trimURL(aUrl, aLabel + "?" + query, aSeq + 1);
}
aSeq++;
}
// Append the url reference.
if (aSeq === 2) {
let ref = aUrl.ref;
if (ref) {
return this._trimURL(aUrl, aLabel + "#" + aUrl.ref, aSeq + 1);
}
aSeq++;
}
// Prepend the url directory.
if (aSeq === 3) {
let dir = aUrl.directory;
if (dir) {
return this._trimURL(aUrl, dir.replace(/^\//, "") + aLabel, aSeq + 1);
}
aSeq++;
}
// Prepend the hostname and port number.
if (aSeq === 4) {
let host = aUrl.hostPort;
if (host) {
return this._trimURL(aUrl, host + "/" + aLabel, aSeq + 1);
}
aSeq++;
}
// Use the whole url spec but ignoring the reference.
if (aSeq === 5) {
return this._trimURL(aUrl, aUrl.specIgnoringRef, aSeq + 1);
}
// Give up.
return aUrl.spec;
},
/**
* Gets a unique, simplified label from a script url.
*
* @param string aUrl
* The script url.
* @param string aHref
* The content location href to be used. If unspecified, it will
* default to the script url prepath.
* @return string
* The simplified label.
*/
_getScriptLabel: function SS__getScriptLabel(aUrl, aHref) {
return this._labelsCache[aUrl] || (this._labelsCache[aUrl] = this._trimURL(aUrl));
},
/**
* Clears the labels cache, populated by SS_getScriptLabel.
* This should be done every time the content location changes.
*/
_clearLabelsCache: function SS__clearLabelsCache() {
this._labelsCache = {};
},
/**
* Add the specified script to the list.
*
* @param object aScript
* The script object coming from the active thread.
* @param boolean aForceFlag
* True to force the script to be immediately added.
*/
_addScript: function SS__addScript(aScript, aForceFlag) {
DebuggerView.Scripts.addScript(
this._getScriptLabel(aScript.url), aScript, aForceFlag);
},
/**
* Load the editor with the script text if available, otherwise fire an event
* to load and display the script text.
*
* @param object aScript
* The script object coming from the active thread.
* @param object [aOptions]
* Additional options for showing the script. Supported options:
* - targetLine: place the editor at the given line number.
*/
showScript: function SS_showScript(aScript, aOptions) {
if (aScript.loaded) {
this._onShowScript(aScript, aOptions);
return;
}
let editor = DebuggerView.editor;
editor.setMode(SourceEditor.MODES.TEXT);
editor.setText(L10N.getStr("loadingText"));
editor.resetUndo();
// Notify that we need to load a script file.
DebuggerController.dispatchEvent("Debugger:LoadSource", {
url: aScript.url,
options: aOptions
});
},
/**
* Display the script source once it loads.
*
* @private
* @param object aScript
* The script object coming from the active thread.
* @param object aOptions [optional]
* Additional options for showing the script. Supported options:
* - targetLine: place the editor at the given line number.
*/
_onShowScript: function SS__onShowScript(aScript, aOptions) {
aOptions = aOptions || {};
if (aScript.text.length < SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) {
this._setEditorMode(aScript.url, aScript.contentType);
}
let editor = DebuggerView.editor;
editor.setText(aScript.text);
editor.resetUndo();
DebuggerController.Breakpoints.updateEditorBreakpoints();
DebuggerController.StackFrames.updateEditorLocation();
// Handle any additional options for showing the script.
if (aOptions.targetLine) {
editor.setCaretPosition(aOptions.targetLine - 1);
}
// Notify that we shown script file.
DebuggerController.dispatchEvent("Debugger:ScriptShown", {
url: aScript.url
});
},
/**
* Handles notifications to load a source script from the cache or from a
* local file.
*
* XXX: It may be better to use nsITraceableChannel to get to the sources
* without relying on caching when we can (not for eval, etc.):
* http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
*/
_onLoadSource: function SS__onLoadSource(aEvent) {
let url = aEvent.detail.url;
let options = aEvent.detail.options;
let self = this;
switch (Services.io.extractScheme(url)) {
case "file":
case "chrome":
case "resource":
try {
NetUtil.asyncFetch(url, function onFetch(aStream, aStatus) {
if (!Components.isSuccessCode(aStatus)) {
return self._logError(url, aStatus);
}
let source = NetUtil.readInputStreamToString(aStream, aStream.available());
self._onLoadSourceFinished(url, source, null, options);
aStream.close();
});
} catch (ex) {
return self._logError(url, ex.name);
}
break;
default:
let channel = Services.io.newChannel(url, null, null);
let chunks = [];
let streamListener = {
onStartRequest: function(aRequest, aContext, aStatusCode) {
if (!Components.isSuccessCode(aStatusCode)) {
return self._logError(url, aStatusCode);
}
},
onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
},
onStopRequest: function(aRequest, aContext, aStatusCode) {
if (!Components.isSuccessCode(aStatusCode)) {
return self._logError(url, aStatusCode);
}
self._onLoadSourceFinished(
url, chunks.join(""), channel.contentType, options);
}
};
channel.loadFlags = channel.LOAD_FROM_CACHE;
channel.asyncOpen(streamListener, null);
break;
}
},
/**
* Called when source has been loaded.
*
* @private
* @param string aSourceUrl
* The URL of the source script.
* @param string aSourceText
* The text of the source script.
* @param string aContentType
* The content type of the source script.
* @param object aOptions [optional]
* Additional options for showing the script. Supported options:
* - targetLine: place the editor at the given line number.
*/
_onLoadSourceFinished:
function SS__onLoadSourceFinished(aSourceUrl, aSourceText, aContentType, aOptions) {
let scripts = document.getElementById("scripts");
let element = scripts.getElementsByAttribute("value", aSourceUrl)[0];
let script = element.getUserData("sourceScript");
script.loaded = true;
script.text = aSourceText;
script.contentType = aContentType;
element.setUserData("sourceScript", script, null);
this.showScript(script, aOptions);
},
/**
* Log an error message in the error console when a script fails to load.
*
* @param string aUrl
* The URL of the source script.
* @param string aStatus
* The failure status code.
*/
_logError: function SS__logError(aUrl, aStatus) {
Cu.reportError(L10N.getFormatStr("loadingError", [aUrl, aStatus]));
},
};
/**
* 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 = {
/**
* 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.
*
* @private
* @type boolean
*/
_skipEditorBreakpointChange: false,
/**
* 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".
*
* @type object
*/
store: {},
/**
* Gets the current thread the client has connected to.
*/
get activeThread() {
return DebuggerController.ThreadState.activeThread;
},
/**
* Gets the source editor in the debugger view.
*/
get editor() {
return DebuggerView.editor;
},
/**
* Sets up the source editor breakpoint handlers.
*/
initialize: function BP_initialize() {
this.editor.addEventListener(
SourceEditor.EVENTS.BREAKPOINT_CHANGE, this._onEditorBreakpointChange);
},
/**
* Removes all currently added breakpoints.
*/
destroy: function BP_destroy() {
for each (let breakpoint in this.store) {
this.removeBreakpoint(breakpoint);
}
},
/**
* Event handler for breakpoint changes that happen in the editor. This
* function syncs the breakpoint changes in the editor to those in the
* debugger.
*
* @private
* @param object aEvent
* The SourceEditor.EVENTS.BREAKPOINT_CHANGE event object.
*/
_onEditorBreakpointChange: function BP__onEditorBreakpointChange(aEvent) {
if (this._skipEditorBreakpointChange) {
return;
}
aEvent.added.forEach(this._onEditorBreakpointAdd, this);
aEvent.removed.forEach(this._onEditorBreakpointRemove, this);
},
/**
* Event handler for new breakpoints that come from the editor.
*
* @private
* @param object aBreakpoint
* The breakpoint object coming from the editor.
*/
_onEditorBreakpointAdd: function BP__onEditorBreakpointAdd(aBreakpoint) {
let url = DebuggerView.Scripts.selected;
if (!url) {
return;
}
let line = aBreakpoint.line + 1;
this.addBreakpoint({ url: url, line: line }, null, true);
},
/**
* Event handler for breakpoints that are removed from the editor.
*
* @private
* @param object aBreakpoint
* The breakpoint object that was removed from the editor.
*/
_onEditorBreakpointRemove: function BP__onEditorBreakpointRemove(aBreakpoint) {
let url = DebuggerView.Scripts.selected;
if (!url) {
return;
}
let line = aBreakpoint.line + 1;
let breakpoint = this.getBreakpoint(url, line);
if (breakpoint) {
this.removeBreakpoint(breakpoint, null, true);
}
},
/**
* 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() {
let url = DebuggerView.Scripts.selected;
if (!url) {
return;
}
this._skipEditorBreakpointChange = true;
for each (let breakpoint in this.store) {
if (breakpoint.location.url == url) {
this.editor.addBreakpoint(breakpoint.location.line - 1);
}
}
this._skipEditorBreakpointChange = false;
},
/**
* Add a breakpoint.
*
* @param object aLocation
* The location where you want the breakpoint. This object must have
* two properties:
* - url - the URL of the script.
* - line - the line number (starting from 1).
* @param function [aCallback]
* Optional function to invoke once the breakpoint is added. The
* callback is invoked with two arguments:
* - aBreakpointClient - the BreakpointActor client object, if the
* breakpoint has been added successfully.
* - aResponseError - if there was any error.
* @param boolean [aNoEditorUpdate=false]
* Tells if you want to skip editor updates. Typically the editor is
* updated to visually indicate that a breakpoint has been added.
*/
addBreakpoint:
function BP_addBreakpoint(aLocation, aCallback, aNoEditorUpdate) {
let breakpoint = this.getBreakpoint(aLocation.url, aLocation.line);
if (breakpoint) {
aCallback && aCallback(breakpoint);
return;
}
this.activeThread.setBreakpoint(aLocation, function(aResponse, aBpClient) {
this.store[aBpClient.actor] = aBpClient;
this.displayBreakpoint(aLocation, aNoEditorUpdate);
aCallback && aCallback(aBpClient, aResponse.error);
}.bind(this));
},
/**
* Update the editor to display the specified breakpoint in the gutter.
*
* @param object aLocation
* The location where you want the breakpoint. This object must have
* two properties:
* - url - the URL of the script.
* - line - the line number (starting from 1).
* @param boolean [aNoEditorUpdate=false]
* Tells if you want to skip editor updates. Typically the editor is
* updated to visually indicate that a breakpoint has been added.
*/
displayBreakpoint: function BP_displayBreakpoint(aLocation, aNoEditorUpdate) {
if (!aNoEditorUpdate) {
let url = DebuggerView.Scripts.selected;
if (url == aLocation.url) {
this._skipEditorBreakpointChange = true;
this.editor.addBreakpoint(aLocation.line - 1);
this._skipEditorBreakpointChange = false;
}
}
},
/**
* Remove a breakpoint.
*
* @param object aBreakpoint
* The breakpoint you want to remove.
* @param function [aCallback]
* Optional function to invoke once the breakpoint is removed. The
* callback is invoked with one argument: the breakpoint location
* object which holds the url and line properties.
* @param boolean [aNoEditorUpdate=false]
* Tells if you want to skip editor updates. Typically the editor is
* updated to visually indicate that a breakpoint has been removed.
*/
removeBreakpoint:
function BP_removeBreakpoint(aBreakpoint, aCallback, aNoEditorUpdate) {
if (!(aBreakpoint.actor in this.store)) {
aCallback && aCallback(aBreakpoint.location);
return;
}
aBreakpoint.remove(function() {
delete this.store[aBreakpoint.actor];
if (!aNoEditorUpdate) {
let url = DebuggerView.Scripts.selected;
if (url == aBreakpoint.location.url) {
this._skipEditorBreakpointChange = true;
this.editor.removeBreakpoint(aBreakpoint.location.line - 1);
this._skipEditorBreakpointChange = false;
}
}
aCallback && aCallback(aBreakpoint.location);
}.bind(this));
},
/**
* 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 breakpoint in this.store) {
if (breakpoint.location.url == aUrl && breakpoint.location.line == aLine) {
return breakpoint;
}
}
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);
});
/**
* Shortcuts for accessing various debugger preferences.
*/
let Prefs = {
/**
* Gets the preferred stackframes pane width.
* @return number
*/
get stackframesWidth() {
if (this._sfrmWidth === undefined) {
this._sfrmWidth = Services.prefs.getIntPref("devtools.debugger.ui.stackframes-width");
}
return this._sfrmWidth;
},
/**
* Sets the preferred stackframes pane width.
* @return number
*/
set stackframesWidth(value) {
Services.prefs.setIntPref("devtools.debugger.ui.stackframes-width", value);
this._sfrmWidth = value;
},
/**
* Gets the preferred variables pane width.
* @return number
*/
get variablesWidth() {
if (this._varsWidth === undefined) {
this._varsWidth = Services.prefs.getIntPref("devtools.debugger.ui.variables-width");
}
return this._varsWidth;
},
/**
* Sets the preferred variables pane width.
* @return number
*/
set variablesWidth(value) {
Services.prefs.setIntPref("devtools.debugger.ui.variables-width", value);
this._varsWidth = value;
},
/**
* Gets a flag specifying if the the debugger should automatically connect to
* the default host and port number.
* @return boolean
*/
get remoteAutoConnect() {
if (this._autoConn === undefined) {
this._autoConn = Services.prefs.getBoolPref("devtools.debugger.remote-autoconnect");
}
return this._autoConn;
},
/**
* Sets a flag specifying if the the debugger should automatically connect.
* @param boolean value
*/
set remoteAutoConnect(value) {
Services.prefs.setBoolPref("devtools.debugger.remote-autoconnect", value);
this._autoConn = value;
}
};
/**
* Gets the preferred default remote debugging host.
* @return string
*/
XPCOMUtils.defineLazyGetter(Prefs, "remoteHost", function() {
return Services.prefs.getCharPref("devtools.debugger.remote-host");
});
/**
* Gets the preferred default remote debugging port.
* @return number
*/
XPCOMUtils.defineLazyGetter(Prefs, "remotePort", function() {
return Services.prefs.getIntPref("devtools.debugger.remote-port");
});
/**
* Gets the max number of attempts to reconnect to a remote server.
* @return number
*/
XPCOMUtils.defineLazyGetter(Prefs, "remoteConnectionRetries", function() {
return Services.prefs.getIntPref("devtools.debugger.remote-connection-retries");
});
/**
* Gets the remote debugging connection timeout (in milliseconds).
* @return number
*/
XPCOMUtils.defineLazyGetter(Prefs, "remoteTimeout", function() {
return Services.prefs.getIntPref("devtools.debugger.remote-timeout");
});
/**
* Preliminary setup for the DebuggerController object.
*/
DebuggerController.init();
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 in tests.
*/
Object.defineProperty(window, "gClient", {
get: function() { return DebuggerController.client; }
});
Object.defineProperty(window, "gTabClient", {
get: function() { return DebuggerController.tabClient; }
});
Object.defineProperty(window, "gThreadClient", {
get: function() { return DebuggerController.activeThread; }
});