summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/breakpoint.js
blob: bfa563bf5587a5cacb321dc34f9045941ceb1b0b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
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) {
    // 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;