diff options
Diffstat (limited to 'devtools/server/actors/breakpoint.js')
-rw-r--r-- | devtools/server/actors/breakpoint.js | 232 |
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; |