breakpoint.js (6991B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* global assert */ 6 7 "use strict"; 8 9 const { 10 evalAndLogEvent, 11 getThrownMessage, 12 } = require("resource://devtools/server/actors/utils/logEvent.js"); 13 14 /** 15 * Set breakpoints on all the given entry points with the given 16 * BreakpointActor as the handler. 17 * 18 * @param BreakpointActor actor 19 * The actor handling the breakpoint hits. 20 * @param Array entryPoints 21 * An array of objects of the form `{ script, offsets }`. 22 */ 23 function setBreakpointAtEntryPoints(actor, entryPoints) { 24 for (const { script, offsets } of entryPoints) { 25 actor.addScript(script, offsets); 26 } 27 } 28 29 exports.setBreakpointAtEntryPoints = setBreakpointAtEntryPoints; 30 31 /** 32 * BreakpointActors are instantiated for each breakpoint that has been installed 33 * by the client. They are not true actors and do not communicate with the 34 * client directly, but encapsulate the DebuggerScript locations where the 35 * breakpoint is installed. 36 */ 37 class BreakpointActor { 38 constructor(threadActor, location) { 39 // A map from Debugger.Script instances to the offsets which the breakpoint 40 // has been set for in that script. 41 this.scripts = new Map(); 42 43 this.threadActor = threadActor; 44 this.location = location; 45 this.options = null; 46 } 47 48 setOptions(options) { 49 const oldOptions = this.options; 50 this.options = options; 51 52 for (const [script, offsets] of this.scripts) { 53 this._newOffsetsOrOptions(script, offsets, oldOptions); 54 } 55 } 56 57 destroy() { 58 this.removeScripts(); 59 this.options = null; 60 } 61 62 hasScript(script) { 63 return this.scripts.has(script); 64 } 65 66 /** 67 * Called when this same breakpoint is added to another Debugger.Script 68 * instance. 69 * 70 * @param script Debugger.Script 71 * The new source script on which the breakpoint has been set. 72 * @param offsets Array 73 * Any offsets in the script the breakpoint is associated with. 74 */ 75 addScript(script, offsets) { 76 this.scripts.set(script, offsets.concat(this.scripts.get(offsets) || [])); 77 this._newOffsetsOrOptions(script, offsets, null); 78 } 79 80 /** 81 * Remove the breakpoints from associated scripts and clear the script cache. 82 */ 83 removeScripts() { 84 for (const [script] of this.scripts) { 85 script.clearBreakpoint(this); 86 } 87 this.scripts.clear(); 88 } 89 90 /** 91 * Called on changes to this breakpoint's script offsets or options. 92 */ 93 _newOffsetsOrOptions(script, offsets) { 94 // Clear any existing handler first in case this is called multiple times 95 // after options change. 96 for (const offset of offsets) { 97 script.clearBreakpoint(this, offset); 98 } 99 100 // In all other cases, this is used as a script breakpoint handler. 101 for (const offset of offsets) { 102 script.setBreakpoint(offset, this); 103 } 104 } 105 106 /** 107 * Check if this breakpoint has a condition that doesn't error and 108 * evaluates to true in frame. 109 * 110 * @param frame Debugger.Frame 111 * The frame to evaluate the condition in 112 * @returns Object 113 * - result: boolean|undefined 114 * True when the conditional breakpoint should trigger a pause, 115 * false otherwise. If the condition evaluation failed/killed, 116 * `result` will be `undefined`. 117 * - message: string 118 * If the condition throws, this is the thrown message. 119 */ 120 checkCondition(frame, condition) { 121 // Ensure disabling breakpoint while evaluating the condition. 122 // All but exception breakpoint to report any exception when running the condition. 123 this.threadActor.insideClientEvaluation = { 124 disableBreaks: true, 125 reportExceptionsWhenBreaksAreDisabled: true, 126 }; 127 let completion; 128 129 // Temporarily enable pause on exception when evaluating the condition. 130 const hadToEnablePauseOnException = 131 !this.threadActor.isPauseOnExceptionsEnabled(); 132 try { 133 if (hadToEnablePauseOnException) { 134 this.threadActor.setPauseOnExceptions(true); 135 } 136 completion = frame.eval(condition, { hideFromDebugger: true }); 137 } finally { 138 this.threadActor.insideClientEvaluation = null; 139 if (hadToEnablePauseOnException) { 140 this.threadActor.setPauseOnExceptions(false); 141 } 142 } 143 if (completion) { 144 if (completion.throw) { 145 // The evaluation failed and threw 146 return { 147 result: true, 148 message: getThrownMessage(completion), 149 }; 150 } else if (completion.yield) { 151 assert(false, "Shouldn't ever get yield completions from an eval"); 152 } else { 153 return { result: !!completion.return }; 154 } 155 } 156 // The evaluation was killed (possibly by the slow script dialog) 157 return { result: undefined }; 158 } 159 160 /** 161 * A function that the engine calls when a breakpoint has been hit. 162 * 163 * @param frame Debugger.Frame 164 * The stack frame that contained the breakpoint. 165 */ 166 // eslint-disable-next-line complexity 167 hit(frame) { 168 if (this.threadActor.shouldSkipAnyBreakpoint) { 169 return undefined; 170 } 171 172 // Don't pause if we are currently stepping (in or over) or the frame is 173 // black-boxed. 174 const location = this.threadActor.sourcesManager.getFrameLocation(frame); 175 if (this.threadActor.sourcesManager.isFrameBlackBoxed(frame)) { 176 return undefined; 177 } 178 179 // If we're trying to pop this frame, and we see a breakpoint at 180 // the spot at which popping started, ignore it. See bug 970469. 181 const locationAtFinish = frame.onPop?.location; 182 if ( 183 locationAtFinish && 184 locationAtFinish.line === location.line && 185 locationAtFinish.column === location.column 186 ) { 187 return undefined; 188 } 189 190 if (!this.threadActor.hasMoved(frame, "breakpoint")) { 191 return undefined; 192 } 193 194 const reason = { type: "breakpoint", actors: [this.actorID] }; 195 const { condition, logValue } = this.options || {}; 196 197 if (condition) { 198 const { result, message } = this.checkCondition(frame, condition); 199 200 // Don't pause if the result is falsey 201 if (!result) { 202 return undefined; 203 } 204 205 if (message) { 206 reason.type = "breakpointConditionThrown"; 207 reason.message = message; 208 } 209 } 210 211 if (logValue) { 212 return evalAndLogEvent({ 213 threadActor: this.threadActor, 214 frame, 215 level: "logPoint", 216 expression: `[${logValue}]`, 217 showStacktrace: this.options.showStacktrace, 218 }); 219 } 220 221 return this.threadActor._pauseAndRespond(frame, reason); 222 } 223 224 delete() { 225 // Remove from the breakpoint store. 226 this.threadActor.breakpointActorMap.deleteActor(this.location); 227 // Remove the actual breakpoint from the associated scripts. 228 this.removeScripts(); 229 this.destroy(); 230 } 231 } 232 233 exports.BreakpointActor = BreakpointActor;