gecko-dev/devtools/server/actors/script.js

2367 строки
74 KiB
JavaScript

/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const Services = require("Services");
const { Cc, Ci, Cr } = require("chrome");
const { ActorPool, OriginalLocation, GeneratedLocation } = require("devtools/server/actors/common");
const { ObjectActor, createValueGrip, longStringGrip } = require("devtools/server/actors/object");
const { ActorClassWithSpec } = require("devtools/shared/protocol");
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
const flags = require("devtools/shared/flags");
const { assert, dumpn } = DevToolsUtils;
const promise = require("promise");
const xpcInspector = require("xpcInspector");
const { DevToolsWorker } = require("devtools/shared/worker/worker");
const object = require("sdk/util/object");
const { threadSpec } = require("devtools/shared/specs/script");
const { defer, resolve, reject, all } = promise;
loader.lazyGetter(this, "Debugger", () => {
let Debugger = require("Debugger");
hackDebugger(Debugger);
return Debugger;
});
loader.lazyRequireGetter(this, "CssLogic", "devtools/server/css-logic", true);
loader.lazyRequireGetter(this, "findCssSelector", "devtools/shared/inspector/css-logic", true);
loader.lazyRequireGetter(this, "events", "sdk/event/core");
loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id");
loader.lazyRequireGetter(this, "BreakpointActor", "devtools/server/actors/breakpoint", true);
loader.lazyRequireGetter(this, "setBreakpointAtEntryPoints", "devtools/server/actors/breakpoint", true);
loader.lazyRequireGetter(this, "getSourceURL", "devtools/server/actors/source", true);
loader.lazyRequireGetter(this, "EnvironmentActor", "devtools/server/actors/environment", true);
loader.lazyRequireGetter(this, "FrameActor", "devtools/server/actors/frame", true);
/**
* A BreakpointActorMap is a map from locations to instances of BreakpointActor.
*/
function BreakpointActorMap() {
this._size = 0;
this._actors = {};
}
BreakpointActorMap.prototype = {
/**
* Return the number of BreakpointActors in this BreakpointActorMap.
*
* @returns Number
* The number of BreakpointActor in this BreakpointActorMap.
*/
get size() {
return this._size;
},
/**
* Generate all BreakpointActors that match the given location in
* this BreakpointActorMap.
*
* @param OriginalLocation location
* The location for which matching BreakpointActors should be generated.
*/
findActors: function* (location = new OriginalLocation()) {
// Fast shortcut for when we know we won't find any actors. Surprisingly
// enough, this speeds up refreshing when there are no breakpoints set by
// about 2x!
if (this.size === 0) {
return;
}
function* findKeys(object, key) {
if (key !== undefined) {
if (key in object) {
yield key;
}
}
else {
for (let key of Object.keys(object)) {
yield key;
}
}
}
let query = {
sourceActorID: location.originalSourceActor ? location.originalSourceActor.actorID : undefined,
line: location.originalLine,
};
// If location contains a line, assume we are searching for a whole line
// breakpoint, and set begin/endColumn accordingly. Otherwise, we are
// searching for all breakpoints, so begin/endColumn should be left unset.
if (location.originalLine) {
query.beginColumn = location.originalColumn ? location.originalColumn : 0;
query.endColumn = location.originalColumn ? location.originalColumn + 1 : Infinity;
} else {
query.beginColumn = location.originalColumn ? query.originalColumn : undefined;
query.endColumn = location.originalColumn ? query.originalColumn + 1 : undefined;
}
for (let sourceActorID of findKeys(this._actors, query.sourceActorID))
for (let line of findKeys(this._actors[sourceActorID], query.line))
for (let beginColumn of findKeys(this._actors[sourceActorID][line], query.beginColumn))
for (let endColumn of findKeys(this._actors[sourceActorID][line][beginColumn], query.endColumn)) {
yield this._actors[sourceActorID][line][beginColumn][endColumn];
}
},
/**
* Return the BreakpointActor at the given location in this
* BreakpointActorMap.
*
* @param OriginalLocation location
* The location for which the BreakpointActor should be returned.
*
* @returns BreakpointActor actor
* The BreakpointActor at the given location.
*/
getActor: function (originalLocation) {
for (let actor of this.findActors(originalLocation)) {
return actor;
}
return null;
},
/**
* Set the given BreakpointActor to the given location in this
* BreakpointActorMap.
*
* @param OriginalLocation location
* The location to which the given BreakpointActor should be set.
*
* @param BreakpointActor actor
* The BreakpointActor to be set to the given location.
*/
setActor: function (location, actor) {
let { originalSourceActor, originalLine, originalColumn } = location;
let sourceActorID = originalSourceActor.actorID;
let line = originalLine;
let beginColumn = originalColumn ? originalColumn : 0;
let endColumn = originalColumn ? originalColumn + 1 : Infinity;
if (!this._actors[sourceActorID]) {
this._actors[sourceActorID] = [];
}
if (!this._actors[sourceActorID][line]) {
this._actors[sourceActorID][line] = [];
}
if (!this._actors[sourceActorID][line][beginColumn]) {
this._actors[sourceActorID][line][beginColumn] = [];
}
if (!this._actors[sourceActorID][line][beginColumn][endColumn]) {
++this._size;
}
this._actors[sourceActorID][line][beginColumn][endColumn] = actor;
},
/**
* Delete the BreakpointActor from the given location in this
* BreakpointActorMap.
*
* @param OriginalLocation location
* The location from which the BreakpointActor should be deleted.
*/
deleteActor: function (location) {
let { originalSourceActor, originalLine, originalColumn } = location;
let sourceActorID = originalSourceActor.actorID;
let line = originalLine;
let beginColumn = originalColumn ? originalColumn : 0;
let endColumn = originalColumn ? originalColumn + 1 : Infinity;
if (this._actors[sourceActorID]) {
if (this._actors[sourceActorID][line]) {
if (this._actors[sourceActorID][line][beginColumn]) {
if (this._actors[sourceActorID][line][beginColumn][endColumn]) {
--this._size;
}
delete this._actors[sourceActorID][line][beginColumn][endColumn];
if (Object.keys(this._actors[sourceActorID][line][beginColumn]).length === 0) {
delete this._actors[sourceActorID][line][beginColumn];
}
}
if (Object.keys(this._actors[sourceActorID][line]).length === 0) {
delete this._actors[sourceActorID][line];
}
}
}
}
};
exports.BreakpointActorMap = BreakpointActorMap;
/**
* Keeps track of persistent sources across reloads and ties different
* source instances to the same actor id so that things like
* breakpoints survive reloads. ThreadSources uses this to force the
* same actorID on a SourceActor.
*/
function SourceActorStore() {
// source identifier --> actor id
this._sourceActorIds = Object.create(null);
}
SourceActorStore.prototype = {
/**
* Lookup an existing actor id that represents this source, if available.
*/
getReusableActorId: function (aSource, aOriginalUrl) {
let url = this.getUniqueKey(aSource, aOriginalUrl);
if (url && url in this._sourceActorIds) {
return this._sourceActorIds[url];
}
return null;
},
/**
* Update a source with an actorID.
*/
setReusableActorId: function (aSource, aOriginalUrl, actorID) {
let url = this.getUniqueKey(aSource, aOriginalUrl);
if (url) {
this._sourceActorIds[url] = actorID;
}
},
/**
* Make a unique URL from a source that identifies it across reloads.
*/
getUniqueKey: function (aSource, aOriginalUrl) {
if (aOriginalUrl) {
// Original source from a sourcemap.
return aOriginalUrl;
}
else {
return getSourceURL(aSource);
}
}
};
exports.SourceActorStore = SourceActorStore;
/**
* Manages pushing event loops and automatically pops and exits them in the
* correct order as they are resolved.
*
* @param ThreadActor thread
* The thread actor instance that owns this EventLoopStack.
* @param DebuggerServerConnection connection
* The remote protocol connection associated with this event loop stack.
* @param Object hooks
* An object with the following properties:
* - url: The URL string of the debuggee we are spinning an event loop
* for.
* - preNest: function called before entering a nested event loop
* - postNest: function called after exiting a nested event loop
*/
function EventLoopStack({ thread, connection, hooks }) {
this._hooks = hooks;
this._thread = thread;
this._connection = connection;
}
EventLoopStack.prototype = {
/**
* The number of nested event loops on the stack.
*/
get size() {
return xpcInspector.eventLoopNestLevel;
},
/**
* The URL of the debuggee who pushed the event loop on top of the stack.
*/
get lastPausedUrl() {
let url = null;
if (this.size > 0) {
try {
url = xpcInspector.lastNestRequestor.url;
} catch (e) {
// The tab's URL getter may throw if the tab is destroyed by the time
// this code runs, but we don't really care at this point.
dumpn(e);
}
}
return url;
},
/**
* The DebuggerServerConnection of the debugger who pushed the event loop on
* top of the stack
*/
get lastConnection() {
return xpcInspector.lastNestRequestor._connection;
},
/**
* Push a new nested event loop onto the stack.
*
* @returns EventLoop
*/
push: function () {
return new EventLoop({
thread: this._thread,
connection: this._connection,
hooks: this._hooks
});
}
};
/**
* An object that represents a nested event loop. It is used as the nest
* requestor with nsIJSInspector instances.
*
* @param ThreadActor thread
* The thread actor that is creating this nested event loop.
* @param DebuggerServerConnection connection
* The remote protocol connection associated with this event loop.
* @param Object hooks
* The same hooks object passed into EventLoopStack during its
* initialization.
*/
function EventLoop({ thread, connection, hooks }) {
this._thread = thread;
this._hooks = hooks;
this._connection = connection;
this.enter = this.enter.bind(this);
this.resolve = this.resolve.bind(this);
}
EventLoop.prototype = {
entered: false,
resolved: false,
get url() { return this._hooks.url; },
/**
* Enter this nested event loop.
*/
enter: function () {
let nestData = this._hooks.preNest
? this._hooks.preNest()
: null;
this.entered = true;
xpcInspector.enterNestedEventLoop(this);
// Keep exiting nested event loops while the last requestor is resolved.
if (xpcInspector.eventLoopNestLevel > 0) {
const { resolved } = xpcInspector.lastNestRequestor;
if (resolved) {
xpcInspector.exitNestedEventLoop();
}
}
if (this._hooks.postNest) {
this._hooks.postNest(nestData);
}
},
/**
* Resolve this nested event loop.
*
* @returns boolean
* True if we exited this nested event loop because it was on top of
* the stack, false if there is another nested event loop above this
* one that hasn't resolved yet.
*/
resolve: function () {
if (!this.entered) {
throw new Error("Can't resolve an event loop before it has been entered!");
}
if (this.resolved) {
throw new Error("Already resolved this nested event loop!");
}
this.resolved = true;
if (this === xpcInspector.lastNestRequestor) {
xpcInspector.exitNestedEventLoop();
return true;
}
return false;
},
};
/**
* JSD2 actors.
*/
/**
* Creates a ThreadActor.
*
* ThreadActors manage a JSInspector object and manage execution/inspection
* of debuggees.
*
* @param aParent object
* This |ThreadActor|'s parent actor. It must implement the following
* properties:
* - url: The URL string of the debuggee.
* - window: The global window object.
* - preNest: Function called before entering a nested event loop.
* - postNest: Function called after exiting a nested event loop.
* - makeDebugger: A function that takes no arguments and instantiates
* a Debugger that manages its globals on its own.
* @param aGlobal object [optional]
* An optional (for content debugging only) reference to the content
* window.
*/
const ThreadActor = ActorClassWithSpec(threadSpec, {
initialize: function (aParent, aGlobal) {
this._state = "detached";
this._frameActors = [];
this._parent = aParent;
this._dbg = null;
this._gripDepth = 0;
this._threadLifetimePool = null;
this._tabClosed = false;
this._scripts = null;
this._pauseOnDOMEvents = null;
this._options = {
useSourceMaps: false,
autoBlackBox: false
};
this.breakpointActorMap = new BreakpointActorMap();
this.sourceActorStore = new SourceActorStore();
this._debuggerSourcesSeen = null;
// A map of actorID -> actor for breakpoints created and managed by the
// server.
this._hiddenBreakpoints = new Map();
this.global = aGlobal;
this._allEventsListener = this._allEventsListener.bind(this);
this.onNewGlobal = this.onNewGlobal.bind(this);
this.onSourceEvent = this.onSourceEvent.bind(this);
this.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this);
this.onDebuggerStatement = this.onDebuggerStatement.bind(this);
this.onNewScript = this.onNewScript.bind(this);
this.objectGrip = this.objectGrip.bind(this);
this.pauseObjectGrip = this.pauseObjectGrip.bind(this);
this._onWindowReady = this._onWindowReady.bind(this);
events.on(this._parent, "window-ready", this._onWindowReady);
// Set a wrappedJSObject property so |this| can be sent via the observer svc
// for the xpcshell harness.
this.wrappedJSObject = this;
},
// Used by the ObjectActor to keep track of the depth of grip() calls.
_gripDepth: null,
get dbg() {
if (!this._dbg) {
this._dbg = this._parent.makeDebugger();
this._dbg.uncaughtExceptionHook = this.uncaughtExceptionHook;
this._dbg.onDebuggerStatement = this.onDebuggerStatement;
this._dbg.onNewScript = this.onNewScript;
this._dbg.on("newGlobal", this.onNewGlobal);
// Keep the debugger disabled until a client attaches.
this._dbg.enabled = this._state != "detached";
}
return this._dbg;
},
get globalDebugObject() {
if (!this._parent.window) {
return null;
}
return this.dbg.makeGlobalObjectReference(this._parent.window);
},
get state() {
return this._state;
},
get attached() {
return this.state == "attached" ||
this.state == "running" ||
this.state == "paused";
},
get threadLifetimePool() {
if (!this._threadLifetimePool) {
this._threadLifetimePool = new ActorPool(this.conn);
this.conn.addActorPool(this._threadLifetimePool);
this._threadLifetimePool.objectActors = new WeakMap();
}
return this._threadLifetimePool;
},
get sources() {
return this._parent.sources;
},
get youngestFrame() {
if (this.state != "paused") {
return null;
}
return this.dbg.getNewestFrame();
},
_prettyPrintWorker: null,
get prettyPrintWorker() {
if (!this._prettyPrintWorker) {
this._prettyPrintWorker = new DevToolsWorker(
"resource://devtools/server/actors/pretty-print-worker.js",
{ name: "pretty-print",
verbose: flags.wantLogging }
);
}
return this._prettyPrintWorker;
},
/**
* Keep track of all of the nested event loops we use to pause the debuggee
* when we hit a breakpoint/debugger statement/etc in one place so we can
* resolve them when we get resume packets. We have more than one (and keep
* them in a stack) because we can pause within client evals.
*/
_threadPauseEventLoops: null,
_pushThreadPause: function () {
if (!this._threadPauseEventLoops) {
this._threadPauseEventLoops = [];
}
const eventLoop = this._nestedEventLoops.push();
this._threadPauseEventLoops.push(eventLoop);
eventLoop.enter();
},
_popThreadPause: function () {
const eventLoop = this._threadPauseEventLoops.pop();
assert(eventLoop, "Should have an event loop.");
eventLoop.resolve();
},
/**
* Remove all debuggees and clear out the thread's sources.
*/
clearDebuggees: function () {
if (this._dbg) {
this.dbg.removeAllDebuggees();
}
this._sources = null;
this._scripts = null;
},
/**
* Listener for our |Debugger|'s "newGlobal" event.
*/
onNewGlobal: function (aGlobal) {
// Notify the client.
this.conn.send({
from: this.actorID,
type: "newGlobal",
// TODO: after bug 801084 lands see if we need to JSONify this.
hostAnnotations: aGlobal.hostAnnotations
});
},
destroy: function () {
dumpn("in ThreadActor.prototype.destroy");
if (this._state == "paused") {
this.onResume();
}
// Blow away our source actor ID store because those IDs are only
// valid for this connection. This is ok because we never keep
// things like breakpoints across connections.
this._sourceActorStore = null;
events.off(this._parent, "window-ready", this._onWindowReady);
this.sources.off("newSource", this.onSourceEvent);
this.sources.off("updatedSource", this.onSourceEvent);
this.clearDebuggees();
this.conn.removeActorPool(this._threadLifetimePool);
this._threadLifetimePool = null;
if (this._prettyPrintWorker) {
this._prettyPrintWorker.destroy();
this._prettyPrintWorker = null;
}
if (!this._dbg) {
return;
}
this._dbg.enabled = false;
this._dbg = null;
},
/**
* destroy the debugger and put the actor in the exited state.
*/
exit: function () {
this.destroy();
this._state = "exited";
},
// Request handlers
onAttach: function (aRequest) {
if (this.state === "exited") {
return { type: "exited" };
}
if (this.state !== "detached") {
return { error: "wrongState",
message: "Current state is " + this.state };
}
this._state = "attached";
this._debuggerSourcesSeen = new WeakSet();
Object.assign(this._options, aRequest.options || {});
this.sources.setOptions(this._options);
this.sources.on("newSource", this.onSourceEvent);
this.sources.on("updatedSource", this.onSourceEvent);
// Initialize an event loop stack. This can't be done in the constructor,
// because this.conn is not yet initialized by the actor pool at that time.
this._nestedEventLoops = new EventLoopStack({
hooks: this._parent,
connection: this.conn,
thread: this
});
this.dbg.addDebuggees();
this.dbg.enabled = true;
try {
// Put ourselves in the paused state.
let packet = this._paused();
if (!packet) {
return { error: "notAttached" };
}
packet.why = { type: "attached" };
// Send the response to the attach request now (rather than
// returning it), because we're going to start a nested event loop
// here.
this.conn.send(packet);
// Start a nested event loop.
this._pushThreadPause();
// We already sent a response to this request, don't send one
// now.
return null;
} catch (e) {
reportError(e);
return { error: "notAttached", message: e.toString() };
}
},
onDetach: function (aRequest) {
this.destroy();
this._state = "detached";
this._debuggerSourcesSeen = null;
dumpn("ThreadActor.prototype.onDetach: returning 'detached' packet");
return {
type: "detached"
};
},
onReconfigure: function (aRequest) {
if (this.state == "exited") {
return { error: "wrongState" };
}
const options = aRequest.options || {};
if ("observeAsmJS" in options) {
this.dbg.allowUnobservedAsmJS = !options.observeAsmJS;
}
Object.assign(this._options, options);
// Update the global source store
this.sources.setOptions(options);
return {};
},
/**
* Pause the debuggee, by entering a nested event loop, and return a 'paused'
* packet to the client.
*
* @param Debugger.Frame aFrame
* The newest debuggee frame in the stack.
* @param object aReason
* An object with a 'type' property containing the reason for the pause.
* @param function onPacket
* Hook to modify the packet before it is sent. Feel free to return a
* promise.
*/
_pauseAndRespond: function (aFrame, aReason, onPacket = function (k) { return k; }) {
try {
let packet = this._paused(aFrame);
if (!packet) {
return undefined;
}
packet.why = aReason;
let generatedLocation = this.sources.getFrameLocation(aFrame);
this.sources.getOriginalLocation(generatedLocation)
.then((originalLocation) => {
if (!originalLocation.originalSourceActor) {
// The only time the source actor will be null is if there
// was a sourcemap and it tried to look up the original
// location but there was no original URL. This is a strange
// scenario so we simply don't pause.
DevToolsUtils.reportException(
"ThreadActor",
new Error("Attempted to pause in a script with a sourcemap but " +
"could not find original location.")
);
return undefined;
}
packet.frame.where = {
source: originalLocation.originalSourceActor.form(),
line: originalLocation.originalLine,
column: originalLocation.originalColumn
};
resolve(onPacket(packet))
.then(null, error => {
reportError(error);
return {
error: "unknownError",
message: error.message + "\n" + error.stack
};
})
.then(packet => {
this.conn.send(packet);
});
});
this._pushThreadPause();
} catch (e) {
reportError(e, "Got an exception during TA__pauseAndRespond: ");
}
// If the browser tab has been closed, terminate the debuggee script
// instead of continuing. Executing JS after the content window is gone is
// a bad idea.
return this._tabClosed ? null : undefined;
},
_makeOnEnterFrame: function ({ pauseAndRespond }) {
return aFrame => {
const generatedLocation = this.sources.getFrameLocation(aFrame);
let { originalSourceActor } = this.unsafeSynchronize(this.sources.getOriginalLocation(
generatedLocation));
let url = originalSourceActor.url;
return this.sources.isBlackBoxed(url)
? undefined
: pauseAndRespond(aFrame);
};
},
_makeOnPop: function ({ thread, pauseAndRespond, createValueGrip }) {
return function (aCompletion) {
// onPop is called with 'this' set to the current frame.
const generatedLocation = thread.sources.getFrameLocation(this);
const { originalSourceActor } = thread.unsafeSynchronize(thread.sources.getOriginalLocation(
generatedLocation));
const url = originalSourceActor.url;
if (thread.sources.isBlackBoxed(url)) {
return undefined;
}
// Note that we're popping this frame; we need to watch for
// subsequent step events on its caller.
this.reportedPop = true;
return pauseAndRespond(this, aPacket => {
aPacket.why.frameFinished = {};
if (!aCompletion) {
aPacket.why.frameFinished.terminated = true;
} else if (aCompletion.hasOwnProperty("return")) {
aPacket.why.frameFinished.return = createValueGrip(aCompletion.return);
} else if (aCompletion.hasOwnProperty("yield")) {
aPacket.why.frameFinished.return = createValueGrip(aCompletion.yield);
} else {
aPacket.why.frameFinished.throw = createValueGrip(aCompletion.throw);
}
return aPacket;
});
};
},
_makeOnStep: function ({ thread, pauseAndRespond, startFrame,
startLocation, steppingType }) {
// Breaking in place: we should always pause.
if (steppingType === "break") {
return function () {
return pauseAndRespond(this);
};
}
// Otherwise take what a "step" means into consideration.
return function () {
// onStep is called with 'this' set to the current frame.
// Only allow stepping stops at entry points for the line, when
// the stepping occurs in a single frame. The "same frame"
// check makes it so a sequence of steps can step out of a frame
// and into subsequent calls in the outer frame. E.g., if there
// is a call "a(b())" and the user steps into b, then this
// condition makes it possible to step out of b and into a.
if (this === startFrame &&
!this.script.getOffsetLocation(this.offset).isEntryPoint) {
return undefined;
}
const generatedLocation = thread.sources.getFrameLocation(this);
const newLocation = thread.unsafeSynchronize(thread.sources.getOriginalLocation(
generatedLocation));
// Cases when we should pause because we have executed enough to consider
// a "step" to have occured:
//
// 1.1. We change frames.
// 1.2. We change URLs (can happen without changing frames thanks to
// source mapping).
// 1.3. We change lines.
//
// Cases when we should always continue execution, even if one of the
// above cases is true:
//
// 2.1. We are in a source mapped region, but inside a null mapping
// (doesn't correlate to any region of original source)
// 2.2. The source we are in is black boxed.
// Cases 2.1 and 2.2
if (newLocation.originalUrl == null
|| thread.sources.isBlackBoxed(newLocation.originalUrl)) {
return undefined;
}
// Cases 1.1, 1.2 and 1.3
if (this !== startFrame
|| startLocation.originalUrl !== newLocation.originalUrl
|| startLocation.originalLine !== newLocation.originalLine) {
return pauseAndRespond(this);
}
// Otherwise, let execution continue (we haven't executed enough code to
// consider this a "step" yet).
return undefined;
};
},
/**
* Define the JS hook functions for stepping.
*/
_makeSteppingHooks: function (aStartLocation, steppingType) {
// Bind these methods and state because some of the hooks are called
// with 'this' set to the current frame. Rather than repeating the
// binding in each _makeOnX method, just do it once here and pass it
// in to each function.
const steppingHookState = {
pauseAndRespond: (aFrame, onPacket = k=>k) => {
return this._pauseAndRespond(aFrame, { type: "resumeLimit" }, onPacket);
},
createValueGrip: v => createValueGrip(v, this._pausePool,
this.objectGrip),
thread: this,
startFrame: this.youngestFrame,
startLocation: aStartLocation,
steppingType: steppingType
};
return {
onEnterFrame: this._makeOnEnterFrame(steppingHookState),
onPop: this._makeOnPop(steppingHookState),
onStep: this._makeOnStep(steppingHookState)
};
},
/**
* Handle attaching the various stepping hooks we need to attach when we
* receive a resume request with a resumeLimit property.
*
* @param Object aRequest
* The request packet received over the RDP.
* @returns A promise that resolves to true once the hooks are attached, or is
* rejected with an error packet.
*/
_handleResumeLimit: function (aRequest) {
let steppingType = aRequest.resumeLimit.type;
if (["break", "step", "next", "finish"].indexOf(steppingType) == -1) {
return reject({ error: "badParameterType",
message: "Unknown resumeLimit type" });
}
const generatedLocation = this.sources.getFrameLocation(this.youngestFrame);
return this.sources.getOriginalLocation(generatedLocation)
.then(originalLocation => {
const { onEnterFrame, onPop, onStep } = this._makeSteppingHooks(originalLocation,
steppingType);
// Make sure there is still a frame on the stack if we are to continue
// stepping.
let stepFrame = this._getNextStepFrame(this.youngestFrame);
if (stepFrame) {
switch (steppingType) {
case "step":
this.dbg.onEnterFrame = onEnterFrame;
// Fall through.
case "break":
case "next":
if (stepFrame.script) {
stepFrame.onStep = onStep;
}
stepFrame.onPop = onPop;
break;
case "finish":
stepFrame.onPop = onPop;
}
}
return true;
});
},
/**
* Clear the onStep and onPop hooks from the given frame and all of the frames
* below it.
*
* @param Debugger.Frame aFrame
* The frame we want to clear the stepping hooks from.
*/
_clearSteppingHooks: function (aFrame) {
if (aFrame && aFrame.live) {
while (aFrame) {
aFrame.onStep = undefined;
aFrame.onPop = undefined;
aFrame = aFrame.older;
}
}
},
/**
* Listen to the debuggee's DOM events if we received a request to do so.
*
* @param Object aRequest
* The resume request packet received over the RDP.
*/
_maybeListenToEvents: function (aRequest) {
// Break-on-DOMEvents is only supported in content debugging.
let events = aRequest.pauseOnDOMEvents;
if (this.global && events &&
(events == "*" ||
(Array.isArray(events) && events.length))) {
this._pauseOnDOMEvents = events;
let els = Cc["@mozilla.org/eventlistenerservice;1"]
.getService(Ci.nsIEventListenerService);
els.addListenerForAllEvents(this.global, this._allEventsListener, true);
}
},
/**
* If we are tasked with breaking on the load event, we have to add the
* listener early enough.
*/
_onWindowReady: function () {
this._maybeListenToEvents({
pauseOnDOMEvents: this._pauseOnDOMEvents
});
},
/**
* Handle a protocol request to resume execution of the debuggee.
*/
onResume: function (aRequest) {
if (this._state !== "paused") {
return {
error: "wrongState",
message: "Can't resume when debuggee isn't paused. Current state is '"
+ this._state + "'",
state: this._state
};
}
// In case of multiple nested event loops (due to multiple debuggers open in
// different tabs or multiple debugger clients connected to the same tab)
// only allow resumption in a LIFO order.
if (this._nestedEventLoops.size && this._nestedEventLoops.lastPausedUrl
&& (this._nestedEventLoops.lastPausedUrl !== this._parent.url
|| this._nestedEventLoops.lastConnection !== this.conn)) {
return {
error: "wrongOrder",
message: "trying to resume in the wrong order.",
lastPausedUrl: this._nestedEventLoops.lastPausedUrl
};
}
let resumeLimitHandled;
if (aRequest && aRequest.resumeLimit) {
resumeLimitHandled = this._handleResumeLimit(aRequest);
} else {
this._clearSteppingHooks(this.youngestFrame);
resumeLimitHandled = resolve(true);
}
return resumeLimitHandled.then(() => {
if (aRequest) {
this._options.pauseOnExceptions = aRequest.pauseOnExceptions;
this._options.ignoreCaughtExceptions = aRequest.ignoreCaughtExceptions;
this.maybePauseOnExceptions();
this._maybeListenToEvents(aRequest);
}
let packet = this._resumed();
this._popThreadPause();
// Tell anyone who cares of the resume (as of now, that's the xpcshell harness and
// devtools-startup.js when handling the --wait-for-jsdebugger flag)
if (Services.obs) {
Services.obs.notifyObservers(this, "devtools-thread-resumed", null);
}
return packet;
}, error => {
return error instanceof Error
? { error: "unknownError",
message: DevToolsUtils.safeErrorString(error) }
// It is a known error, and the promise was rejected with an error
// packet.
: error;
});
},
/**
* Spin up a nested event loop so we can synchronously resolve a promise.
*
* DON'T USE THIS UNLESS YOU ABSOLUTELY MUST! Nested event loops suck: the
* world's state can change out from underneath your feet because JS is no
* longer run-to-completion.
*
* @param aPromise
* The promise we want to resolve.
* @returns The promise's resolution.
*/
unsafeSynchronize: function (aPromise) {
let needNest = true;
let eventLoop;
let returnVal;
aPromise
.then((aResolvedVal) => {
needNest = false;
returnVal = aResolvedVal;
})
.then(null, (aError) => {
reportError(aError, "Error inside unsafeSynchronize:");
})
.then(() => {
if (eventLoop) {
eventLoop.resolve();
}
});
if (needNest) {
eventLoop = this._nestedEventLoops.push();
eventLoop.enter();
}
return returnVal;
},
/**
* Set the debugging hook to pause on exceptions if configured to do so.
*/
maybePauseOnExceptions: function () {
if (this._options.pauseOnExceptions) {
this.dbg.onExceptionUnwind = this.onExceptionUnwind.bind(this);
}
},
/**
* A listener that gets called for every event fired on the page, when a list
* of interesting events was provided with the pauseOnDOMEvents property. It
* is used to set server-managed breakpoints on any existing event listeners
* for those events.
*
* @param Event event
* The event that was fired.
*/
_allEventsListener: function (event) {
if (this._pauseOnDOMEvents == "*" ||
this._pauseOnDOMEvents.indexOf(event.type) != -1) {
for (let listener of this._getAllEventListeners(event.target)) {
if (event.type == listener.type || this._pauseOnDOMEvents == "*") {
this._breakOnEnter(listener.script);
}
}
}
},
/**
* Return an array containing all the event listeners attached to the
* specified event target and its ancestors in the event target chain.
*
* @param EventTarget eventTarget
* The target the event was dispatched on.
* @returns Array
*/
_getAllEventListeners: function (eventTarget) {
let els = Cc["@mozilla.org/eventlistenerservice;1"]
.getService(Ci.nsIEventListenerService);
let targets = els.getEventTargetChainFor(eventTarget, true);
let listeners = [];
for (let target of targets) {
let handlers = els.getListenerInfoFor(target);
for (let handler of handlers) {
// Null is returned for all-events handlers, and native event listeners
// don't provide any listenerObject, which makes them not that useful to
// a JS debugger.
if (!handler || !handler.listenerObject || !handler.type)
continue;
// Create a listener-like object suitable for our purposes.
let l = Object.create(null);
l.type = handler.type;
let listener = handler.listenerObject;
let listenerDO = this.globalDebugObject.makeDebuggeeValue(listener);
// If the listener is not callable, assume it is an event handler object.
if (!listenerDO.callable) {
// For some events we don't have permission to access the
// 'handleEvent' property when running in content scope.
if (!listenerDO.unwrap()) {
continue;
}
let heDesc;
while (!heDesc && listenerDO) {
heDesc = listenerDO.getOwnPropertyDescriptor("handleEvent");
listenerDO = listenerDO.proto;
}
if (heDesc && heDesc.value) {
listenerDO = heDesc.value;
}
}
// When the listener is a bound function, we are actually interested in
// the target function.
while (listenerDO.isBoundFunction) {
listenerDO = listenerDO.boundTargetFunction;
}
l.script = listenerDO.script;
// Chrome listeners won't be converted to debuggee values, since their
// compartment is not added as a debuggee.
if (!l.script)
continue;
listeners.push(l);
}
}
return listeners;
},
/**
* Set a breakpoint on the first line of the given script that has an entry
* point.
*/
_breakOnEnter: function (script) {
let offsets = script.getAllOffsets();
for (let line = 0, n = offsets.length; line < n; line++) {
if (offsets[line]) {
// N.B. Hidden breakpoints do not have an original location, and are not
// stored in the breakpoint actor map.
let actor = new BreakpointActor(this);
this.threadLifetimePool.addActor(actor);
let scripts = this.dbg.findScripts({ source: script.source, line: line });
let entryPoints = findEntryPointsForLine(scripts, line);
setBreakpointAtEntryPoints(actor, entryPoints);
this._hiddenBreakpoints.set(actor.actorID, actor);
break;
}
}
},
/**
* Helper method that returns the next frame when stepping.
*/
_getNextStepFrame: function (aFrame) {
let stepFrame = aFrame.reportedPop ? aFrame.older : aFrame;
if (!stepFrame || !stepFrame.script) {
stepFrame = null;
}
return stepFrame;
},
onClientEvaluate: function (aRequest) {
if (this.state !== "paused") {
return { error: "wrongState",
message: "Debuggee must be paused to evaluate code." };
}
let frame = this._requestFrame(aRequest.frame);
if (!frame) {
return { error: "unknownFrame",
message: "Evaluation frame not found" };
}
if (!frame.environment) {
return { error: "notDebuggee",
message: "cannot access the environment of this frame." };
}
let youngest = this.youngestFrame;
// Put ourselves back in the running state and inform the client.
let resumedPacket = this._resumed();
this.conn.send(resumedPacket);
// Run the expression.
// XXX: test syntax errors
let completion = frame.eval(aRequest.expression);
// Put ourselves back in the pause state.
let packet = this._paused(youngest);
packet.why = { type: "clientEvaluated",
frameFinished: this.createProtocolCompletionValue(completion) };
// Return back to our previous pause's event loop.
return packet;
},
onFrames: function (aRequest) {
if (this.state !== "paused") {
return { error: "wrongState",
message: "Stack frames are only available while the debuggee is paused."};
}
let start = aRequest.start ? aRequest.start : 0;
let count = aRequest.count;
// Find the starting frame...
let frame = this.youngestFrame;
let i = 0;
while (frame && (i < start)) {
frame = frame.older;
i++;
}
// Return request.count frames, or all remaining
// frames if count is not defined.
let promises = [];
for (; frame && (!count || i < (start + count)); i++, frame = frame.older) {
let form = this._createFrameActor(frame).form();
form.depth = i;
let promise = this.sources.getOriginalLocation(new GeneratedLocation(
this.sources.createNonSourceMappedActor(frame.script.source),
form.where.line,
form.where.column
)).then((originalLocation) => {
if (!originalLocation.originalSourceActor) {
return null;
}
let sourceForm = originalLocation.originalSourceActor.form();
form.where = {
source: sourceForm,
line: originalLocation.originalLine,
column: originalLocation.originalColumn
};
form.source = sourceForm;
return form;
});
promises.push(promise);
}
return all(promises).then(function (frames) {
// Filter null values because sourcemapping may have failed.
return { frames: frames.filter(x => !!x) };
});
},
onReleaseMany: function (aRequest) {
if (!aRequest.actors) {
return { error: "missingParameter",
message: "no actors were specified" };
}
let res;
for (let actorID of aRequest.actors) {
let actor = this.threadLifetimePool.get(actorID);
if (!actor) {
if (!res) {
res = { error: "notReleasable",
message: "Only thread-lifetime actors can be released." };
}
continue;
}
actor.onRelease();
}
return res ? res : {};
},
/**
* Get the script and source lists from the debugger.
*/
_discoverSources: function () {
// Only get one script per Debugger.Source.
const sourcesToScripts = new Map();
const scripts = this.dbg.findScripts();
for (let i = 0, len = scripts.length; i < len; i++) {
let s = scripts[i];
if (s.source) {
sourcesToScripts.set(s.source, s);
}
}
return all([...sourcesToScripts.values()].map(script => {
return this.sources.createSourceActors(script.source);
}));
},
onSources: function (aRequest) {
return this._discoverSources().then(() => {
// No need to flush the new source packets here, as we are sending the
// list of sources out immediately and we don't need to invoke the
// overhead of an RDP packet for every source right now. Let the default
// timeout flush the buffered packets.
return {
sources: this.sources.iter().map(s => s.form())
};
});
},
/**
* Disassociate all breakpoint actors from their scripts and clear the
* breakpoint handlers. This method can be used when the thread actor intends
* to keep the breakpoint store, but needs to clear any actual breakpoints,
* e.g. due to a page navigation. This way the breakpoint actors' script
* caches won't hold on to the Debugger.Script objects leaking memory.
*/
disableAllBreakpoints: function () {
for (let bpActor of this.breakpointActorMap.findActors()) {
bpActor.removeScripts();
}
},
/**
* Handle a protocol request to pause the debuggee.
*/
onInterrupt: function (aRequest) {
if (this.state == "exited") {
return { type: "exited" };
} else if (this.state == "paused") {
// TODO: return the actual reason for the existing pause.
return { type: "paused", why: { type: "alreadyPaused" } };
} else if (this.state != "running") {
return { error: "wrongState",
message: "Received interrupt request in " + this.state +
" state." };
}
try {
// If execution should pause just before the next JavaScript bytecode is
// executed, just set an onEnterFrame handler.
if (aRequest.when == "onNext") {
let onEnterFrame = (aFrame) => {
return this._pauseAndRespond(aFrame, { type: "interrupted", onNext: true });
};
this.dbg.onEnterFrame = onEnterFrame;
return { type: "willInterrupt" };
}
// If execution should pause immediately, just put ourselves in the paused
// state.
let packet = this._paused();
if (!packet) {
return { error: "notInterrupted" };
}
packet.why = { type: "interrupted" };
// Send the response to the interrupt request now (rather than
// returning it), because we're going to start a nested event loop
// here.
this.conn.send(packet);
// Start a nested event loop.
this._pushThreadPause();
// We already sent a response to this request, don't send one
// now.
return null;
} catch (e) {
reportError(e);
return { error: "notInterrupted", message: e.toString() };
}
},
/**
* Handle a protocol request to retrieve all the event listeners on the page.
*/
onEventListeners: function (aRequest) {
// This request is only supported in content debugging.
if (!this.global) {
return {
error: "notImplemented",
message: "eventListeners request is only supported in content debugging"
};
}
let els = Cc["@mozilla.org/eventlistenerservice;1"]
.getService(Ci.nsIEventListenerService);
let nodes = this.global.document.getElementsByTagName("*");
nodes = [this.global].concat([].slice.call(nodes));
let listeners = [];
for (let node of nodes) {
let handlers = els.getListenerInfoFor(node);
for (let handler of handlers) {
// Create a form object for serializing the listener via the protocol.
let listenerForm = Object.create(null);
let listener = handler.listenerObject;
// Native event listeners don't provide any listenerObject or type and
// are not that useful to a JS debugger.
if (!listener || !handler.type) {
continue;
}
// There will be no tagName if the event listener is set on the window.
let selector = node.tagName ? findCssSelector(node) : "window";
let nodeDO = this.globalDebugObject.makeDebuggeeValue(node);
listenerForm.node = {
selector: selector,
object: createValueGrip(nodeDO, this._pausePool, this.objectGrip)
};
listenerForm.type = handler.type;
listenerForm.capturing = handler.capturing;
listenerForm.allowsUntrusted = handler.allowsUntrusted;
listenerForm.inSystemEventGroup = handler.inSystemEventGroup;
let handlerName = "on" + listenerForm.type;
listenerForm.isEventHandler = false;
if (typeof node.hasAttribute !== "undefined") {
listenerForm.isEventHandler = !!node.hasAttribute(handlerName);
}
if (!!node[handlerName]) {
listenerForm.isEventHandler = !!node[handlerName];
}
// Get the Debugger.Object for the listener object.
let listenerDO = this.globalDebugObject.makeDebuggeeValue(listener);
// If the listener is an object with a 'handleEvent' method, use that.
if (listenerDO.class == "Object" || listenerDO.class == "XULElement") {
// For some events we don't have permission to access the
// 'handleEvent' property when running in content scope.
if (!listenerDO.unwrap()) {
continue;
}
let heDesc;
while (!heDesc && listenerDO) {
heDesc = listenerDO.getOwnPropertyDescriptor("handleEvent");
listenerDO = listenerDO.proto;
}
if (heDesc && heDesc.value) {
listenerDO = heDesc.value;
}
}
// When the listener is a bound function, we are actually interested in
// the target function.
while (listenerDO.isBoundFunction) {
listenerDO = listenerDO.boundTargetFunction;
}
listenerForm.function = createValueGrip(listenerDO, this._pausePool,
this.objectGrip);
listeners.push(listenerForm);
}
}
return { listeners: listeners };
},
/**
* Return the Debug.Frame for a frame mentioned by the protocol.
*/
_requestFrame: function (aFrameID) {
if (!aFrameID) {
return this.youngestFrame;
}
if (this._framePool.has(aFrameID)) {
return this._framePool.get(aFrameID).frame;
}
return undefined;
},
_paused: function (aFrame) {
// We don't handle nested pauses correctly. Don't try - if we're
// paused, just continue running whatever code triggered the pause.
// We don't want to actually have nested pauses (although we
// have nested event loops). If code runs in the debuggee during
// a pause, it should cause the actor to resume (dropping
// pause-lifetime actors etc) and then repause when complete.
if (this.state === "paused") {
return undefined;
}
// Clear stepping hooks.
this.dbg.onEnterFrame = undefined;
this.dbg.onExceptionUnwind = undefined;
if (aFrame) {
aFrame.onStep = undefined;
aFrame.onPop = undefined;
}
// Clear DOM event breakpoints.
// XPCShell tests don't use actual DOM windows for globals and cause
// removeListenerForAllEvents to throw.
if (!isWorker && this.global && !this.global.toString().includes("Sandbox")) {
let els = Cc["@mozilla.org/eventlistenerservice;1"]
.getService(Ci.nsIEventListenerService);
els.removeListenerForAllEvents(this.global, this._allEventsListener, true);
for (let [, bp] of this._hiddenBreakpoints) {
bp.delete();
}
this._hiddenBreakpoints.clear();
}
this._state = "paused";
// Create the actor pool that will hold the pause actor and its
// children.
assert(!this._pausePool, "No pause pool should exist yet");
this._pausePool = new ActorPool(this.conn);
this.conn.addActorPool(this._pausePool);
// Give children of the pause pool a quick link back to the
// thread...
this._pausePool.threadActor = this;
// Create the pause actor itself...
assert(!this._pauseActor, "No pause actor should exist yet");
this._pauseActor = new PauseActor(this._pausePool);
this._pausePool.addActor(this._pauseActor);
// Update the list of frames.
let poppedFrames = this._updateFrames();
// Send off the paused packet and spin an event loop.
let packet = { from: this.actorID,
type: "paused",
actor: this._pauseActor.actorID };
if (aFrame) {
packet.frame = this._createFrameActor(aFrame).form();
}
if (poppedFrames) {
packet.poppedFrames = poppedFrames;
}
return packet;
},
_resumed: function () {
this._state = "running";
// Drop the actors in the pause actor pool.
this.conn.removeActorPool(this._pausePool);
this._pausePool = null;
this._pauseActor = null;
return { from: this.actorID, type: "resumed" };
},
/**
* Expire frame actors for frames that have been popped.
*
* @returns A list of actor IDs whose frames have been popped.
*/
_updateFrames: function () {
let popped = [];
// Create the actor pool that will hold the still-living frames.
let framePool = new ActorPool(this.conn);
let frameList = [];
for (let frameActor of this._frameActors) {
if (frameActor.frame.live) {
framePool.addActor(frameActor);
frameList.push(frameActor);
} else {
popped.push(frameActor.actorID);
}
}
// Remove the old frame actor pool, this will expire
// any actors that weren't added to the new pool.
if (this._framePool) {
this.conn.removeActorPool(this._framePool);
}
this._frameActors = frameList;
this._framePool = framePool;
this.conn.addActorPool(framePool);
return popped;
},
_createFrameActor: function (aFrame) {
if (aFrame.actor) {
return aFrame.actor;
}
let actor = new FrameActor(aFrame, this);
this._frameActors.push(actor);
this._framePool.addActor(actor);
aFrame.actor = actor;
return actor;
},
/**
* Create and return an environment actor that corresponds to the provided
* Debugger.Environment.
* @param Debugger.Environment aEnvironment
* The lexical environment we want to extract.
* @param object aPool
* The pool where the newly-created actor will be placed.
* @return The EnvironmentActor for aEnvironment or undefined for host
* functions or functions scoped to a non-debuggee global.
*/
createEnvironmentActor: function (aEnvironment, aPool) {
if (!aEnvironment) {
return undefined;
}
if (aEnvironment.actor) {
return aEnvironment.actor;
}
let actor = new EnvironmentActor(aEnvironment, this);
aPool.addActor(actor);
aEnvironment.actor = actor;
return actor;
},
/**
* Return a protocol completion value representing the given
* Debugger-provided completion value.
*/
createProtocolCompletionValue: function (aCompletion) {
let protoValue = {};
if (aCompletion == null) {
protoValue.terminated = true;
} else if ("return" in aCompletion) {
protoValue.return = createValueGrip(aCompletion.return,
this._pausePool, this.objectGrip);
} else if ("throw" in aCompletion) {
protoValue.throw = createValueGrip(aCompletion.throw,
this._pausePool, this.objectGrip);
} else {
protoValue.return = createValueGrip(aCompletion.yield,
this._pausePool, this.objectGrip);
}
return protoValue;
},
/**
* Create a grip for the given debuggee object.
*
* @param aValue Debugger.Object
* The debuggee object value.
* @param aPool ActorPool
* The actor pool where the new object actor will be added.
*/
objectGrip: function (aValue, aPool) {
if (!aPool.objectActors) {
aPool.objectActors = new WeakMap();
}
if (aPool.objectActors.has(aValue)) {
return aPool.objectActors.get(aValue).grip();
} else if (this.threadLifetimePool.objectActors.has(aValue)) {
return this.threadLifetimePool.objectActors.get(aValue).grip();
}
let actor = new PauseScopedObjectActor(aValue, {
getGripDepth: () => this._gripDepth,
incrementGripDepth: () => this._gripDepth++,
decrementGripDepth: () => this._gripDepth--,
createValueGrip: v => createValueGrip(v, this._pausePool,
this.pauseObjectGrip),
sources: () => this.sources,
createEnvironmentActor: (env, pool) =>
this.createEnvironmentActor(env, pool),
promote: () => this.threadObjectGrip(actor),
isThreadLifetimePool: () =>
actor.registeredPool !== this.threadLifetimePool,
getGlobalDebugObject: () => this.globalDebugObject
});
aPool.addActor(actor);
aPool.objectActors.set(aValue, actor);
return actor.grip();
},
/**
* Create a grip for the given debuggee object with a pause lifetime.
*
* @param aValue Debugger.Object
* The debuggee object value.
*/
pauseObjectGrip: function (aValue) {
if (!this._pausePool) {
throw "Object grip requested while not paused.";
}
return this.objectGrip(aValue, this._pausePool);
},
/**
* Extend the lifetime of the provided object actor to thread lifetime.
*
* @param aActor object
* The object actor.
*/
threadObjectGrip: function (aActor) {
// We want to reuse the existing actor ID, so we just remove it from the
// current pool's weak map and then let pool.addActor do the rest.
aActor.registeredPool.objectActors.delete(aActor.obj);
this.threadLifetimePool.addActor(aActor);
this.threadLifetimePool.objectActors.set(aActor.obj, aActor);
},
/**
* Handle a protocol request to promote multiple pause-lifetime grips to
* thread-lifetime grips.
*
* @param aRequest object
* The protocol request object.
*/
onThreadGrips: function (aRequest) {
if (this.state != "paused") {
return { error: "wrongState" };
}
if (!aRequest.actors) {
return { error: "missingParameter",
message: "no actors were specified" };
}
for (let actorID of aRequest.actors) {
let actor = this._pausePool.get(actorID);
if (actor) {
this.threadObjectGrip(actor);
}
}
return {};
},
/**
* Create a long string grip that is scoped to a pause.
*
* @param aString String
* The string we are creating a grip for.
*/
pauseLongStringGrip: function (aString) {
return longStringGrip(aString, this._pausePool);
},
/**
* Create a long string grip that is scoped to a thread.
*
* @param aString String
* The string we are creating a grip for.
*/
threadLongStringGrip: function (aString) {
return longStringGrip(aString, this._threadLifetimePool);
},
// JS Debugger API hooks.
/**
* A function that the engine calls when a call to a debug event hook,
* breakpoint handler, watchpoint handler, or similar function throws some
* exception.
*
* @param aException exception
* The exception that was thrown in the debugger code.
*/
uncaughtExceptionHook: function (aException) {
dumpn("Got an exception: " + aException.message + "\n" + aException.stack);
},
/**
* A function that the engine calls when a debugger statement has been
* executed in the specified frame.
*
* @param aFrame Debugger.Frame
* The stack frame that contained the debugger statement.
*/
onDebuggerStatement: function (aFrame) {
// Don't pause if we are currently stepping (in or over) or the frame is
// black-boxed.
const generatedLocation = this.sources.getFrameLocation(aFrame);
const { originalSourceActor } = this.unsafeSynchronize(this.sources.getOriginalLocation(
generatedLocation));
const url = originalSourceActor ? originalSourceActor.url : null;
return this.sources.isBlackBoxed(url) || aFrame.onStep
? undefined
: this._pauseAndRespond(aFrame, { type: "debuggerStatement" });
},
/**
* A function that the engine calls when an exception has been thrown and has
* propagated to the specified frame.
*
* @param aFrame Debugger.Frame
* The youngest remaining stack frame.
* @param aValue object
* The exception that was thrown.
*/
onExceptionUnwind: function (aFrame, aValue) {
let willBeCaught = false;
for (let frame = aFrame; frame != null; frame = frame.older) {
if (frame.script.isInCatchScope(frame.offset)) {
willBeCaught = true;
break;
}
}
if (willBeCaught && this._options.ignoreCaughtExceptions) {
return undefined;
}
// NS_ERROR_NO_INTERFACE exceptions are a special case in browser code,
// since they're almost always thrown by QueryInterface functions, and
// handled cleanly by native code.
if (aValue == Cr.NS_ERROR_NO_INTERFACE) {
return undefined;
}
const generatedLocation = this.sources.getFrameLocation(aFrame);
const { originalSourceActor } = this.unsafeSynchronize(this.sources.getOriginalLocation(
generatedLocation));
const url = originalSourceActor ? originalSourceActor.url : null;
if (this.sources.isBlackBoxed(url)) {
return undefined;
}
try {
let packet = this._paused(aFrame);
if (!packet) {
return undefined;
}
packet.why = { type: "exception",
exception: createValueGrip(aValue, this._pausePool,
this.objectGrip)
};
this.conn.send(packet);
this._pushThreadPause();
} catch (e) {
reportError(e, "Got an exception during TA_onExceptionUnwind: ");
}
return undefined;
},
/**
* A function that the engine calls when a new script has been loaded into the
* scope of the specified debuggee global.
*
* @param aScript Debugger.Script
* The source script that has been loaded into a debuggee compartment.
* @param aGlobal Debugger.Object
* A Debugger.Object instance whose referent is the global object.
*/
onNewScript: function (aScript, aGlobal) {
this._addSource(aScript.source);
},
/**
* A function called when there's a new or updated source from a thread actor's
* sources. Emits `newSource` and `updatedSource` on the tab actor.
*
* @param {String} name
* @param {SourceActor} source
*/
onSourceEvent: function (name, source) {
this.conn.send({
from: this._parent.actorID,
type: name,
source: source.form()
});
// For compatibility and debugger still using `newSource` on the thread client,
// still emit this event here. Clean up in bug 1247084
if (name === "newSource") {
this.conn.send({
from: this.actorID,
type: name,
source: source.form()
});
}
},
/**
* Add the provided source to the server cache.
*
* @param aSource Debugger.Source
* The source that will be stored.
* @returns true, if the source was added; false otherwise.
*/
_addSource: function (aSource) {
if (!this.sources.allowSource(aSource) || this._debuggerSourcesSeen.has(aSource)) {
return false;
}
let sourceActor = this.sources.createNonSourceMappedActor(aSource);
let bpActors = [...this.breakpointActorMap.findActors()];
if (this._options.useSourceMaps) {
let promises = [];
// Go ahead and establish the source actors for this script, which
// fetches sourcemaps if available and sends onNewSource
// notifications.
let sourceActorsCreated = this.sources._createSourceMappedActors(aSource);
if (bpActors.length) {
// We need to use unsafeSynchronize here because if the page is being reloaded,
// this call will replace the previous set of source actors for this source
// with a new one. If the source actors have not been replaced by the time
// we try to reset the breakpoints below, their location objects will still
// point to the old set of source actors, which point to different
// scripts.
this.unsafeSynchronize(sourceActorsCreated);
}
for (let _actor of bpActors) {
// XXX bug 1142115: We do async work in here, so we need to create a fresh
// binding because for/of does not yet do that in SpiderMonkey.
let actor = _actor;
if (actor.isPending) {
promises.push(actor.originalLocation.originalSourceActor._setBreakpoint(actor));
} else {
promises.push(this.sources.getAllGeneratedLocations(actor.originalLocation)
.then((generatedLocations) => {
if (generatedLocations.length > 0 &&
generatedLocations[0].generatedSourceActor.actorID === sourceActor.actorID) {
sourceActor._setBreakpointAtAllGeneratedLocations(actor, generatedLocations);
}
}));
}
}
if (promises.length > 0) {
this.unsafeSynchronize(promise.all(promises));
}
} else {
// Bug 1225160: If addSource is called in response to a new script
// notification, and this notification was triggered by loading a JSM from
// chrome code, calling unsafeSynchronize could cause a debuggee timer to
// fire. If this causes the JSM to be loaded a second time, the browser
// will crash, because loading JSMS is not reentrant, and the first load
// has not completed yet.
//
// The root of the problem is that unsafeSynchronize can cause debuggee
// code to run. Unfortunately, fixing that is prohibitively difficult. The
// best we can do at the moment is disable source maps for the browser
// debugger, and carefully avoid the use of unsafeSynchronize in this
// function when source maps are disabled.
for (let actor of bpActors) {
if (actor.isPending) {
actor.originalLocation.originalSourceActor._setBreakpoint(actor);
} else {
actor.originalLocation.originalSourceActor._setBreakpointAtGeneratedLocation(
actor, GeneratedLocation.fromOriginalLocation(actor.originalLocation)
);
}
}
}
this._debuggerSourcesSeen.add(aSource);
return true;
},
/**
* Get prototypes and properties of multiple objects.
*/
onPrototypesAndProperties: function (aRequest) {
let result = {};
for (let actorID of aRequest.actors) {
// This code assumes that there are no lazily loaded actors returned
// by this call.
let actor = this.conn.getActor(actorID);
if (!actor) {
return { from: this.actorID,
error: "noSuchActor" };
}
let handler = actor.onPrototypeAndProperties;
if (!handler) {
return { from: this.actorID,
error: "unrecognizedPacketType",
message: ('Actor "' + actorID +
'" does not recognize the packet type ' +
'"prototypeAndProperties"') };
}
result[actorID] = handler.call(actor, {});
}
return { from: this.actorID,
actors: result };
}
});
ThreadActor.prototype.requestTypes = object.merge(ThreadActor.prototype.requestTypes, {
"attach": ThreadActor.prototype.onAttach,
"detach": ThreadActor.prototype.onDetach,
"reconfigure": ThreadActor.prototype.onReconfigure,
"resume": ThreadActor.prototype.onResume,
"clientEvaluate": ThreadActor.prototype.onClientEvaluate,
"frames": ThreadActor.prototype.onFrames,
"interrupt": ThreadActor.prototype.onInterrupt,
"eventListeners": ThreadActor.prototype.onEventListeners,
"releaseMany": ThreadActor.prototype.onReleaseMany,
"sources": ThreadActor.prototype.onSources,
"threadGrips": ThreadActor.prototype.onThreadGrips,
"prototypesAndProperties": ThreadActor.prototype.onPrototypesAndProperties
});
exports.ThreadActor = ThreadActor;
/**
* Creates a PauseActor.
*
* PauseActors exist for the lifetime of a given debuggee pause. Used to
* scope pause-lifetime grips.
*
* @param ActorPool aPool
* The actor pool created for this pause.
*/
function PauseActor(aPool)
{
this.pool = aPool;
}
PauseActor.prototype = {
actorPrefix: "pause"
};
/**
* A base actor for any actors that should only respond receive messages in the
* paused state. Subclasses may expose a `threadActor` which is used to help
* determine when we are in a paused state. Subclasses should set their own
* "constructor" property if they want better error messages. You should never
* instantiate a PauseScopedActor directly, only through subclasses.
*/
function PauseScopedActor()
{
}
/**
* A function decorator for creating methods to handle protocol messages that
* should only be received while in the paused state.
*
* @param aMethod Function
* The function we are decorating.
*/
PauseScopedActor.withPaused = function (aMethod) {
return function () {
if (this.isPaused()) {
return aMethod.apply(this, arguments);
} else {
return this._wrongState();
}
};
};
PauseScopedActor.prototype = {
/**
* Returns true if we are in the paused state.
*/
isPaused: function () {
// When there is not a ThreadActor available (like in the webconsole) we
// have to be optimistic and assume that we are paused so that we can
// respond to requests.
return this.threadActor ? this.threadActor.state === "paused" : true;
},
/**
* Returns the wrongState response packet for this actor.
*/
_wrongState: function () {
return {
error: "wrongState",
message: this.constructor.name +
" actors can only be accessed while the thread is paused."
};
}
};
/**
* Creates a pause-scoped actor for the specified object.
* @see ObjectActor
*/
function PauseScopedObjectActor(obj, hooks) {
ObjectActor.call(this, obj, hooks);
this.hooks.promote = hooks.promote;
this.hooks.isThreadLifetimePool = hooks.isThreadLifetimePool;
}
PauseScopedObjectActor.prototype = Object.create(PauseScopedActor.prototype);
Object.assign(PauseScopedObjectActor.prototype, ObjectActor.prototype);
Object.assign(PauseScopedObjectActor.prototype, {
constructor: PauseScopedObjectActor,
actorPrefix: "pausedobj",
onOwnPropertyNames:
PauseScopedActor.withPaused(ObjectActor.prototype.onOwnPropertyNames),
onPrototypeAndProperties:
PauseScopedActor.withPaused(ObjectActor.prototype.onPrototypeAndProperties),
onPrototype: PauseScopedActor.withPaused(ObjectActor.prototype.onPrototype),
onProperty: PauseScopedActor.withPaused(ObjectActor.prototype.onProperty),
onDecompile: PauseScopedActor.withPaused(ObjectActor.prototype.onDecompile),
onDisplayString:
PauseScopedActor.withPaused(ObjectActor.prototype.onDisplayString),
onParameterNames:
PauseScopedActor.withPaused(ObjectActor.prototype.onParameterNames),
/**
* Handle a protocol request to promote a pause-lifetime grip to a
* thread-lifetime grip.
*
* @param aRequest object
* The protocol request object.
*/
onThreadGrip: PauseScopedActor.withPaused(function (aRequest) {
this.hooks.promote();
return {};
}),
/**
* Handle a protocol request to release a thread-lifetime grip.
*
* @param aRequest object
* The protocol request object.
*/
onRelease: PauseScopedActor.withPaused(function (aRequest) {
if (this.hooks.isThreadLifetimePool()) {
return { error: "notReleasable",
message: "Only thread-lifetime actors can be released." };
}
this.release();
return {};
}),
});
Object.assign(PauseScopedObjectActor.prototype.requestTypes, {
"threadGrip": PauseScopedObjectActor.prototype.onThreadGrip,
});
function hackDebugger(Debugger) {
// TODO: Improve native code instead of hacking on top of it
/**
* Override the toString method in order to get more meaningful script output
* for debugging the debugger.
*/
Debugger.Script.prototype.toString = function () {
if (this.type == "wasm") {
return "[wasm]";
}
let output = "";
if (this.url) {
output += this.url;
}
if (typeof this.staticLevel != "undefined") {
output += ":L" + this.staticLevel;
}
if (typeof this.startLine != "undefined") {
output += ":" + this.startLine;
if (this.lineCount && this.lineCount > 1) {
output += "-" + (this.startLine + this.lineCount - 1);
}
}
if (typeof this.startLine != "undefined") {
output += ":" + this.startLine;
if (this.lineCount && this.lineCount > 1) {
output += "-" + (this.startLine + this.lineCount - 1);
}
}
if (this.strictMode) {
output += ":strict";
}
return output;
};
/**
* Helper property for quickly getting to the line number a stack frame is
* currently paused at.
*/
Object.defineProperty(Debugger.Frame.prototype, "line", {
configurable: true,
get: function () {
if (this.script) {
return this.script.getOffsetLocation(this.offset).lineNumber;
} else {
return null;
}
}
});
}
/**
* Creates an actor for handling chrome debugging. ChromeDebuggerActor is a
* thin wrapper over ThreadActor, slightly changing some of its behavior.
*
* @param aConnection object
* The DebuggerServerConnection with which this ChromeDebuggerActor
* is associated. (Currently unused, but required to make this
* constructor usable with addGlobalActor.)
*
* @param aParent object
* This actor's parent actor. See ThreadActor for a list of expected
* properties.
*/
function ChromeDebuggerActor(aConnection, aParent)
{
ThreadActor.prototype.initialize.call(this, aParent);
}
ChromeDebuggerActor.prototype = Object.create(ThreadActor.prototype);
Object.assign(ChromeDebuggerActor.prototype, {
constructor: ChromeDebuggerActor,
// A constant prefix that will be used to form the actor ID by the server.
actorPrefix: "chromeDebugger"
});
exports.ChromeDebuggerActor = ChromeDebuggerActor;
/**
* Creates an actor for handling add-on debugging. AddonThreadActor is
* a thin wrapper over ThreadActor.
*
* @param aConnection object
* The DebuggerServerConnection with which this AddonThreadActor
* is associated. (Currently unused, but required to make this
* constructor usable with addGlobalActor.)
*
* @param aParent object
* This actor's parent actor. See ThreadActor for a list of expected
* properties.
*/
function AddonThreadActor(aConnect, aParent) {
ThreadActor.prototype.initialize.call(this, aParent);
}
AddonThreadActor.prototype = Object.create(ThreadActor.prototype);
Object.assign(AddonThreadActor.prototype, {
constructor: AddonThreadActor,
// A constant prefix that will be used to form the actor ID by the server.
actorPrefix: "addonThread"
});
exports.AddonThreadActor = AddonThreadActor;
// Utility functions.
/**
* Report the given error in the error console and to stdout.
*
* @param Error aError
* The error object you wish to report.
* @param String aPrefix
* An optional prefix for the reported error message.
*/
var oldReportError = reportError;
reportError = function (aError, aPrefix = "") {
assert(aError instanceof Error, "Must pass Error objects to reportError");
let msg = aPrefix + aError.message + ":\n" + aError.stack;
oldReportError(msg);
dumpn(msg);
};
/**
* Find the scripts which contain offsets that are an entry point to the given
* line.
*
* @param Array scripts
* The set of Debugger.Scripts to consider.
* @param Number line
* The line we are searching for entry points into.
* @returns Array of objects of the form { script, offsets } where:
* - script is a Debugger.Script
* - offsets is an array of offsets that are entry points into the
* given line.
*/
function findEntryPointsForLine(scripts, line) {
const entryPoints = [];
for (let script of scripts) {
const offsets = script.getLineOffsets(line);
if (offsets.length) {
entryPoints.push({ script, offsets });
}
}
return entryPoints;
}
/**
* Unwrap a global that is wrapped in a |Debugger.Object|, or if the global has
* become a dead object, return |undefined|.
*
* @param Debugger.Object wrappedGlobal
* The |Debugger.Object| which wraps a global.
*
* @returns {Object|undefined}
* Returns the unwrapped global object or |undefined| if unwrapping
* failed.
*/
exports.unwrapDebuggerObjectGlobal = wrappedGlobal => {
try {
// Because of bug 991399 we sometimes get nuked window references here. We
// just bail out in that case.
//
// Note that addon sandboxes have a DOMWindow as their prototype. So make
// sure that we can touch the prototype too (whatever it is), in case _it_
// is it a nuked window reference. We force stringification to make sure
// that any dead object proxies make themselves known.
let global = wrappedGlobal.unsafeDereference();
Object.getPrototypeOf(global) + "";
return global;
}
catch (e) {
return undefined;
}
};