Bug 991797 - convert most of the debugger frontend to use Task.jsm and fix discovered async errors r=victorporof

This commit is contained in:
Wes Kocher 2014-05-20 15:55:39 -07:00
parent 7c96c27167
commit 5c2407243c
9 changed files with 409 additions and 339 deletions

View File

@ -44,9 +44,12 @@ const EVENTS = {
BREAKPOINT_ADDED: "Debugger:BreakpointAdded",
BREAKPOINT_REMOVED: "Debugger:BreakpointRemoved",
// When a breakpoint has been shown or hidden in the source editor.
BREAKPOINT_SHOWN: "Debugger:BreakpointShown",
BREAKPOINT_HIDDEN: "Debugger:BreakpointHidden",
// 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 showing or hiding.
CONDITIONAL_BREAKPOINT_POPUP_SHOWING: "Debugger:ConditionalBreakpointPopupShowing",
@ -68,6 +71,9 @@ const EVENTS = {
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",
@ -90,7 +96,6 @@ const FRAME_TYPE = {
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/devtools/event-emitter.js");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource:///modules/devtools/SimpleListWidget.jsm");
Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm");
Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
@ -105,6 +110,9 @@ const DebuggerEditor = require("devtools/sourceeditor/debugger.js");
const {Tooltip} = require("devtools/shared/widgets/Tooltip");
const FastListWidget = require("devtools/shared/widgets/FastListWidget");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Parser",
"resource:///modules/devtools/Parser.jsm");
@ -147,13 +155,14 @@ let DebuggerController = {
* @return object
* A promise that is resolved when the debugger finishes startup.
*/
startupDebugger: function() {
startupDebugger: Task.async(function*() {
if (this._startup) {
return this._startup;
return;
}
return this._startup = DebuggerView.initialize();
},
yield DebuggerView.initialize();
this._startup = true;
}),
/**
* Destroys the view and disconnects the debugger client from the server.
@ -161,20 +170,20 @@ let DebuggerController = {
* @return object
* A promise that is resolved when the debugger finishes shutdown.
*/
shutdownDebugger: function() {
shutdownDebugger: Task.async(function*() {
if (this._shutdown) {
return this._shutdown;
return;
}
return this._shutdown = DebuggerView.destroy().then(() => {
DebuggerView.destroy();
this.SourceScripts.disconnect();
this.StackFrames.disconnect();
this.ThreadState.disconnect();
this.Tracer.disconnect();
this.disconnect();
});
},
yield DebuggerView.destroy();
this.SourceScripts.disconnect();
this.StackFrames.disconnect();
this.ThreadState.disconnect();
this.Tracer.disconnect();
this.disconnect();
this._shutdown = true;
}),
/**
* Initiates remote debugging based on the current target, wiring event
@ -183,14 +192,11 @@ let DebuggerController = {
* @return object
* A promise that is resolved when the debugger finishes connecting.
*/
connect: function() {
if (this._connection) {
return this._connection;
connect: Task.async(function*() {
if (this._connected) {
return;
}
let startedDebugging = promise.defer();
this._connection = startedDebugging.promise;
let target = this._target;
let { client, form: { chromeDebugger, traceActor, addonActor } } = target;
target.on("close", this._onTabDetached);
@ -199,23 +205,17 @@ let DebuggerController = {
this.client = client;
if (addonActor) {
this._startAddonDebugging(addonActor, startedDebugging.resolve);
yield this._startAddonDebugging(addonActor);
} else if (target.chrome) {
this._startChromeDebugging(chromeDebugger, startedDebugging.resolve);
yield this._startChromeDebugging(chromeDebugger);
} else {
this._startDebuggingTab(startedDebugging.resolve);
const startedTracing = promise.defer();
yield this._startDebuggingTab();
if (Prefs.tracerEnabled && traceActor) {
this._startTracingTab(traceActor, startedTracing.resolve);
} else {
startedTracing.resolve();
yield this._startTracingTab(traceActor);
}
return promise.all([startedDebugging.promise, startedTracing.promise]);
}
return startedDebugging.promise;
},
}),
/**
* Disconnects the debugger client and removes event handlers as necessary.
@ -226,7 +226,7 @@ let DebuggerController = {
return;
}
this._connection = null;
this._connected = false;
this.client = null;
this.activeThread = null;
},
@ -286,30 +286,33 @@ let DebuggerController = {
/**
* Sets up a debugging session.
*
* @param function aCallback
* A function to invoke once the client attaches to the active thread.
* @return object
* A promise resolved once the client attaches to the active thread.
*/
_startDebuggingTab: function(aCallback) {
this._target.activeTab.attachThread({
_startDebuggingTab: function() {
let deferred = promise.defer();
let threadOptions = {
useSourceMaps: Prefs.sourceMapsEnabled
}, (aResponse, aThreadClient) => {
};
this._target.activeTab.attachThread(threadOptions, (aResponse, aThreadClient) => {
if (!aThreadClient) {
Cu.reportError("Couldn't attach to thread: " + aResponse.error);
deferred.reject(new Error("Couldn't attach to thread: " + aResponse.error));
return;
}
this.activeThread = aThreadClient;
this.ThreadState.connect();
this.StackFrames.connect();
this.SourceScripts.connect();
if (aThreadClient.paused) {
aThreadClient.resume(this._ensureResumptionOrder);
}
if (aCallback) {
aCallback();
}
deferred.resolve();
});
return deferred.promise;
},
/**
@ -317,13 +320,17 @@ let DebuggerController = {
*
* @param object aAddonActor
* The actor for the addon that is being debugged.
* @param function aCallback
* A function to invoke once the client attaches to the active thread.
* @return object
* A promise resolved once the client attaches to the active thread.
*/
_startAddonDebugging: function(aAddonActor, aCallback) {
this.client.attachAddon(aAddonActor, (aResponse) => {
return this._startChromeDebugging(aResponse.threadActor, aCallback);
_startAddonDebugging: function(aAddonActor) {
let deferred = promise.defer();
this.client.attachAddon(aAddonActor, aResponse => {
this._startChromeDebugging(aResponse.threadActor).then(deferred.resolve);
});
return deferred.promise;
},
/**
@ -331,28 +338,33 @@ let DebuggerController = {
*
* @param object aChromeDebugger
* The remote protocol grip of the chrome debugger.
* @param function aCallback
* A function to invoke once the client attaches to the active thread.
* @return object
* A promise resolved once the client attaches to the active thread.
*/
_startChromeDebugging: function(aChromeDebugger, aCallback) {
_startChromeDebugging: function(aChromeDebugger) {
let deferred = promise.defer();
let threadOptions = {
useSourceMaps: Prefs.sourceMapsEnabled
};
this.client.attachThread(aChromeDebugger, (aResponse, aThreadClient) => {
if (!aThreadClient) {
Cu.reportError("Couldn't attach to thread: " + aResponse.error);
deferred.reject(new Error("Couldn't attach to thread: " + aResponse.error));
return;
}
this.activeThread = aThreadClient;
this.ThreadState.connect();
this.StackFrames.connect();
this.SourceScripts.connect();
if (aThreadClient.paused) {
aThreadClient.resume(this._ensureResumptionOrder);
}
if (aCallback) {
aCallback();
}
}, { useSourceMaps: Prefs.sourceMapsEnabled });
deferred.resolve();
}, threadOptions);
return deferred.promise;
},
/**
@ -360,24 +372,24 @@ let DebuggerController = {
*
* @param object aTraceActor
* The remote protocol grip of the trace actor.
* @param function aCallback
* A function to invoke once the client attaches to the tracer.
* @return object
* A promise resolved once the client attaches to the tracer.
*/
_startTracingTab: function(aTraceActor, aCallback) {
_startTracingTab: function(aTraceActor) {
let deferred = promise.defer();
this.client.attachTracer(aTraceActor, (response, traceClient) => {
if (!traceClient) {
DevToolsUtils.reportException("DebuggerController._startTracingTab",
new Error("Failed to attach to tracing actor."));
deferred.reject(new Error("Failed to attach to tracing actor."));
return;
}
this.traceClient = traceClient;
this.Tracer.connect();
if (aCallback) {
aCallback();
}
deferred.resolve();
});
return deferred.promise;
},
/**
@ -405,9 +417,9 @@ let DebuggerController = {
});
},
_startup: null,
_shutdown: null,
_connection: null,
_startup: false,
_shutdown: false,
_connected: false,
client: null,
activeThread: null
};
@ -587,79 +599,21 @@ StackFrames.prototype = {
/**
* Handler for the thread client's framesadded notification.
*/
_onFrames: function() {
_onFrames: Task.async(function*() {
// Ignore useless notifications.
if (!this.activeThread || !this.activeThread.cachedFrames.length) {
return;
}
let waitForNextPause = false;
let breakLocation = this._currentBreakpointLocation;
let watchExpressions = this._currentWatchExpressions;
let client = DebuggerController.activeThread.client;
// 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. bug 990137 is
// filed to mark this code to be removed.
if (!client.mainRoot.traits.conditionalBreakpoints) {
// Conditional breakpoints are { breakpoint, expression } tuples. The
// boolean evaluation of the expression decides if the active thread
// automatically resumes execution or not.
if (breakLocation) {
// Make sure a breakpoint actually exists at the specified url and line.
let breakpointPromise = DebuggerController.Breakpoints._getAdded(breakLocation);
if (breakpointPromise) {
breakpointPromise.then(({ conditionalExpression: e }) => { if (e) {
// 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.
this.evaluate(e, { depth: 0, meta: FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL });
waitForNextPause = true;
}});
}
}
// We'll get our evaluation of the current breakpoint's conditional
// expression the next time the thread client pauses...
if (waitForNextPause) {
return;
}
if (this._currentFrameDescription == FRAME_TYPE.CONDITIONAL_BREAKPOINT_EVAL) {
this._currentFrameDescription = FRAME_TYPE.NORMAL;
// If the breakpoint's conditional expression evaluation is falsy,
// automatically resume execution.
if (VariablesView.isFalsy({ value: this._currentEvaluation.return })) {
this.activeThread.resume(DebuggerController._ensureResumptionOrder);
return;
}
}
}
// Watch expressions are evaluated in the context of the topmost frame,
// and the results are displayed in the variables view.
// TODO: handle all of this server-side: Bug 832470, comment 14.
if (watchExpressions) {
// Evaluation causes the stack frames to be cleared and active thread to
// pause, sending a 'clientEvaluated' packet and adding the frames again.
this.evaluate(watchExpressions, { depth: 0, meta: FRAME_TYPE.WATCH_EXPRESSIONS_EVAL });
waitForNextPause = true;
}
// We'll get our evaluation of the current watch expressions the next time
// the thread client pauses...
if (waitForNextPause) {
if (this._currentFrameDescription != FRAME_TYPE.NORMAL &&
this._currentFrameDescription != FRAME_TYPE.PUBLIC_CLIENT_EVAL) {
return;
}
if (this._currentFrameDescription == FRAME_TYPE.WATCH_EXPRESSIONS_EVAL) {
this._currentFrameDescription = FRAME_TYPE.NORMAL;
// If an error was thrown during the evaluation of the watch expressions,
// then at least one expression evaluation could not be performed. So
// remove the most recent watch expression and try again.
if (this._currentEvaluation.throw) {
DebuggerView.WatchExpressions.removeAt(0);
DebuggerController.StackFrames.syncWatchExpressions();
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();
@ -669,7 +623,7 @@ StackFrames.prototype = {
if (this._currentFrameDescription != FRAME_TYPE.NORMAL) {
this._currentFrameDescription = FRAME_TYPE.NORMAL;
}
},
}),
/**
* Fill the StackFrames view with the frames we have in the cache, compressing
@ -678,7 +632,6 @@ StackFrames.prototype = {
_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, where: { url, line }, source } = frame;
let isBlackBoxed = source ? this.activeThread.source(source).isBlackBoxed : false;
@ -689,6 +642,8 @@ StackFrames.prototype = {
DebuggerView.StackFrames.selectedDepth = Math.max(this.currentFrameDepth, 0);
DebuggerView.StackFrames.dirty = this.activeThread.moreFrames;
window.emit(EVENTS.AFTER_FRAMES_REFILLED);
},
/**
@ -731,14 +686,16 @@ StackFrames.prototype = {
* 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;
if (this.activeThread.state == "paused") {
this.activeThread.fillFrames(
CALL_STACK_PAGE_SIZE,
() => DebuggerView.Sources.selectedValue = source);
}
this.activeThread.fillFrames(CALL_STACK_PAGE_SIZE, () => {
DebuggerView.Sources.selectedValue = source;
});
},
/**
@ -926,6 +883,87 @@ StackFrames.prototype = {
}
},
/**
* 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 breakpointPromise = DebuggerController.Breakpoints._getAdded(breakLocation);
if (!breakpointPromise) {
return;
}
let breakpointClient = yield breakpointPromise;
let conditionalExpression = breakpointClient.conditionalExpression;
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,
// automatically resume execution.
if (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,
// then at least one expression evaluation could not be performed. So
// remove the most recent watch expression and try again.
if (this._currentEvaluation.throw) {
DebuggerView.WatchExpressions.removeAt(0);
yield DebuggerController.StackFrames.syncWatchExpressions();
}
}),
/**
* Adds the watch expressions evaluation results to a scope in the view.
*
@ -986,30 +1024,29 @@ StackFrames.prototype = {
}
});
if (sanitizedExpressions.length) {
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, "\\$&") + "\" + " + "'\\n'" + " + \"" +
"} catch (e) {" +
"e.name + ': ' + e.message;" + // TODO: Bug 812765, 812764.
"}" +
"\")"
).join(",") +
"]";
if (!sanitizedExpressions.length) {
this._currentWatchExpressions = null;
this._syncedWatchExpressions = null;
} else {
this._syncedWatchExpressions =
this._currentWatchExpressions = null;
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, "\\$&") + "\" + " + "'\\n'" + " + \"" +
"} catch (e) {" +
"e.name + ': ' + e.message;" + // TODO: Bug 812765, 812764.
"}" +
"\")"
).join(",") +
"]";
}
this.currentFrameDepth = -1;
this._onFrames();
return this._onFrames();
}
};
@ -1310,13 +1347,12 @@ SourceScripts.prototype = {
}
// Get the source text from the active thread.
this.activeThread.source(aSource)
.source(({ error, message, source: text, contentType }) => {
this.activeThread.source(aSource).source(({ error, source: text, contentType }) => {
if (aOnTimeout) {
window.clearTimeout(fetchTimeout);
}
if (error) {
deferred.reject([aSource, message || error]);
deferred.reject([aSource, error]);
} else {
deferred.resolve([aSource, text, contentType]);
}
@ -1429,12 +1465,14 @@ Tracer.prototype = {
* Instructs the tracer actor to start tracing.
*/
startTracing: function(aCallback = () => {}) {
DebuggerView.Tracer.selectTab();
if (this.tracing) {
return;
}
this._trace = "dbg.trace" + Math.random();
this.traceClient.startTrace([
DebuggerView.Tracer.selectTab();
let id = this._trace = "dbg.trace" + Math.random();
let fields = [
"name",
"location",
"parameterNames",
@ -1443,7 +1481,9 @@ Tracer.prototype = {
"return",
"throw",
"yield"
], this._trace, (aResponse) => {
];
this.traceClient.startTrace(fields, id, aResponse => {
const { error } = aResponse;
if (error) {
DevToolsUtils.reportException("Tracer.prototype.startTracing", error);
@ -1475,11 +1515,11 @@ Tracer.prototype = {
onTraces: function (aEvent, { traces }) {
const tracesLength = traces.length;
let tracesToShow;
if (tracesLength > TracerView.MAX_TRACES) {
tracesToShow = traces.slice(tracesLength - TracerView.MAX_TRACES,
tracesLength);
DebuggerView.Tracer.empty();
tracesToShow = traces.slice(tracesLength - TracerView.MAX_TRACES, tracesLength);
this._stack.splice(0, this._stack.length);
DebuggerView.Tracer.empty();
} else {
tracesToShow = traces;
}
@ -1602,7 +1642,6 @@ Tracer.prototype = {
* Handles breaking on event listeners in the currently debugged target.
*/
function EventListeners() {
this._onEventListeners = this._onEventListeners.bind(this);
}
EventListeners.prototype = {
@ -1630,65 +1669,71 @@ EventListeners.prototype = {
},
/**
* Fetches the currently attached event listeners from the debugee.
* Schedules fetching the currently attached event listeners from the debugee.
*/
scheduleEventListenersFetch: function() {
let getListeners = aCallback => gThreadClient.eventListeners(aResponse => {
if (aResponse.error) {
let msg = "Error getting event listeners: " + aResponse.message;
DevToolsUtils.reportException("scheduleEventListenersFetch", msg);
return;
}
let outstandingListenersDefinitionSite = aResponse.listeners.map(aListener => {
const deferred = promise.defer();
gThreadClient.pauseGrip(aListener.function).getDefinitionSite(aResponse => {
if (aResponse.error) {
const msg = "Error getting function definition site: " + aResponse.message;
DevToolsUtils.reportException("scheduleEventListenersFetch", msg);
} else {
aListener.function.url = aResponse.url;
}
deferred.resolve(aListener);
});
return deferred.promise;
});
promise.all(outstandingListenersDefinitionSite).then(aListeners => {
this._onEventListeners(aListeners);
// Notify that event listeners were fetched and shown in the view,
// and callback to resume the active thread if necessary.
window.emit(EVENTS.EVENT_LISTENERS_FETCHED);
aCallback && aCallback();
});
});
// Make sure we're not sending a batch of closely repeated requests.
// This can easily happen whenever new sources are fetched.
setNamedTimeout("event-listeners-fetch", FETCH_EVENT_LISTENERS_DELAY, () => {
if (gThreadClient.state != "paused") {
gThreadClient.interrupt(() => getListeners(() => gThreadClient.resume()));
gThreadClient.interrupt(() => this._getListeners(() => gThreadClient.resume()));
} else {
getListeners();
this._getListeners();
}
});
},
/**
* Callback for a debugger's successful active thread eventListeners() call.
* Fetches the currently attached event listeners from the debugee.
* The thread client state is assumed to be "paused".
*
* @param function aCallback
* Invoked once the event listeners are fetched and displayed.
*/
_onEventListeners: function(aListeners) {
// Add all the listeners in the debugger view event linsteners container.
for (let listener of aListeners) {
DebuggerView.EventListeners.addListener(listener, { staged: true });
}
_getListeners: function(aCallback) {
gThreadClient.eventListeners(Task.async(function*(aResponse) {
if (aResponse.error) {
throw "Error getting event listeners: " + aResponse.message;
}
// Flushes all the prepared events into the event listeners container.
DebuggerView.EventListeners.commit();
// Add all the listeners in the debugger view event linsteners container.
for (let listener of aResponse.listeners) {
let definitionSite = yield this._getDefinitionSite(listener.function);
listener.function.url = definitionSite;
DebuggerView.EventListeners.addListener(listener, { staged: true });
}
// Flushes all the prepared events into the event listeners container.
DebuggerView.EventListeners.commit();
// Notify that event listeners were fetched and shown in the view,
// and callback to resume the active thread if necessary.
window.emit(EVENTS.EVENT_LISTENERS_FETCHED);
aCallback && aCallback();
}.bind(this)));
},
/**
* Gets a function's source-mapped definiton site.
*
* @param object aFunction
* The grip of the function to get the definition site for.
* @return object
* A promise that is resolved with the function's owner source url,
* or rejected if an error occured.
*/
_getDefinitionSite: function(aFunction) {
let deferred = promise.defer();
gThreadClient.pauseGrip(aFunction).getDefinitionSite(aResponse => {
if (aResponse.error) {
deferred.reject("Error getting function definition site: " + aResponse.message);
} else {
deferred.resolve(aResponse.url);
}
});
return deferred.promise;
}
};
@ -1744,9 +1789,10 @@ Breakpoints.prototype = {
* @param number aLine
* Line number where breakpoint was set.
*/
_onEditorBreakpointAdd: function(_, aLine) {
_onEditorBreakpointAdd: Task.async(function*(_, aLine) {
let url = DebuggerView.Sources.selectedValue;
let location = { url: url, line: aLine + 1 };
let breakpointClient = yield this.addBreakpoint(location, { noEditorUpdate: true });
// Initialize the breakpoint, but don't update the editor, since this
// callback is invoked because a breakpoint was added in the editor itself.
@ -1754,16 +1800,16 @@ Breakpoints.prototype = {
// If the breakpoint client has a "requestedLocation" 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.requestedLocation) {
if (breakpointClient.requestedLocation) {
DebuggerView.editor.moveBreakpoint(
aBreakpointClient.requestedLocation.line - 1,
aBreakpointClient.location.line - 1
breakpointClient.requestedLocation.line - 1,
breakpointClient.location.line - 1
);
}
// Notify that we've shown a breakpoint in the source editor.
window.emit(EVENTS.BREAKPOINT_SHOWN);
window.emit(EVENTS.BREAKPOINT_SHOWN_IN_EDITOR);
});
},
}),
/**
* Event handler for breakpoints that are removed from the editor.
@ -1771,17 +1817,14 @@ Breakpoints.prototype = {
* @param number aLine
* Line number where breakpoint was removed.
*/
_onEditorBreakpointRemove: function(_, aLine) {
_onEditorBreakpointRemove: Task.async(function*(_, aLine) {
let url = DebuggerView.Sources.selectedValue;
let location = { url: url, line: aLine + 1 };
yield this.removeBreakpoint(location, { noEditorUpdate: true });
// Destroy the breakpoint, but don't update the editor, since this callback
// is invoked because a breakpoint was removed from the editor itself.
this.removeBreakpoint(location, { noEditorUpdate: true }).then(() => {
// Notify that we've hidden a breakpoint in the source editor.
window.emit(EVENTS.BREAKPOINT_HIDDEN);
});
},
// Notify that we've hidden a breakpoint in the source editor.
window.emit(EVENTS.BREAKPOINT_HIDDEN_IN_EDITOR);
}),
/**
* Update the breakpoints in the editor view. This function takes the list of
@ -1789,19 +1832,18 @@ Breakpoints.prototype = {
* This is invoked when the selected script is changed, or when new sources
* are received via the _onNewSource and _onSourcesAdded event listeners.
*/
updateEditorBreakpoints: function() {
updateEditorBreakpoints: Task.async(function*() {
for (let breakpointPromise of this._addedOrDisabled) {
breakpointPromise.then(aBreakpointClient => {
let currentSourceUrl = DebuggerView.Sources.selectedValue;
let breakpointUrl = aBreakpointClient.location.url;
let breakpointClient = yield breakpointPromise;
let currentSourceUrl = DebuggerView.Sources.selectedValue;
let breakpointUrl = breakpointClient.location.url;
// Update the view only if the breakpoint is in the currently shown source.
if (currentSourceUrl == breakpointUrl) {
this._showBreakpoint(aBreakpointClient, { noPaneUpdate: true });
}
});
// Update the view only if the breakpoint is in the currently shown source.
if (currentSourceUrl == breakpointUrl) {
this._showBreakpoint(breakpointClient, { noPaneUpdate: true });
}
}
},
}),
/**
* Update the breakpoints in the pane view. This function takes the list of
@ -1809,19 +1851,18 @@ Breakpoints.prototype = {
* This is invoked when new sources are received via the _onNewSource and
* _onSourcesAdded event listeners.
*/
updatePaneBreakpoints: function() {
updatePaneBreakpoints: Task.async(function*() {
for (let breakpointPromise of this._addedOrDisabled) {
breakpointPromise.then(aBreakpointClient => {
let container = DebuggerView.Sources;
let breakpointUrl = aBreakpointClient.location.url;
let breakpointClient = yield breakpointPromise;
let container = DebuggerView.Sources;
let breakpointUrl = breakpointClient.location.url;
// Update the view only if the breakpoint exists in a known source.
if (container.containsValue(breakpointUrl)) {
this._showBreakpoint(aBreakpointClient, { noEditorUpdate: true });
}
});
// Update the view only if the breakpoint exists in a known source.
if (container.containsValue(breakpointUrl)) {
this._showBreakpoint(breakpointClient, { noEditorUpdate: true });
}
}
},
}),
/**
* Add a breakpoint.
@ -2036,22 +2077,19 @@ Breakpoints.prototype = {
* @return object
* A promise that will be resolved with the breakpoint client
*/
updateCondition: function(aLocation, aCondition) {
updateCondition: Task.async(function*(aLocation, aCondition) {
let addedPromise = this._getAdded(aLocation);
if (!addedPromise) {
return promise.reject(new Error('breakpoint does not exist ' +
'in specified location'));
throw new Error("Breakpoint does not exist at the specified location");
}
var promise = addedPromise.then(aBreakpointClient => {
return aBreakpointClient.setCondition(gThreadClient, aCondition);
});
let breakpointClient = yield addedPromise;
let promise = breakpointClient.setCondition(gThreadClient, aCondition);
// `setCondition` returns a new breakpoint that has the condition,
// so we need to update the store
this._added.set(this.getIdentifier(aLocation), promise);
return promise;
},
}),
/**
* Update the editor and breakpoints pane to show a specified breakpoint.

View File

@ -3,12 +3,8 @@
/* 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";
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
// Used to detect minification for automatic pretty printing
const SAMPLE_SIZE = 50; // no of lines
const INDENT_COUNT_THRESHOLD = 5; // percentage
@ -41,8 +37,6 @@ function SourcesView() {
this._onConditionalPopupShown = this._onConditionalPopupShown.bind(this);
this._onConditionalPopupHiding = this._onConditionalPopupHiding.bind(this);
this._onConditionalTextboxKeyPress = this._onConditionalTextboxKeyPress.bind(this);
this.updateToolbarButtonsState = this.updateToolbarButtonsState.bind(this);
}
SourcesView.prototype = Heritage.extend(WidgetMethods, {
@ -56,15 +50,6 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
showArrows: true
});
// Sort known source groups towards the end of the list
this.widget.groupSortPredicate = function(a, b) {
if ((a in KNOWN_SOURCE_GROUPS) == (b in KNOWN_SOURCE_GROUPS)) {
return a.localeCompare(b);
}
return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1;
};
this.emptyText = L10N.getStr("noSourcesText");
this._blackBoxCheckboxTooltip = L10N.getStr("blackBoxCheckboxTooltip");
@ -98,6 +83,14 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
return +(aFirst.attachment.label.toLowerCase() >
aSecond.attachment.label.toLowerCase());
});
// Sort known source groups towards the end of the list
this.widget.groupSortPredicate = function(a, b) {
if ((a in KNOWN_SOURCE_GROUPS) == (b in KNOWN_SOURCE_GROUPS)) {
return a.localeCompare(b);
}
return (a in KNOWN_SOURCE_GROUPS) ? 1 : -1;
};
},
/**
@ -216,6 +209,8 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
if (aOptions.openPopup || !aOptions.noEditorUpdate) {
this.highlightBreakpoint(location, aOptions);
}
window.emit(EVENTS.BREAKPOINT_SHOWN_IN_PANE);
},
/**
@ -239,6 +234,8 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
// Clear the breakpoint view.
sourceItem.remove(breakpointItem);
window.emit(EVENTS.BREAKPOINT_HIDDEN_IN_PANE);
},
/**
@ -429,8 +426,8 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
* Unhighlights the current breakpoint in this sources container.
*/
unhighlightBreakpoint: function() {
this._unselectBreakpoint();
this._hideConditionalPopup();
this._unselectBreakpoint();
},
/**
@ -459,7 +456,7 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
/**
* Toggle the pretty printing of the selected source.
*/
togglePrettyPrint: function() {
togglePrettyPrint: Task.async(function*() {
if (this._prettyPrintButton.hasAttribute("disabled")) {
return;
}
@ -486,25 +483,29 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
this._prettyPrintButton.removeAttribute("checked");
}
DebuggerController.SourceScripts.togglePrettyPrint(source)
.then(resetEditor, printError)
.then(DebuggerView.showEditor)
.then(this.updateToolbarButtonsState);
},
try {
let resolution = yield DebuggerController.SourceScripts.togglePrettyPrint(source);
resetEditor(resolution);
} catch (rejection) {
printError(rejection);
}
DebuggerView.showEditor();
this.updateToolbarButtonsState();
}),
/**
* Toggle the black boxed state of the selected source.
*/
toggleBlackBoxing: function() {
toggleBlackBoxing: Task.async(function*() {
const { source } = this.selectedItem.attachment;
const sourceClient = gThreadClient.source(source);
const shouldBlackBox = !sourceClient.isBlackBoxed;
// Be optimistic that the (un-)black boxing will succeed, so enable/disable
// the pretty print button and check/uncheck the black box button
// immediately. Then, once we actually get the results from the server, make
// sure that it is in the correct state again by calling
// `updateToolbarButtonsState`.
// the pretty print button and check/uncheck the black box button immediately.
// Then, once we actually get the results from the server, make sure that
// it is in the correct state again by calling `updateToolbarButtonsState`.
if (shouldBlackBox) {
this._prettyPrintButton.setAttribute("disabled", true);
@ -514,10 +515,14 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
this._blackBoxButton.removeAttribute("checked");
}
DebuggerController.SourceScripts.setBlackBoxing(source, shouldBlackBox)
.then(this.updateToolbarButtonsState,
this.updateToolbarButtonsState);
},
try {
yield DebuggerController.SourceScripts.setBlackBoxing(source, shouldBlackBox);
} catch (e) {
// Continue execution in this task even if blackboxing failed.
}
this.updateToolbarButtonsState();
}),
/**
* Toggles all breakpoints enabled/disabled.
@ -806,7 +811,7 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
/**
* The select listener for the sources container.
*/
_onSourceSelect: function({ detail: sourceItem }) {
_onSourceSelect: Task.async(function*({ detail: sourceItem }) {
if (!sourceItem) {
return;
}
@ -816,12 +821,12 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
// The container is not empty and an actual item was selected.
DebuggerView.setEditorLocation(sourceItem.value);
// Attempt to automatically pretty print minified source code.
if (Prefs.autoPrettyPrint && !sourceClient.isPrettyPrinted) {
DebuggerController.SourceScripts.getText(source).then(([, aText]) => {
if (SourceUtils.isMinified(sourceClient, aText)) {
this.togglePrettyPrint();
}
}).then(null, e => DevToolsUtils.reportException("_onSourceSelect", e));
let isMinified = yield SourceUtils.isMinified(sourceClient);
if (isMinified) {
this.togglePrettyPrint();
}
}
// Set window title. No need to split the url by " -> " here, because it was
@ -830,18 +835,22 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
DebuggerView.maybeShowBlackBoxMessage();
this.updateToolbarButtonsState();
},
}),
/**
* The click listener for the "stop black boxing" button.
*/
_onStopBlackBoxing: function() {
_onStopBlackBoxing: Task.async(function*() {
const { source } = this.selectedItem.attachment;
DebuggerController.SourceScripts.setBlackBoxing(source, false)
.then(this.updateToolbarButtonsState,
this.updateToolbarButtonsState);
},
try {
yield DebuggerController.SourceScripts.setBlackBoxing(source, false);
} catch (e) {
// Continue execution in this task even if blackboxing failed.
}
this.updateToolbarButtonsState();
}),
/**
* The click listener for a breakpoint container.
@ -913,6 +922,7 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
*/
_onConditionalPopupHiding: Task.async(function*() {
this._conditionalPopupVisible = false; // Used in tests.
let breakpointItem = this._selectedBreakpointItem;
let attachment = breakpointItem.attachment;
@ -920,11 +930,9 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
// save the current conditional epression.
let breakpointPromise = DebuggerController.Breakpoints._getAdded(attachment);
if (breakpointPromise) {
let breakpointClient = yield breakpointPromise;
yield DebuggerController.Breakpoints.updateCondition(
breakpointClient.location,
this._cbTextbox.value
);
let { location } = yield breakpointPromise;
let condition = this._cbTextbox.value;
yield DebuggerController.Breakpoints.updateCondition(location, condition);
}
window.emit(EVENTS.CONDITIONAL_BREAKPOINT_POPUP_HIDING);
@ -1124,7 +1132,8 @@ function TracerView() {
DevToolsUtils.makeInfallible(this._onSelect.bind(this));
this._onMouseOver =
DevToolsUtils.makeInfallible(this._onMouseOver.bind(this));
this._onSearch = DevToolsUtils.makeInfallible(this._onSearch.bind(this));
this._onSearch =
DevToolsUtils.makeInfallible(this._onSearch.bind(this));
}
TracerView.MAX_TRACES = 200;
@ -1257,6 +1266,7 @@ TracerView.prototype = Heritage.extend(WidgetMethods, {
*/
_populateVariable: function(aName, aParent, aValue) {
let item = aParent.addItem(aName, { value: aValue });
if (aValue) {
let wrappedValue = new DebuggerController.Tracer.WrappedObject(aValue);
DebuggerView.Variables.controller.populate(item, wrappedValue);
@ -1512,16 +1522,15 @@ let SourceUtils = {
* Determines if the source text is minified by using
* the percentage indented of a subset of lines
*
* @param string aText
* The source text.
* @return boolean
* True if source text is minified.
* @return object
* A promise that resolves to true if source text is minified.
*/
isMinified: function(sourceClient, aText){
isMinified: Task.async(function*(sourceClient) {
if (this._minifiedCache.has(sourceClient)) {
return this._minifiedCache.get(sourceClient);
}
let [, text] = yield DebuggerController.SourceScripts.getText(sourceClient);
let isMinified;
let lineEndIndex = 0;
let lineStartIndex = 0;
@ -1530,14 +1539,14 @@ let SourceUtils = {
let overCharLimit = false;
// Strip comments.
aText = aText.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, "");
text = text.replace(/\/\*[\S\s]*?\*\/|\/\/(.+|\n)/g, "");
while (lines++ < SAMPLE_SIZE) {
lineEndIndex = aText.indexOf("\n", lineStartIndex);
lineEndIndex = text.indexOf("\n", lineStartIndex);
if (lineEndIndex == -1) {
break;
}
if (/^\s+/.test(aText.slice(lineStartIndex, lineEndIndex))) {
if (/^\s+/.test(text.slice(lineStartIndex, lineEndIndex))) {
indentCount++;
}
// For files with no indents but are not minified.
@ -1547,12 +1556,13 @@ let SourceUtils = {
}
lineStartIndex = lineEndIndex + 1;
}
isMinified = ((indentCount / lines ) * 100) < INDENT_COUNT_THRESHOLD ||
overCharLimit;
isMinified =
((indentCount / lines) * 100) < INDENT_COUNT_THRESHOLD || overCharLimit;
this._minifiedCache.set(sourceClient, isMinified);
return isMinified;
},
}),
/**
* Clears the labels, groups and minify cache, populated by methods like
@ -1590,6 +1600,7 @@ let SourceUtils = {
if (!sourceLabel) {
sourceLabel = this.trimUrl(aUrl);
}
let unicodeLabel = NetworkHelper.convertToUnicode(unescape(sourceLabel));
this._labelsCache.set(aUrl, unicodeLabel);
return unicodeLabel;
@ -2328,12 +2339,11 @@ WatchExpressionsView.prototype = Heritage.extend(WidgetMethods, {
* The keypress listener for a watch expression's textbox.
*/
_onKeyPress: function(e) {
switch(e.keyCode) {
switch (e.keyCode) {
case e.DOM_VK_RETURN:
case e.DOM_VK_ESCAPE:
e.stopPropagation();
DebuggerView.editor.focus();
return;
}
}
});
@ -2404,6 +2414,7 @@ EventListenersView.prototype = Heritage.extend(WidgetMethods, {
let eventItem = this.getItemForPredicate(aItem =>
aItem.attachment.url == url &&
aItem.attachment.type == type);
if (eventItem) {
let { selectors, view: { targets } } = eventItem.attachment;
if (selectors.indexOf(selector) == -1) {

View File

@ -120,6 +120,10 @@ ToolbarView.prototype = {
* Listener handling the pause/resume button click event.
*/
_onResumePressed: function() {
if (DebuggerController.StackFrames._currentFrameDescription != FRAME_TYPE.NORMAL) {
return;
}
if (DebuggerController.activeThread.paused) {
let warn = DebuggerController._ensureResumptionOrder;
DebuggerController.StackFrames.currentFrameDepth = -1;
@ -144,6 +148,10 @@ ToolbarView.prototype = {
* Listener handling the step in button click event.
*/
_onStepInPressed: function() {
if (DebuggerController.StackFrames._currentFrameDescription != FRAME_TYPE.NORMAL) {
return;
}
if (DebuggerController.activeThread.paused) {
DebuggerController.StackFrames.currentFrameDepth = -1;
let warn = DebuggerController._ensureResumptionOrder;
@ -315,7 +323,7 @@ OptionsView.prototype = {
window.setTimeout(() => {
DebuggerController.reconfigureThread(pref);
}, POPUP_HIDDEN_DELAY);
}, false);
});
},
_button: null,

View File

@ -59,7 +59,7 @@ function test() {
is(gBreakpointsAdded.size, 0, "no breakpoints added");
let cmd = gContextMenu.querySelector('menuitem[command=addBreakpointCommand]');
let bpShown = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN);
let bpShown = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_EDITOR);
EventUtils.synthesizeMouseAtCenter(cmd, {}, gDebugger);
return bpShown;
}).then(() => {

View File

@ -51,7 +51,12 @@ function test() {
return resumeAndTestBreakpoint(30);
})
.then(() => resumeAndTestNoBreakpoint())
.then(() => reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN, 13))
.then(() => {
return promise.all([
reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_EDITOR, 13),
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_PANE, 13)
]);
})
.then(() => testAfterReload())
.then(() => {
// Reset traits back to default value

View File

@ -103,7 +103,8 @@ function afterDebuggerStatementHit() {
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.NEW_SOURCE),
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCES_ADDED),
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN),
reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN)
reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_EDITOR),
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_PANE)
]).then(testClickAgain);
}

View File

@ -38,7 +38,12 @@ function test() {
.then(() => resumeAndTestBreakpoint(28))
.then(() => resumeAndTestBreakpoint(29))
.then(() => resumeAndTestNoBreakpoint())
.then(() => reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN, 13))
.then(() => {
return promise.all([
reloadActiveTab(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_EDITOR, 13),
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.BREAKPOINT_SHOWN_IN_PANE, 13)
]);
})
.then(() => testAfterReload())
.then(() => closeDebuggerAndFinish(gPanel))
.then(null, aError => {

View File

@ -19,7 +19,9 @@ function test() {
gFrames = gDebugger.DebuggerView.StackFrames;
gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
waitForSourceAndCaretAndScopes(gPanel, ".html", 26).then(performTest);
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED)
.then(performTest);
gDebuggee.gRecurseLimit = (gDebugger.gCallStackPageSize * 2) + 1;
gDebuggee.recurse();
@ -32,15 +34,15 @@ function performTest() {
is(gFrames.itemCount, gDebugger.gCallStackPageSize,
"Should have only the max limit of frames.");
is(gClassicFrames.itemCount, gDebugger.gCallStackPageSize,
"Should have only the max limit of frames in the mirrored view as well.")
"Should have only the max limit of frames in the mirrored view as well.");
gDebugger.gThreadClient.addOneTimeListener("framesadded", () => {
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED).then(() => {
is(gFrames.itemCount, gDebugger.gCallStackPageSize * 2,
"Should now have twice the max limit of frames.");
is(gClassicFrames.itemCount, gDebugger.gCallStackPageSize * 2,
"Should now have twice the max limit of frames in the mirrored view as well.");
gDebugger.gThreadClient.addOneTimeListener("framesadded", () => {
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.AFTER_FRAMES_REFILLED).then(() => {
is(gFrames.itemCount, gDebuggee.gRecurseLimit,
"Should have reached the recurse limit.");
is(gClassicFrames.itemCount, gDebuggee.gRecurseLimit,

View File

@ -25,9 +25,9 @@ function test() {
gDebugger.DebuggerView.toggleInstrumentsPane({ visible: true, animated: false });
waitForSourceShown(gPanel, ".html", 1)
.then(() => addExpressions())
.then(() => performTest())
.then(() => finishTest())
.then(addExpressions)
.then(performTest)
.then(finishTest)
.then(() => closeDebuggerAndFinish(gPanel))
.then(null, aError => {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);