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

251 строка
7.4 KiB
JavaScript

/* 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/. */
/* global assert */
"use strict";
const {
logEvent,
getThrownMessage,
} = require("devtools/server/actors/utils/logEvent");
/**
* Set breakpoints on all the given entry points with the given
* BreakpointActor as the handler.
*
* @param BreakpointActor actor
* The actor handling the breakpoint hits.
* @param Array entryPoints
* An array of objects of the form `{ script, offsets }`.
*/
function setBreakpointAtEntryPoints(actor, entryPoints) {
for (const { script, offsets } of entryPoints) {
actor.addScript(script, offsets);
}
}
exports.setBreakpointAtEntryPoints = setBreakpointAtEntryPoints;
/**
* BreakpointActors are instantiated for each breakpoint that has been installed
* by the client. They are not true actors and do not communicate with the
* client directly, but encapsulate the DebuggerScript locations where the
* breakpoint is installed.
*/
function BreakpointActor(threadActor, location) {
// A map from Debugger.Script instances to the offsets which the breakpoint
// has been set for in that script.
this.scripts = new Map();
this.threadActor = threadActor;
this.location = location;
this.options = null;
}
BreakpointActor.prototype = {
setOptions(options) {
for (const [script, offsets] of this.scripts) {
this._newOffsetsOrOptions(script, offsets, this.options, options);
}
this.options = options;
},
destroy: function() {
this.removeScripts();
},
hasScript: function(script) {
return this.scripts.has(script);
},
/**
* Called when this same breakpoint is added to another Debugger.Script
* instance.
*
* @param script Debugger.Script
* The new source script on which the breakpoint has been set.
* @param offsets Array
* Any offsets in the script the breakpoint is associated with.
*/
addScript: function(script, offsets) {
this.scripts.set(script, offsets.concat(this.scripts.get(offsets) || []));
this._newOffsetsOrOptions(script, offsets, null, this.options);
},
/**
* Remove the breakpoints from associated scripts and clear the script cache.
*/
removeScripts: function() {
for (const [script] of this.scripts) {
script.clearBreakpoint(this);
}
this.scripts.clear();
},
/**
* Called on changes to this breakpoint's script offsets or options.
*/
_newOffsetsOrOptions(script, offsets, oldOptions, options) {
// When replaying, logging breakpoints are handled using an API to get logged
// messages from throughout the recording.
if (this.threadActor.dbg.replaying && options.logValue) {
if (
oldOptions &&
oldOptions.logValue == options.logValue &&
oldOptions.condition == options.condition
) {
return;
}
for (const offset of offsets) {
const { lineNumber, columnNumber } = script.getOffsetLocation(offset);
script.replayVirtualConsoleLog(
offset,
options.logValue,
options.condition,
(executionPoint, rv) => {
const message = {
filename: script.url,
lineNumber,
columnNumber,
executionPoint,
arguments: rv,
logpointId: options.logGroupId,
};
this.threadActor._parent._consoleActor.onConsoleAPICall(message);
}
);
}
// Treat `displayName` breakpoints as standard breakpoints
if (options.logValue != "displayName") {
return;
}
}
// In all other cases, this is used as a script breakpoint handler.
// Clear any existing handler first in case this is called multiple times
// after options change.
for (const offset of offsets) {
script.clearBreakpoint(this, offset);
script.setBreakpoint(offset, this);
}
},
/**
* Check if this breakpoint has a condition that doesn't error and
* evaluates to true in frame.
*
* @param frame Debugger.Frame
* The frame to evaluate the condition in
* @returns Object
* - result: boolean|undefined
* True when the conditional breakpoint should trigger a pause,
* false otherwise. If the condition evaluation failed/killed,
* `result` will be `undefined`.
* - message: string
* If the condition throws, this is the thrown message.
*/
checkCondition: function(frame, condition) {
const completion = frame.eval(condition);
if (completion) {
if (completion.throw) {
// The evaluation failed and threw
return {
result: true,
message: getThrownMessage(completion),
};
} else if (completion.yield) {
assert(false, "Shouldn't ever get yield completions from an eval");
} else {
return { result: !!completion.return };
}
}
// The evaluation was killed (possibly by the slow script dialog)
return { result: undefined };
},
/**
* A function that the engine calls when a breakpoint has been hit.
*
* @param frame Debugger.Frame
* The stack frame that contained the breakpoint.
*/
/* eslint-disable complexity */
hit: function(frame) {
// Don't pause if we are currently stepping (in or over) or the frame is
// black-boxed.
const location = this.threadActor.sources.getFrameLocation(frame);
const { sourceActor, line, column } = location;
if (
this.threadActor.sources.isBlackBoxed(sourceActor.url, line, column) ||
this.threadActor.skipBreakpoints
) {
return undefined;
}
// If we're trying to pop this frame, and we see a breakpoint at
// the spot at which popping started, ignore it. See bug 970469.
const locationAtFinish = frame.onPop && frame.onPop.location;
if (
locationAtFinish &&
locationAtFinish.line === line &&
locationAtFinish.column === column
) {
return undefined;
}
if (!this.threadActor.hasMoved(frame, "breakpoint")) {
return undefined;
}
const reason = { type: "breakpoint", actors: [this.actorID] };
const { condition, logValue } = this.options || {};
if (condition) {
const { result, message } = this.checkCondition(frame, condition);
// Don't pause if the result is falsey
if (!result) {
return undefined;
}
if (message) {
// Don't pause if there is an exception message and POE is false
if (!this.threadActor._options.pauseOnExceptions) {
return undefined;
}
reason.type = "breakpointConditionThrown";
reason.message = message;
}
}
// Replay logpoints are handled in _newOffsetsOrOptions
if (logValue && !this.threadActor.dbg.replaying) {
return logEvent({
threadActor: this.threadActor,
frame,
level: "logPoint",
expression: `[${logValue}]`,
});
}
return this.threadActor._pauseAndRespond(frame, reason);
},
/* eslint-enable complexity */
delete: function() {
// Remove from the breakpoint store.
this.threadActor.breakpointActorMap.deleteActor(this.location);
this.threadActor.threadLifetimePool.removeActor(this);
// Remove the actual breakpoint from the associated scripts.
this.removeScripts();
},
};
exports.BreakpointActor = BreakpointActor;