Bug 1795082 - Part 2/2: Drop post-processing from getURL() r=zombie
[gecko.git] / devtools / server / actors / breakpoint.js
blobbfa563bf5587a5cacb321dc34f9045941ceb1b0b
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/. */
5 /* global assert */
7 "use strict";
9 const {
10   logEvent,
11   getThrownMessage,
12 } = require("resource://devtools/server/actors/utils/logEvent.js");
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   }
29 exports.setBreakpointAtEntryPoints = setBreakpointAtEntryPoints;
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();
43     this.threadActor = threadActor;
44     this.location = location;
45     this.options = null;
46   }
48   setOptions(options) {
49     const oldOptions = this.options;
50     this.options = options;
52     for (const [script, offsets] of this.scripts) {
53       this._newOffsetsOrOptions(script, offsets, oldOptions);
54     }
55   }
57   destroy() {
58     this.removeScripts();
59     this.options = null;
60   }
62   hasScript(script) {
63     return this.scripts.has(script);
64   }
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   }
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   }
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     }
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   }
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;
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   }
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     }
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     }
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     }
190     if (!this.threadActor.hasMoved(frame, "breakpoint")) {
191       return undefined;
192     }
194     const reason = { type: "breakpoint", actors: [this.actorID] };
195     const { condition, logValue } = this.options || {};
197     if (condition) {
198       const { result, message } = this.checkCondition(frame, condition);
200       // Don't pause if the result is falsey
201       if (!result) {
202         return undefined;
203       }
205       if (message) {
206         reason.type = "breakpointConditionThrown";
207         reason.message = message;
208       }
209     }
211     if (logValue) {
212       return logEvent({
213         threadActor: this.threadActor,
214         frame,
215         level: "logPoint",
216         expression: `[${logValue}]`,
217       });
218     }
220     return this.threadActor._pauseAndRespond(frame, reason);
221   }
223   delete() {
224     // Remove from the breakpoint store.
225     this.threadActor.breakpointActorMap.deleteActor(this.location);
226     // Remove the actual breakpoint from the associated scripts.
227     this.removeScripts();
228     this.destroy();
229   }
232 exports.BreakpointActor = BreakpointActor;