summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/breakpoint.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/breakpoint.js')
-rw-r--r--devtools/server/actors/breakpoint.js232
1 files changed, 232 insertions, 0 deletions
diff --git a/devtools/server/actors/breakpoint.js b/devtools/server/actors/breakpoint.js
new file mode 100644
index 0000000000..d1a469658c
--- /dev/null
+++ b/devtools/server/actors/breakpoint.js
@@ -0,0 +1,232 @@
+/* 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("resource://devtools/server/actors/utils/logEvent.js");
+
+/**
+ * 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.
+ */
+class BreakpointActor {
+ constructor(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;
+ }
+
+ setOptions(options) {
+ const oldOptions = this.options;
+ this.options = options;
+
+ for (const [script, offsets] of this.scripts) {
+ this._newOffsetsOrOptions(script, offsets, oldOptions);
+ }
+ }
+
+ destroy() {
+ this.removeScripts();
+ this.options = null;
+ }
+
+ hasScript(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(script, offsets) {
+ this.scripts.set(script, offsets.concat(this.scripts.get(offsets) || []));
+ this._newOffsetsOrOptions(script, offsets, null);
+ }
+
+ /**
+ * Remove the breakpoints from associated scripts and clear the script cache.
+ */
+ removeScripts() {
+ 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) {
+ // Clear any existing handler first in case this is called multiple times
+ // after options change.
+ for (const offset of offsets) {
+ script.clearBreakpoint(this, offset);
+ }
+
+ // In all other cases, this is used as a script breakpoint handler.
+ for (const offset of offsets) {
+ 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(frame, condition) {
+ // Ensure disabling breakpoint while evaluating the condition.
+ // All but exception breakpoint to report any exception when running the condition.
+ this.threadActor.insideClientEvaluation = {
+ disableBreaks: true,
+ reportExceptionsWhenBreaksAreDisabled: true,
+ };
+ let completion;
+
+ // Temporarily enable pause on exception when evaluating the condition.
+ const hadToEnablePauseOnException =
+ !this.threadActor.isPauseOnExceptionsEnabled();
+ try {
+ if (hadToEnablePauseOnException) {
+ this.threadActor.setPauseOnExceptions(true);
+ }
+ completion = frame.eval(condition, { hideFromDebugger: true });
+ } finally {
+ this.threadActor.insideClientEvaluation = null;
+ if (hadToEnablePauseOnException) {
+ this.threadActor.setPauseOnExceptions(false);
+ }
+ }
+ 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-next-line complexity
+ hit(frame) {
+ if (this.threadActor.shouldSkipAnyBreakpoint) {
+ return undefined;
+ }
+
+ // Don't pause if we are currently stepping (in or over) or the frame is
+ // black-boxed.
+ const location = this.threadActor.sourcesManager.getFrameLocation(frame);
+ if (this.threadActor.sourcesManager.isFrameBlackBoxed(frame)) {
+ 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?.location;
+ if (
+ locationAtFinish &&
+ locationAtFinish.line === location.line &&
+ locationAtFinish.column === location.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) {
+ reason.type = "breakpointConditionThrown";
+ reason.message = message;
+ }
+ }
+
+ if (logValue) {
+ return logEvent({
+ threadActor: this.threadActor,
+ frame,
+ level: "logPoint",
+ expression: `[${logValue}]`,
+ });
+ }
+
+ return this.threadActor._pauseAndRespond(frame, reason);
+ }
+
+ delete() {
+ // Remove from the breakpoint store.
+ this.threadActor.breakpointActorMap.deleteActor(this.location);
+ // Remove the actual breakpoint from the associated scripts.
+ this.removeScripts();
+ this.destroy();
+ }
+}
+
+exports.BreakpointActor = BreakpointActor;