summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/thread.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /devtools/server/actors/thread.js
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/server/actors/thread.js')
-rw-r--r--devtools/server/actors/thread.js2324
1 files changed, 2324 insertions, 0 deletions
diff --git a/devtools/server/actors/thread.js b/devtools/server/actors/thread.js
new file mode 100644
index 0000000000..ecf79abfe5
--- /dev/null
+++ b/devtools/server/actors/thread.js
@@ -0,0 +1,2324 @@
+/* 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/. */
+
+"use strict";
+
+// protocol.js uses objects as exceptions in order to define
+// error packets.
+/* eslint-disable no-throw-literal */
+
+const { Actor } = require("resource://devtools/shared/protocol/Actor.js");
+const { Pool } = require("resource://devtools/shared/protocol/Pool.js");
+const { threadSpec } = require("resource://devtools/shared/specs/thread.js");
+
+const {
+ createValueGrip,
+} = require("resource://devtools/server/actors/object/utils.js");
+const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+const Debugger = require("Debugger");
+const { assert, dumpn, reportException } = DevToolsUtils;
+const {
+ getAvailableEventBreakpoints,
+ eventBreakpointForNotification,
+ eventsRequireNotifications,
+ firstStatementBreakpointId,
+ makeEventBreakpointMessage,
+} = require("resource://devtools/server/actors/utils/event-breakpoints.js");
+const {
+ WatchpointMap,
+} = require("resource://devtools/server/actors/utils/watchpoint-map.js");
+
+const {
+ logEvent,
+} = require("resource://devtools/server/actors/utils/logEvent.js");
+
+loader.lazyRequireGetter(
+ this,
+ "EnvironmentActor",
+ "resource://devtools/server/actors/environment.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "BreakpointActorMap",
+ "resource://devtools/server/actors/utils/breakpoint-actor-map.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "PauseScopedObjectActor",
+ "resource://devtools/server/actors/pause-scoped.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "EventLoop",
+ "resource://devtools/server/actors/utils/event-loop.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ ["FrameActor", "getSavedFrameParent", "isValidSavedFrame"],
+ "resource://devtools/server/actors/frame.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "HighlighterEnvironment",
+ "resource://devtools/server/actors/highlighters.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "PausedDebuggerOverlay",
+ "resource://devtools/server/actors/highlighters/paused-debugger.js",
+ true
+);
+
+const PROMISE_REACTIONS = new WeakMap();
+function cacheReactionsForFrame(frame) {
+ if (frame.asyncPromise) {
+ const reactions = frame.asyncPromise.getPromiseReactions();
+ const existingReactions = PROMISE_REACTIONS.get(frame.asyncPromise);
+ if (
+ reactions.length &&
+ (!existingReactions || reactions.length > existingReactions.length)
+ ) {
+ PROMISE_REACTIONS.set(frame.asyncPromise, reactions);
+ }
+ }
+}
+
+function createStepForReactionTracking(onStep) {
+ return function () {
+ cacheReactionsForFrame(this);
+ return onStep ? onStep.apply(this, arguments) : undefined;
+ };
+}
+
+const getAsyncParentFrame = frame => {
+ if (!frame.asyncPromise) {
+ return null;
+ }
+
+ // We support returning Frame actors for frames that are suspended
+ // at an 'await', and here we want to walk upward to look for the first
+ // frame that will be resumed when the current frame's promise resolves.
+ let reactions =
+ PROMISE_REACTIONS.get(frame.asyncPromise) ||
+ frame.asyncPromise.getPromiseReactions();
+
+ while (true) {
+ // We loop here because we may have code like:
+ //
+ // async function inner(){ debugger; }
+ //
+ // async function outer() {
+ // await Promise.resolve().then(() => inner());
+ // }
+ //
+ // where we can see that when `inner` resolves, we will resume from
+ // `outer`, even though there is a layer of promises between, and
+ // that layer could be any number of promises deep.
+ if (!(reactions[0] instanceof Debugger.Object)) {
+ break;
+ }
+
+ reactions = reactions[0].getPromiseReactions();
+ }
+
+ if (reactions[0] instanceof Debugger.Frame) {
+ return reactions[0];
+ }
+ return null;
+};
+const RESTARTED_FRAMES = new WeakSet();
+
+// Thread actor possible states:
+const STATES = {
+ // Before ThreadActor.attach is called:
+ DETACHED: "detached",
+ // After the actor is destroyed:
+ EXITED: "exited",
+
+ // States possible in between DETACHED AND EXITED:
+ // Default state, when the thread isn't paused,
+ RUNNING: "running",
+ // When paused on any type of breakpoint, or, when the client requested an interrupt.
+ PAUSED: "paused",
+};
+exports.STATES = STATES;
+
+// Possible values for the `why.type` attribute in "paused" event
+const PAUSE_REASONS = {
+ ALREADY_PAUSED: "alreadyPaused",
+ INTERRUPTED: "interrupted", // Associated with why.onNext attribute
+ MUTATION_BREAKPOINT: "mutationBreakpoint", // Associated with why.mutationType and why.message attributes
+ DEBUGGER_STATEMENT: "debuggerStatement",
+ EXCEPTION: "exception",
+ XHR: "XHR",
+ EVENT_BREAKPOINT: "eventBreakpoint",
+ RESUME_LIMIT: "resumeLimit",
+};
+exports.PAUSE_REASONS = PAUSE_REASONS;
+
+class ThreadActor extends Actor {
+ /**
+ * Creates a ThreadActor.
+ *
+ * ThreadActors manage execution/inspection of debuggees.
+ *
+ * @param parent TargetActor
+ * This |ThreadActor|'s parent actor. i.e. one of the many Target actors.
+ * @param aGlobal object [optional]
+ * An optional (for content debugging only) reference to the content
+ * window.
+ */
+ constructor(parent, global) {
+ super(parent.conn, threadSpec);
+
+ this._state = STATES.DETACHED;
+ this._parent = parent;
+ this.global = global;
+ this._options = {
+ skipBreakpoints: false,
+ };
+ this._gripDepth = 0;
+ this._parentClosed = false;
+ this._observingNetwork = false;
+ this._frameActors = [];
+ this._xhrBreakpoints = [];
+
+ this._dbg = null;
+ this._threadLifetimePool = null;
+ this._activeEventPause = null;
+ this._pauseOverlay = null;
+ this._priorPause = null;
+
+ this._activeEventBreakpoints = new Set();
+ this._frameActorMap = new WeakMap();
+ this._debuggerSourcesSeen = new WeakSet();
+
+ // A Set of URLs string to watch for when new sources are found by
+ // the debugger instance.
+ this._onLoadBreakpointURLs = new Set();
+
+ // A WeakMap from Debugger.Frame to an exception value which will be ignored
+ // when deciding to pause if the value is thrown by the frame. When we are
+ // pausing on exceptions then we only want to pause when the youngest frame
+ // throws a particular exception, instead of for all older frames as well.
+ this._handledFrameExceptions = new WeakMap();
+
+ this._watchpointsMap = new WatchpointMap(this);
+
+ this.breakpointActorMap = new BreakpointActorMap(this);
+
+ this._nestedEventLoop = new EventLoop({
+ thread: this,
+ });
+
+ this.onNewSourceEvent = this.onNewSourceEvent.bind(this);
+
+ this.createCompletionGrip = this.createCompletionGrip.bind(this);
+ this.onDebuggerStatement = this.onDebuggerStatement.bind(this);
+ this.onNewScript = this.onNewScript.bind(this);
+ this.objectGrip = this.objectGrip.bind(this);
+ this.pauseObjectGrip = this.pauseObjectGrip.bind(this);
+ this._onOpeningRequest = this._onOpeningRequest.bind(this);
+ this._onNewDebuggee = this._onNewDebuggee.bind(this);
+ this._onExceptionUnwind = this._onExceptionUnwind.bind(this);
+ this._eventBreakpointListener = this._eventBreakpointListener.bind(this);
+ this._onWindowReady = this._onWindowReady.bind(this);
+ this._onWillNavigate = this._onWillNavigate.bind(this);
+ this._onNavigate = this._onNavigate.bind(this);
+
+ this._parent.on("window-ready", this._onWindowReady);
+ this._parent.on("will-navigate", this._onWillNavigate);
+ this._parent.on("navigate", this._onNavigate);
+
+ this._firstStatementBreakpoint = null;
+ this._debuggerNotificationObserver = new DebuggerNotificationObserver();
+ }
+
+ // Used by the ObjectActor to keep track of the depth of grip() calls.
+ _gripDepth = null;
+
+ get dbg() {
+ if (!this._dbg) {
+ this._dbg = this._parent.dbg;
+ // Keep the debugger disabled until a client attaches.
+ if (this._state === STATES.DETACHED) {
+ this._dbg.disable();
+ } else {
+ this._dbg.enable();
+ }
+ }
+ return this._dbg;
+ }
+
+ // Current state of the thread actor:
+ // - detached: state, before ThreadActor.attach is called,
+ // - exited: state, after the actor is destroyed,
+ // States possible in between these two states:
+ // - running: default state, when the thread isn't paused,
+ // - paused: state, when paused on any type of breakpoint, or, when the client requested an interrupt.
+ get state() {
+ return this._state;
+ }
+
+ // XXX: soon to be equivalent to !isDestroyed once the thread actor is initialized on target creation.
+ get attached() {
+ return this.state == STATES.RUNNING || this.state == STATES.PAUSED;
+ }
+
+ get threadLifetimePool() {
+ if (!this._threadLifetimePool) {
+ this._threadLifetimePool = new Pool(this.conn, "thread");
+ this._threadLifetimePool.objectActors = new WeakMap();
+ }
+ return this._threadLifetimePool;
+ }
+
+ getThreadLifetimeObject(raw) {
+ return this.threadLifetimePool.objectActors.get(raw);
+ }
+
+ createValueGrip(value) {
+ return createValueGrip(value, this.threadLifetimePool, this.objectGrip);
+ }
+
+ get sourcesManager() {
+ return this._parent.sourcesManager;
+ }
+
+ get breakpoints() {
+ return this._parent.breakpoints;
+ }
+
+ get youngestFrame() {
+ if (this.state != STATES.PAUSED) {
+ return null;
+ }
+ return this.dbg.getNewestFrame();
+ }
+
+ get skipBreakpointsOption() {
+ return (
+ this._options.skipBreakpoints ||
+ (this.insideClientEvaluation && this.insideClientEvaluation.eager)
+ );
+ }
+
+ isPaused() {
+ return this._state === STATES.PAUSED;
+ }
+
+ lastPausedPacket() {
+ return this._priorPause;
+ }
+
+ /**
+ * Remove all debuggees and clear out the thread's sources.
+ */
+ clearDebuggees() {
+ if (this._dbg) {
+ this.dbg.removeAllDebuggees();
+ }
+ }
+
+ /**
+ * Destroy the debugger and put the actor in the exited state.
+ *
+ * As part of destroy, we: clean up listeners, debuggees and
+ * clear actor pools associated with the lifetime of this actor.
+ */
+ destroy() {
+ dumpn("in ThreadActor.prototype.destroy");
+ if (this._state == STATES.PAUSED) {
+ this.doResume();
+ }
+
+ this.removeAllWatchpoints();
+ this._xhrBreakpoints = [];
+ this._updateNetworkObserver();
+
+ this._activeEventBreakpoints = new Set();
+ this._debuggerNotificationObserver.removeListener(
+ this._eventBreakpointListener
+ );
+
+ for (const global of this.dbg.getDebuggees()) {
+ try {
+ this._debuggerNotificationObserver.disconnect(
+ global.unsafeDereference()
+ );
+ } catch (e) {}
+ }
+
+ this._parent.off("window-ready", this._onWindowReady);
+ this._parent.off("will-navigate", this._onWillNavigate);
+ this._parent.off("navigate", this._onNavigate);
+
+ this.sourcesManager.off("newSource", this.onNewSourceEvent);
+ this.clearDebuggees();
+ this._threadLifetimePool.destroy();
+ this._threadLifetimePool = null;
+ this._dbg = null;
+ this._state = STATES.EXITED;
+
+ super.destroy();
+ }
+
+ /**
+ * Tells if the thread actor has been initialized/attached on target creation
+ * by the server codebase. (And not late, from the frontend, by the TargetMixinFront class)
+ */
+ isAttached() {
+ return !!this.alreadyAttached;
+ }
+
+ // Request handlers
+ attach(options) {
+ // Note that the client avoids trying to call attach if already attached.
+ // But just in case, avoid any possible duplicate call to attach.
+ if (this.alreadyAttached) {
+ return;
+ }
+
+ if (this.state === STATES.EXITED) {
+ throw {
+ error: "exited",
+ message: "threadActor has exited",
+ };
+ }
+
+ if (this.state !== STATES.DETACHED) {
+ throw {
+ error: "wrongState",
+ message: "Current state is " + this.state,
+ };
+ }
+
+ this.dbg.onDebuggerStatement = this.onDebuggerStatement;
+ this.dbg.onNewScript = this.onNewScript;
+ this.dbg.onNewDebuggee = this._onNewDebuggee;
+
+ this.sourcesManager.on("newSource", this.onNewSourceEvent);
+
+ this.reconfigure(options);
+
+ // Switch state from DETACHED to RUNNING
+ this._state = STATES.RUNNING;
+
+ this.alreadyAttached = true;
+ this.dbg.enable();
+
+ // Notify the parent that we've finished attaching. If this is a worker
+ // thread which was paused until attaching, this will allow content to
+ // begin executing.
+ if (this._parent.onThreadAttached) {
+ this._parent.onThreadAttached();
+ }
+ if (Services.obs) {
+ // Set a wrappedJSObject property so |this| can be sent via the observer service
+ // for the xpcshell harness.
+ this.wrappedJSObject = this;
+ Services.obs.notifyObservers(this, "devtools-thread-ready");
+ }
+ }
+
+ toggleEventLogging(logEventBreakpoints) {
+ this._options.logEventBreakpoints = logEventBreakpoints;
+ return this._options.logEventBreakpoints;
+ }
+
+ get pauseOverlay() {
+ if (this._pauseOverlay) {
+ return this._pauseOverlay;
+ }
+
+ const env = new HighlighterEnvironment();
+ env.initFromTargetActor(this._parent);
+ const highlighter = new PausedDebuggerOverlay(env, {
+ resume: () => this.resume(null),
+ stepOver: () => this.resume({ type: "next" }),
+ });
+ this._pauseOverlay = highlighter;
+ return highlighter;
+ }
+
+ _canShowOverlay() {
+ const { window } = this._parent;
+
+ // The CanvasFrameAnonymousContentHelper class we're using to create the paused overlay
+ // need to have access to a documentElement.
+ // We might have access to a non-chrome window getter that is a Sandox (e.g. in the
+ // case of ContentProcessTargetActor).
+ if (!window?.document?.documentElement) {
+ return false;
+ }
+
+ // Ignore privileged document (top level window, special about:* pages, …).
+ if (window.isChromeWindow) {
+ return false;
+ }
+
+ return true;
+ }
+
+ async showOverlay() {
+ if (
+ this.isPaused() &&
+ this._canShowOverlay() &&
+ this._parent.on &&
+ this.pauseOverlay
+ ) {
+ const reason = this._priorPause.why.type;
+ await this.pauseOverlay.isReady;
+
+ // we might not be paused anymore.
+ if (!this.isPaused()) {
+ return;
+ }
+
+ this.pauseOverlay.show(reason);
+ }
+ }
+
+ hideOverlay() {
+ if (this._canShowOverlay() && this._pauseOverlay) {
+ this.pauseOverlay.hide();
+ }
+ }
+
+ /**
+ * Tell the thread to automatically add a breakpoint on the first line of
+ * a given file, when it is first loaded.
+ *
+ * This is currently only used by the xpcshell test harness, and unless
+ * we decide to expand the scope of this feature, we should keep it that way.
+ */
+ setBreakpointOnLoad(urls) {
+ this._onLoadBreakpointURLs = new Set(urls);
+ }
+
+ _findXHRBreakpointIndex(p, m) {
+ return this._xhrBreakpoints.findIndex(
+ ({ path, method }) => path === p && method === m
+ );
+ }
+
+ // We clear the priorPause field when a breakpoint is added or removed
+ // at the same location because we are no longer worried about pausing twice
+ // at that location (e.g. debugger statement, stepping).
+ _maybeClearPriorPause(location) {
+ if (!this._priorPause) {
+ return;
+ }
+
+ const { where } = this._priorPause.frame;
+ if (where.line === location.line && where.column === location.column) {
+ this._priorPause = null;
+ }
+ }
+
+ async setBreakpoint(location, options) {
+ let actor = this.breakpointActorMap.get(location);
+ // Avoid resetting the exact same breakpoint twice
+ if (actor && JSON.stringify(actor.options) == JSON.stringify(options)) {
+ return;
+ }
+ if (!actor) {
+ actor = this.breakpointActorMap.getOrCreateBreakpointActor(location);
+ }
+ actor.setOptions(options);
+ this._maybeClearPriorPause(location);
+
+ if (location.sourceUrl) {
+ // There can be multiple source actors for a URL if there are multiple
+ // inline sources on an HTML page.
+ const sourceActors = this.sourcesManager.getSourceActorsByURL(
+ location.sourceUrl
+ );
+ for (const sourceActor of sourceActors) {
+ await sourceActor.applyBreakpoint(actor);
+ }
+ } else {
+ const sourceActor = this.sourcesManager.getSourceActorById(
+ location.sourceId
+ );
+ if (sourceActor) {
+ await sourceActor.applyBreakpoint(actor);
+ }
+ }
+ }
+
+ removeBreakpoint(location) {
+ const actor = this.breakpointActorMap.getOrCreateBreakpointActor(location);
+ this._maybeClearPriorPause(location);
+ actor.delete();
+ }
+
+ removeXHRBreakpoint(path, method) {
+ const index = this._findXHRBreakpointIndex(path, method);
+
+ if (index >= 0) {
+ this._xhrBreakpoints.splice(index, 1);
+ }
+ return this._updateNetworkObserver();
+ }
+
+ setXHRBreakpoint(path, method) {
+ // request.path is a string,
+ // If requested url contains the path, then we pause.
+ const index = this._findXHRBreakpointIndex(path, method);
+
+ if (index === -1) {
+ this._xhrBreakpoints.push({ path, method });
+ }
+ return this._updateNetworkObserver();
+ }
+
+ getAvailableEventBreakpoints() {
+ return getAvailableEventBreakpoints(this._parent.window);
+ }
+ getActiveEventBreakpoints() {
+ return Array.from(this._activeEventBreakpoints);
+ }
+
+ /**
+ * Add event breakpoints to the list of active event breakpoints
+ *
+ * @param {Array<String>} ids: events to add (e.g. ["event.mouse.click","event.mouse.mousedown"])
+ */
+ addEventBreakpoints(ids) {
+ this.setActiveEventBreakpoints(
+ this.getActiveEventBreakpoints().concat(ids)
+ );
+ }
+
+ /**
+ * Remove event breakpoints from the list of active event breakpoints
+ *
+ * @param {Array<String>} ids: events to remove (e.g. ["event.mouse.click","event.mouse.mousedown"])
+ */
+ removeEventBreakpoints(ids) {
+ this.setActiveEventBreakpoints(
+ this.getActiveEventBreakpoints().filter(eventBp => !ids.includes(eventBp))
+ );
+ }
+
+ /**
+ * Set the the list of active event breakpoints
+ *
+ * @param {Array<String>} ids: events to add breakpoint for (e.g. ["event.mouse.click","event.mouse.mousedown"])
+ */
+ setActiveEventBreakpoints(ids) {
+ this._activeEventBreakpoints = new Set(ids);
+
+ if (eventsRequireNotifications(ids)) {
+ this._debuggerNotificationObserver.addListener(
+ this._eventBreakpointListener
+ );
+ } else {
+ this._debuggerNotificationObserver.removeListener(
+ this._eventBreakpointListener
+ );
+ }
+
+ if (this._activeEventBreakpoints.has(firstStatementBreakpointId())) {
+ this._ensureFirstStatementBreakpointInitialized();
+
+ this._firstStatementBreakpoint.hit = frame =>
+ this._pauseAndRespondEventBreakpoint(
+ frame,
+ firstStatementBreakpointId()
+ );
+ } else if (this._firstStatementBreakpoint) {
+ // Disabling the breakpoint disables the feature as much as we need it
+ // to. We do not bother removing breakpoints from the scripts themselves
+ // here because the breakpoints will be a no-op if `hit` is `null`, and
+ // if we wanted to remove them, we'd need a way to iterate through them
+ // all, which would require us to hold strong references to them, which
+ // just isn't needed. Plus, if the user disables and then re-enables the
+ // feature again later, the breakpoints will still be there to work.
+ this._firstStatementBreakpoint.hit = null;
+ }
+ }
+
+ _ensureFirstStatementBreakpointInitialized() {
+ if (this._firstStatementBreakpoint) {
+ return;
+ }
+
+ this._firstStatementBreakpoint = { hit: null };
+ for (const script of this.dbg.findScripts()) {
+ this._maybeTrackFirstStatementBreakpoint(script);
+ }
+ }
+
+ _maybeTrackFirstStatementBreakpointForNewGlobal(global) {
+ if (this._firstStatementBreakpoint) {
+ for (const script of this.dbg.findScripts({ global })) {
+ this._maybeTrackFirstStatementBreakpoint(script);
+ }
+ }
+ }
+
+ _maybeTrackFirstStatementBreakpoint(script) {
+ if (
+ // If the feature is not enabled yet, there is nothing to do.
+ !this._firstStatementBreakpoint ||
+ // WASM files don't have a first statement.
+ script.format !== "js" ||
+ // All "top-level" scripts are non-functions, whether that's because
+ // the script is a module, a global script, or an eval or what.
+ script.isFunction
+ ) {
+ return;
+ }
+
+ const bps = script.getPossibleBreakpoints();
+
+ // Scripts aren't guaranteed to have a step start if for instance the
+ // file contains only function declarations, so in that case we try to
+ // fall back to whatever we can find.
+ let meta = bps.find(bp => bp.isStepStart) || bps[0];
+ if (!meta) {
+ // We've tried to avoid using `getAllColumnOffsets()` because the set of
+ // locations included in this list is very under-defined, but for this
+ // usecase it's not the end of the world. Maybe one day we could have an
+ // "onEnterFrame" that was scoped to a specific script to avoid this.
+ meta = script.getAllColumnOffsets()[0];
+ }
+
+ if (!meta) {
+ // Not certain that this is actually possible, but including for sanity
+ // so that we don't throw unexpectedly.
+ return;
+ }
+ script.setBreakpoint(meta.offset, this._firstStatementBreakpoint);
+ }
+
+ _onNewDebuggee(global) {
+ this._maybeTrackFirstStatementBreakpointForNewGlobal(global);
+ try {
+ this._debuggerNotificationObserver.connect(global.unsafeDereference());
+ } catch (e) {}
+ }
+
+ _updateNetworkObserver() {
+ // Workers don't have access to `Services` and even if they did, network
+ // requests are all dispatched to the main thread, so there would be
+ // nothing here to listen for. We'll need to revisit implementing
+ // XHR breakpoints for workers.
+ if (isWorker) {
+ return false;
+ }
+
+ if (this._xhrBreakpoints.length && !this._observingNetwork) {
+ this._observingNetwork = true;
+ Services.obs.addObserver(
+ this._onOpeningRequest,
+ "http-on-opening-request"
+ );
+ } else if (this._xhrBreakpoints.length === 0 && this._observingNetwork) {
+ this._observingNetwork = false;
+ Services.obs.removeObserver(
+ this._onOpeningRequest,
+ "http-on-opening-request"
+ );
+ }
+
+ return true;
+ }
+
+ _onOpeningRequest(subject) {
+ if (this.skipBreakpointsOption) {
+ return;
+ }
+
+ const channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ const url = channel.URI.asciiSpec;
+ const requestMethod = channel.requestMethod;
+
+ let causeType = Ci.nsIContentPolicy.TYPE_OTHER;
+ if (channel.loadInfo) {
+ causeType = channel.loadInfo.externalContentPolicyType;
+ }
+
+ const isXHR =
+ causeType === Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST ||
+ causeType === Ci.nsIContentPolicy.TYPE_FETCH;
+
+ if (!isXHR) {
+ // We currently break only if the request is either fetch or xhr
+ return;
+ }
+
+ let shouldPause = false;
+ for (const { path, method } of this._xhrBreakpoints) {
+ if (method !== "ANY" && method !== requestMethod) {
+ continue;
+ }
+ if (url.includes(path)) {
+ shouldPause = true;
+ break;
+ }
+ }
+
+ if (shouldPause) {
+ const frame = this.dbg.getNewestFrame();
+
+ // If there is no frame, this request was dispatched by logic that isn't
+ // primarily JS, so pausing the event loop wouldn't make sense.
+ // This covers background requests like loading the initial page document,
+ // or loading favicons. This also includes requests dispatched indirectly
+ // from workers. We'll need to handle them separately in the future.
+ if (frame) {
+ this._pauseAndRespond(frame, { type: PAUSE_REASONS.XHR });
+ }
+ }
+ }
+
+ reconfigure(options = {}) {
+ if (this.state == STATES.EXITED) {
+ throw {
+ error: "wrongState",
+ };
+ }
+ this._options = { ...this._options, ...options };
+
+ if ("observeAsmJS" in options) {
+ this.dbg.allowUnobservedAsmJS = !options.observeAsmJS;
+ }
+ if ("observeWasm" in options) {
+ this.dbg.allowUnobservedWasm = !options.observeWasm;
+ }
+
+ if (
+ "pauseWorkersUntilAttach" in options &&
+ this._parent.pauseWorkersUntilAttach
+ ) {
+ this._parent.pauseWorkersUntilAttach(options.pauseWorkersUntilAttach);
+ }
+
+ if (options.breakpoints) {
+ for (const breakpoint of Object.values(options.breakpoints)) {
+ this.setBreakpoint(breakpoint.location, breakpoint.options);
+ }
+ }
+
+ if (options.eventBreakpoints) {
+ this.setActiveEventBreakpoints(options.eventBreakpoints);
+ }
+
+ this.maybePauseOnExceptions();
+ }
+
+ _eventBreakpointListener(notification) {
+ if (this._state === STATES.PAUSED || this._state === STATES.DETACHED) {
+ return;
+ }
+
+ const eventBreakpoint = eventBreakpointForNotification(
+ this.dbg,
+ notification
+ );
+
+ if (!this._activeEventBreakpoints.has(eventBreakpoint)) {
+ return;
+ }
+
+ if (notification.phase === "pre" && !this._activeEventPause) {
+ this._activeEventPause = this._captureDebuggerHooks();
+
+ this.dbg.onEnterFrame =
+ this._makeEventBreakpointEnterFrame(eventBreakpoint);
+ } else if (notification.phase === "post" && this._activeEventPause) {
+ this._restoreDebuggerHooks(this._activeEventPause);
+ this._activeEventPause = null;
+ } else if (!notification.phase && !this._activeEventPause) {
+ const frame = this.dbg.getNewestFrame();
+ if (frame) {
+ if (this.sourcesManager.isFrameBlackBoxed(frame)) {
+ return;
+ }
+
+ this._pauseAndRespondEventBreakpoint(frame, eventBreakpoint);
+ }
+ }
+ }
+
+ _makeEventBreakpointEnterFrame(eventBreakpoint) {
+ return frame => {
+ if (this.sourcesManager.isFrameBlackBoxed(frame)) {
+ return undefined;
+ }
+
+ this._restoreDebuggerHooks(this._activeEventPause);
+ this._activeEventPause = null;
+
+ return this._pauseAndRespondEventBreakpoint(frame, eventBreakpoint);
+ };
+ }
+
+ _pauseAndRespondEventBreakpoint(frame, eventBreakpoint) {
+ if (this.skipBreakpointsOption) {
+ return undefined;
+ }
+
+ if (this._options.logEventBreakpoints) {
+ return logEvent({
+ threadActor: this,
+ frame,
+ level: "logPoint",
+ expression: `[_event]`,
+ bindings: { _event: frame.arguments[0] },
+ });
+ }
+
+ return this._pauseAndRespond(frame, {
+ type: PAUSE_REASONS.EVENT_BREAKPOINT,
+ breakpoint: eventBreakpoint,
+ message: makeEventBreakpointMessage(eventBreakpoint),
+ });
+ }
+
+ _captureDebuggerHooks() {
+ return {
+ onEnterFrame: this.dbg.onEnterFrame,
+ onStep: this.dbg.onStep,
+ onPop: this.dbg.onPop,
+ };
+ }
+
+ _restoreDebuggerHooks(hooks) {
+ this.dbg.onEnterFrame = hooks.onEnterFrame;
+ this.dbg.onStep = hooks.onStep;
+ this.dbg.onPop = hooks.onPop;
+ }
+
+ /**
+ * Pause the debuggee, by entering a nested event loop, and return a 'paused'
+ * packet to the client.
+ *
+ * @param Debugger.Frame frame
+ * The newest debuggee frame in the stack.
+ * @param object reason
+ * An object with a 'type' property containing the reason for the pause.
+ * @param function onPacket
+ * Hook to modify the packet before it is sent. Feel free to return a
+ * promise.
+ */
+ _pauseAndRespond(frame, reason, onPacket = k => k) {
+ try {
+ const packet = this._paused(frame);
+ if (!packet) {
+ return undefined;
+ }
+
+ const { sourceActor, line, column } =
+ this.sourcesManager.getFrameLocation(frame);
+
+ packet.why = reason;
+
+ if (!sourceActor) {
+ // If the frame location is in a source that not pass the 'isHiddenSource'
+ // check and thus has no actor, we do not bother pausing.
+ return undefined;
+ }
+
+ packet.frame.where = {
+ actor: sourceActor.actorID,
+ line,
+ column,
+ };
+ const pkt = onPacket(packet);
+
+ this._priorPause = pkt;
+ this.emit("paused", pkt);
+ this.showOverlay();
+ } catch (error) {
+ reportException("DBG-SERVER", error);
+ this.conn.send({
+ error: "unknownError",
+ message: error.message + "\n" + error.stack,
+ });
+ return undefined;
+ }
+
+ try {
+ this._nestedEventLoop.enter();
+ } catch (e) {
+ reportException("TA__pauseAndRespond", e);
+ }
+
+ if (this._requestedFrameRestart) {
+ return null;
+ }
+
+ // If the parent actor has been closed, terminate the debuggee script
+ // instead of continuing. Executing JS after the content window is gone is
+ // a bad idea.
+ return this._parentClosed ? null : undefined;
+ }
+
+ _makeOnEnterFrame({ pauseAndRespond }) {
+ return frame => {
+ if (this._requestedFrameRestart) {
+ return null;
+ }
+
+ // Continue forward until we get to a valid step target.
+ const { onStep, onPop } = this._makeSteppingHooks({
+ steppingType: "next",
+ });
+
+ if (this.sourcesManager.isFrameBlackBoxed(frame)) {
+ return undefined;
+ }
+
+ frame.onStep = onStep;
+ frame.onPop = onPop;
+ return undefined;
+ };
+ }
+
+ _makeOnPop({ pauseAndRespond, steppingType }) {
+ const thread = this;
+ return function (completion) {
+ if (thread._requestedFrameRestart === this) {
+ return thread.restartFrame(this);
+ }
+
+ // onPop is called when we temporarily leave an async/generator
+ if (steppingType != "finish" && (completion.await || completion.yield)) {
+ thread.suspendedFrame = this;
+ thread.dbg.onEnterFrame = undefined;
+ return undefined;
+ }
+
+ // Note that we're popping this frame; we need to watch for
+ // subsequent step events on its caller.
+ this.reportedPop = true;
+
+ // Cache the frame so that the onPop and onStep hooks are cleared
+ // on the next pause.
+ thread.suspendedFrame = this;
+
+ if (
+ steppingType != "finish" &&
+ !thread.sourcesManager.isFrameBlackBoxed(this)
+ ) {
+ const pauseAndRespValue = pauseAndRespond(this, packet =>
+ thread.createCompletionGrip(packet, completion)
+ );
+
+ // If the requested frame to restart differs from this frame, we don't
+ // need to restart it at this point.
+ if (thread._requestedFrameRestart === this) {
+ return thread.restartFrame(this);
+ }
+
+ return pauseAndRespValue;
+ }
+
+ thread._attachSteppingHooks(this, "next", completion);
+ return undefined;
+ };
+ }
+
+ restartFrame(frame) {
+ this._requestedFrameRestart = null;
+ this._priorPause = null;
+
+ if (
+ frame.type !== "call" ||
+ frame.script.isGeneratorFunction ||
+ frame.script.isAsyncFunction
+ ) {
+ return undefined;
+ }
+ RESTARTED_FRAMES.add(frame);
+
+ const completion = frame.callee.apply(frame.this, frame.arguments);
+
+ return completion;
+ }
+
+ hasMoved(frame, newType) {
+ const newLocation = this.sourcesManager.getFrameLocation(frame);
+
+ if (!this._priorPause) {
+ return true;
+ }
+
+ // Recursion/Loops makes it okay to resume and land at
+ // the same breakpoint or debugger statement.
+ // It is not okay to transition from a breakpoint to debugger statement
+ // or a step to a debugger statement.
+ const { type } = this._priorPause.why;
+
+ if (type == newType) {
+ return true;
+ }
+
+ const { line, column } = this._priorPause.frame.where;
+ return line !== newLocation.line || column !== newLocation.column;
+ }
+
+ _makeOnStep({ pauseAndRespond, startFrame, steppingType, completion }) {
+ const thread = this;
+ return function () {
+ if (thread._validFrameStepOffset(this, startFrame, this.offset)) {
+ return pauseAndRespond(this, packet =>
+ thread.createCompletionGrip(packet, completion)
+ );
+ }
+
+ return undefined;
+ };
+ }
+
+ _validFrameStepOffset(frame, startFrame, offset) {
+ const meta = frame.script.getOffsetMetadata(offset);
+
+ // Continue if:
+ // 1. the location is not a valid breakpoint position
+ // 2. the source is blackboxed
+ // 3. we have not moved since the last pause
+ if (
+ !meta.isBreakpoint ||
+ this.sourcesManager.isFrameBlackBoxed(frame) ||
+ !this.hasMoved(frame)
+ ) {
+ return false;
+ }
+
+ // Pause if:
+ // 1. the frame has changed
+ // 2. the location is a step position.
+ return frame !== startFrame || meta.isStepStart;
+ }
+
+ atBreakpointLocation(frame) {
+ const location = this.sourcesManager.getFrameLocation(frame);
+ return !!this.breakpointActorMap.get(location);
+ }
+
+ createCompletionGrip(packet, completion) {
+ if (!completion) {
+ return packet;
+ }
+
+ const createGrip = value =>
+ createValueGrip(value, this._pausePool, this.objectGrip);
+ packet.why.frameFinished = {};
+
+ if (completion.hasOwnProperty("return")) {
+ packet.why.frameFinished.return = createGrip(completion.return);
+ } else if (completion.hasOwnProperty("yield")) {
+ packet.why.frameFinished.return = createGrip(completion.yield);
+ } else if (completion.hasOwnProperty("throw")) {
+ packet.why.frameFinished.throw = createGrip(completion.throw);
+ }
+
+ return packet;
+ }
+
+ /**
+ * Define the JS hook functions for stepping.
+ */
+ _makeSteppingHooks({ steppingType, startFrame, completion }) {
+ // Bind these methods and state because some of the hooks are called
+ // with 'this' set to the current frame. Rather than repeating the
+ // binding in each _makeOnX method, just do it once here and pass it
+ // in to each function.
+ const steppingHookState = {
+ pauseAndRespond: (frame, onPacket = k => k) =>
+ this._pauseAndRespond(
+ frame,
+ { type: PAUSE_REASONS.RESUME_LIMIT },
+ onPacket
+ ),
+ startFrame: startFrame || this.youngestFrame,
+ steppingType,
+ completion,
+ };
+
+ return {
+ onEnterFrame: this._makeOnEnterFrame(steppingHookState),
+ onPop: this._makeOnPop(steppingHookState),
+ onStep: this._makeOnStep(steppingHookState),
+ };
+ }
+
+ /**
+ * Handle attaching the various stepping hooks we need to attach when we
+ * receive a resume request with a resumeLimit property.
+ *
+ * @param Object { resumeLimit }
+ * The values received over the RDP.
+ * @returns A promise that resolves to true once the hooks are attached, or is
+ * rejected with an error packet.
+ */
+ async _handleResumeLimit({ resumeLimit, frameActorID }) {
+ const steppingType = resumeLimit.type;
+ if (
+ !["break", "step", "next", "finish", "restart"].includes(steppingType)
+ ) {
+ return Promise.reject({
+ error: "badParameterType",
+ message: "Unknown resumeLimit type",
+ });
+ }
+
+ let frame = this.youngestFrame;
+
+ if (frameActorID) {
+ frame = this._framesPool.getActorByID(frameActorID).frame;
+ if (!frame) {
+ throw new Error("Frame should exist in the frames pool.");
+ }
+ }
+
+ if (steppingType === "restart") {
+ if (
+ frame.type !== "call" ||
+ frame.script.isGeneratorFunction ||
+ frame.script.isAsyncFunction
+ ) {
+ return undefined;
+ }
+ this._requestedFrameRestart = frame;
+ }
+
+ return this._attachSteppingHooks(frame, steppingType, undefined);
+ }
+
+ _attachSteppingHooks(frame, steppingType, completion) {
+ // If we are stepping out of the onPop handler, we want to use "next" mode
+ // so that the parent frame's handlers behave consistently.
+ if (steppingType === "finish" && frame.reportedPop) {
+ steppingType = "next";
+ }
+
+ // If there are no more frames on the stack, use "step" mode so that we will
+ // pause on the next script to execute.
+ const stepFrame = this._getNextStepFrame(frame);
+ if (!stepFrame) {
+ steppingType = "step";
+ }
+
+ const { onEnterFrame, onPop, onStep } = this._makeSteppingHooks({
+ steppingType,
+ completion,
+ startFrame: frame,
+ });
+
+ if (steppingType === "step" || steppingType === "restart") {
+ this.dbg.onEnterFrame = onEnterFrame;
+ }
+
+ if (stepFrame) {
+ switch (steppingType) {
+ case "step":
+ case "break":
+ case "next":
+ if (stepFrame.script) {
+ if (!this.sourcesManager.isFrameBlackBoxed(stepFrame)) {
+ stepFrame.onStep = onStep;
+ }
+ }
+ // eslint-disable-next-line no-fallthrough
+ case "finish":
+ stepFrame.onStep = createStepForReactionTracking(stepFrame.onStep);
+ // eslint-disable-next-line no-fallthrough
+ case "restart":
+ stepFrame.onPop = onPop;
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Clear the onStep and onPop hooks for all frames on the stack.
+ */
+ _clearSteppingHooks() {
+ if (this.suspendedFrame) {
+ this.suspendedFrame.onStep = undefined;
+ this.suspendedFrame.onPop = undefined;
+ this.suspendedFrame = undefined;
+ }
+
+ let frame = this.youngestFrame;
+ if (frame?.onStack) {
+ while (frame) {
+ frame.onStep = undefined;
+ frame.onPop = undefined;
+ frame = frame.older;
+ }
+ }
+ }
+
+ /**
+ * Handle a protocol request to resume execution of the debuggee.
+ */
+ async resume(resumeLimit, frameActorID) {
+ if (this._state !== STATES.PAUSED) {
+ return {
+ error: "wrongState",
+ message:
+ "Can't resume when debuggee isn't paused. Current state is '" +
+ this._state +
+ "'",
+ state: this._state,
+ };
+ }
+
+ // In case of multiple nested event loops (due to multiple debuggers open in
+ // different tabs or multiple devtools clients connected to the same tab)
+ // only allow resumption in a LIFO order.
+ if (!this._nestedEventLoop.isTheLastPausedThreadActor()) {
+ return {
+ error: "wrongOrder",
+ message: "trying to resume in the wrong order.",
+ };
+ }
+
+ try {
+ if (resumeLimit) {
+ await this._handleResumeLimit({ resumeLimit, frameActorID });
+ } else {
+ this._clearSteppingHooks();
+ }
+
+ this.doResume({ resumeLimit });
+ return {};
+ } catch (error) {
+ return error instanceof Error
+ ? {
+ error: "unknownError",
+ message: DevToolsUtils.safeErrorString(error),
+ }
+ : // It is a known error, and the promise was rejected with an error
+ // packet.
+ error;
+ }
+ }
+
+ /**
+ * Only resume and notify necessary observers. This should be used in cases
+ * when we do not want to notify the front end of a resume, for example when
+ * we are shutting down.
+ */
+ doResume({ resumeLimit } = {}) {
+ this._state = STATES.RUNNING;
+
+ // Drop the actors in the pause actor pool.
+ this._pausePool.destroy();
+ this._pausePool = null;
+
+ this._pauseActor = null;
+ this._nestedEventLoop.exit();
+
+ // Tell anyone who cares of the resume (as of now, that's the xpcshell harness and
+ // devtools-startup.js when handling the --wait-for-jsdebugger flag)
+ this.emit("resumed");
+ this.hideOverlay();
+ }
+
+ /**
+ * Set the debugging hook to pause on exceptions if configured to do so.
+ */
+ maybePauseOnExceptions() {
+ if (this._options.pauseOnExceptions) {
+ this.dbg.onExceptionUnwind = this._onExceptionUnwind;
+ } else {
+ this.dbg.onExceptionUnwind = undefined;
+ }
+ }
+
+ /**
+ * Helper method that returns the next frame when stepping.
+ */
+ _getNextStepFrame(frame) {
+ const endOfFrame = frame.reportedPop;
+ const stepFrame = endOfFrame
+ ? frame.older || getAsyncParentFrame(frame)
+ : frame;
+ if (!stepFrame || !stepFrame.script) {
+ return null;
+ }
+
+ // Skips a frame that has been restarted.
+ if (RESTARTED_FRAMES.has(stepFrame)) {
+ return this._getNextStepFrame(stepFrame.older);
+ }
+
+ return stepFrame;
+ }
+
+ frames(start, count) {
+ if (this.state !== STATES.PAUSED) {
+ return {
+ error: "wrongState",
+ message:
+ "Stack frames are only available while the debuggee is paused.",
+ };
+ }
+
+ // Find the starting frame...
+ let frame = this.youngestFrame;
+
+ const walkToParentFrame = () => {
+ if (!frame) {
+ return;
+ }
+
+ const currentFrame = frame;
+ frame = null;
+
+ if (!(currentFrame instanceof Debugger.Frame)) {
+ frame = getSavedFrameParent(this, currentFrame);
+ } else if (currentFrame.older) {
+ frame = currentFrame.older;
+ } else if (
+ this._options.shouldIncludeSavedFrames &&
+ currentFrame.olderSavedFrame
+ ) {
+ frame = currentFrame.olderSavedFrame;
+ if (frame && !isValidSavedFrame(this, frame)) {
+ frame = null;
+ }
+ } else if (
+ this._options.shouldIncludeAsyncLiveFrames &&
+ currentFrame.asyncPromise
+ ) {
+ const asyncFrame = getAsyncParentFrame(currentFrame);
+ if (asyncFrame) {
+ frame = asyncFrame;
+ }
+ }
+ };
+
+ let i = 0;
+ while (frame && i < start) {
+ walkToParentFrame();
+ i++;
+ }
+
+ // Return count frames, or all remaining frames if count is not defined.
+ const frames = [];
+ for (; frame && (!count || i < start + count); i++, walkToParentFrame()) {
+ // SavedFrame instances don't have direct Debugger.Source object. If
+ // there is an active Debugger.Source that represents the SaveFrame's
+ // source, it will have already been created in the server.
+ if (frame instanceof Debugger.Frame) {
+ this.sourcesManager.createSourceActor(frame.script.source);
+ }
+
+ if (RESTARTED_FRAMES.has(frame)) {
+ continue;
+ }
+
+ const frameActor = this._createFrameActor(frame, i);
+ frames.push(frameActor);
+ }
+
+ return { frames };
+ }
+
+ addAllSources() {
+ // Compare the sources we find with the source URLs which have been loaded
+ // in debuggee realms. Count the number of sources associated with each
+ // URL so that we can detect if an HTML file has had some inline sources
+ // collected but not all.
+ const urlMap = {};
+ for (const url of this.dbg.findSourceURLs()) {
+ if (url !== "self-hosted") {
+ if (!urlMap[url]) {
+ urlMap[url] = { count: 0, sources: [] };
+ }
+ urlMap[url].count++;
+ }
+ }
+
+ const sources = this.dbg.findSources();
+
+ for (const source of sources) {
+ this._addSource(source);
+
+ // The following check should match the filtering done by `findSourceURLs`:
+ // https://searchfox.org/mozilla-central/rev/ac7a567f036e1954542763f4722fbfce041fb752/js/src/debugger/Debugger.cpp#2406-2409
+ // Otherwise we may populate `urlMap` incorrectly and resurrect sources that weren't GCed,
+ // and spawn duplicated SourceActors/Debugger.Source for the same actual source.
+ // `findSourceURLs` uses !introductionScript check as that allows to identify <script>'s
+ // loaded from the HTML page. This boolean will be defined only when the <script> tag
+ // is added by Javascript code at runtime.
+ // https://searchfox.org/mozilla-central/rev/3d03a3ca09f03f06ef46a511446537563f62a0c6/devtools/docs/user/debugger-api/debugger.source/index.rst#113
+ if (!source.introductionScript && urlMap[source.url]) {
+ urlMap[source.url].count--;
+ urlMap[source.url].sources.push(source);
+ }
+ }
+
+ // Resurrect any URLs for which not all sources are accounted for.
+ for (const [url, data] of Object.entries(urlMap)) {
+ if (data.count > 0) {
+ this._resurrectSource(url, data.sources);
+ }
+ }
+ }
+
+ sources(request) {
+ this.addAllSources();
+
+ // No need to flush the new source packets here, as we are sending the
+ // list of sources out immediately and we don't need to invoke the
+ // overhead of an RDP packet for every source right now. Let the default
+ // timeout flush the buffered packets.
+
+ return this.sourcesManager.iter().map(s => s.form());
+ }
+
+ /**
+ * Disassociate all breakpoint actors from their scripts and clear the
+ * breakpoint handlers. This method can be used when the thread actor intends
+ * to keep the breakpoint store, but needs to clear any actual breakpoints,
+ * e.g. due to a page navigation. This way the breakpoint actors' script
+ * caches won't hold on to the Debugger.Script objects leaking memory.
+ */
+ disableAllBreakpoints() {
+ for (const bpActor of this.breakpointActorMap.findActors()) {
+ bpActor.removeScripts();
+ }
+ }
+
+ removeAllWatchpoints() {
+ for (const actor of this.threadLifetimePool.poolChildren()) {
+ if (actor.typeName == "obj") {
+ actor.removeWatchpoints();
+ }
+ }
+ }
+
+ addWatchpoint(objActor, data) {
+ this._watchpointsMap.add(objActor, data);
+ }
+
+ removeWatchpoint(objActor, property) {
+ this._watchpointsMap.remove(objActor, property);
+ }
+
+ getWatchpoint(obj, property) {
+ return this._watchpointsMap.get(obj, property);
+ }
+
+ /**
+ * Handle a protocol request to pause the debuggee.
+ */
+ interrupt(when) {
+ if (this.state == STATES.EXITED) {
+ return { type: "exited" };
+ } else if (this.state == STATES.PAUSED) {
+ // TODO: return the actual reason for the existing pause.
+ this.emit("paused", {
+ why: { type: PAUSE_REASONS.ALREADY_PAUSED },
+ });
+ return {};
+ } else if (this.state != STATES.RUNNING) {
+ return {
+ error: "wrongState",
+ message: "Received interrupt request in " + this.state + " state.",
+ };
+ }
+ try {
+ // If execution should pause just before the next JavaScript bytecode is
+ // executed, just set an onEnterFrame handler.
+ if (when == "onNext") {
+ const onEnterFrame = frame => {
+ this._pauseAndRespond(frame, {
+ type: PAUSE_REASONS.INTERRUPTED,
+ onNext: true,
+ });
+ };
+ this.dbg.onEnterFrame = onEnterFrame;
+ return {};
+ }
+
+ // If execution should pause immediately, just put ourselves in the paused
+ // state.
+ const packet = this._paused();
+ if (!packet) {
+ return { error: "notInterrupted" };
+ }
+ packet.why = { type: PAUSE_REASONS.INTERRUPTED, onNext: false };
+
+ // Send the response to the interrupt request now (rather than
+ // returning it), because we're going to start a nested event loop
+ // here.
+ this.conn.send({ from: this.actorID, type: "interrupt" });
+ this.emit("paused", packet);
+
+ // Start a nested event loop.
+ this._nestedEventLoop.enter();
+
+ // We already sent a response to this request, don't send one
+ // now.
+ return null;
+ } catch (e) {
+ reportException("DBG-SERVER", e);
+ return { error: "notInterrupted", message: e.toString() };
+ }
+ }
+
+ _paused(frame) {
+ // We don't handle nested pauses correctly. Don't try - if we're
+ // paused, just continue running whatever code triggered the pause.
+ // We don't want to actually have nested pauses (although we
+ // have nested event loops). If code runs in the debuggee during
+ // a pause, it should cause the actor to resume (dropping
+ // pause-lifetime actors etc) and then repause when complete.
+
+ if (this.state === STATES.PAUSED) {
+ return undefined;
+ }
+
+ this._state = STATES.PAUSED;
+
+ // Clear stepping hooks.
+ this.dbg.onEnterFrame = undefined;
+ this._requestedFrameRestart = null;
+ this._clearSteppingHooks();
+
+ // Create the actor pool that will hold the pause actor and its
+ // children.
+ assert(!this._pausePool, "No pause pool should exist yet");
+ this._pausePool = new Pool(this.conn, "pause");
+
+ // Give children of the pause pool a quick link back to the
+ // thread...
+ this._pausePool.threadActor = this;
+
+ // Create the pause actor itself...
+ assert(!this._pauseActor, "No pause actor should exist yet");
+ this._pauseActor = new PauseActor(this._pausePool);
+ this._pausePool.manage(this._pauseActor);
+
+ // Update the list of frames.
+ this._updateFrames();
+
+ // Send off the paused packet and spin an event loop.
+ const packet = {
+ actor: this._pauseActor.actorID,
+ };
+
+ if (frame) {
+ packet.frame = this._createFrameActor(frame);
+ }
+
+ return packet;
+ }
+
+ /**
+ * Expire frame actors for frames that are no longer on the current stack.
+ */
+ _updateFrames() {
+ // Create the actor pool that will hold the still-living frames.
+ const framesPool = new Pool(this.conn, "frames");
+ const frameList = [];
+
+ for (const frameActor of this._frameActors) {
+ if (frameActor.frame.onStack) {
+ framesPool.manage(frameActor);
+ frameList.push(frameActor);
+ }
+ }
+
+ // Remove the old frame actor pool, this will expire
+ // any actors that weren't added to the new pool.
+ if (this._framesPool) {
+ this._framesPool.destroy();
+ }
+
+ this._frameActors = frameList;
+ this._framesPool = framesPool;
+ }
+
+ _createFrameActor(frame, depth) {
+ let actor = this._frameActorMap.get(frame);
+ if (!actor || actor.isDestroyed()) {
+ actor = new FrameActor(frame, this, depth);
+ this._frameActors.push(actor);
+ this._framesPool.manage(actor);
+
+ this._frameActorMap.set(frame, actor);
+ }
+ return actor;
+ }
+
+ /**
+ * Create and return an environment actor that corresponds to the provided
+ * Debugger.Environment.
+ * @param Debugger.Environment environment
+ * The lexical environment we want to extract.
+ * @param object pool
+ * The pool where the newly-created actor will be placed.
+ * @return The EnvironmentActor for environment or undefined for host
+ * functions or functions scoped to a non-debuggee global.
+ */
+ createEnvironmentActor(environment, pool) {
+ if (!environment) {
+ return undefined;
+ }
+
+ if (environment.actor) {
+ return environment.actor;
+ }
+
+ const actor = new EnvironmentActor(environment, this);
+ pool.manage(actor);
+ environment.actor = actor;
+
+ return actor;
+ }
+
+ /**
+ * Create a grip for the given debuggee object.
+ *
+ * @param value Debugger.Object
+ * The debuggee object value.
+ * @param pool Pool
+ * The actor pool where the new object actor will be added.
+ */
+ objectGrip(value, pool) {
+ if (!pool.objectActors) {
+ pool.objectActors = new WeakMap();
+ }
+
+ if (pool.objectActors.has(value)) {
+ return pool.objectActors.get(value).form();
+ }
+
+ if (this.threadLifetimePool.objectActors.has(value)) {
+ return this.threadLifetimePool.objectActors.get(value).form();
+ }
+
+ const actor = new PauseScopedObjectActor(
+ value,
+ {
+ thread: this,
+ getGripDepth: () => this._gripDepth,
+ incrementGripDepth: () => this._gripDepth++,
+ decrementGripDepth: () => this._gripDepth--,
+ createValueGrip: v => {
+ if (this._pausePool) {
+ return createValueGrip(v, this._pausePool, this.pauseObjectGrip);
+ }
+
+ return createValueGrip(v, this.threadLifetimePool, this.objectGrip);
+ },
+ createEnvironmentActor: (e, p) => this.createEnvironmentActor(e, p),
+ promote: () => this.threadObjectGrip(actor),
+ isThreadLifetimePool: () =>
+ actor.getParent() !== this.threadLifetimePool,
+ },
+ this.conn
+ );
+ pool.manage(actor);
+ pool.objectActors.set(value, actor);
+ return actor.form();
+ }
+
+ /**
+ * Create a grip for the given debuggee object with a pause lifetime.
+ *
+ * @param value Debugger.Object
+ * The debuggee object value.
+ */
+ pauseObjectGrip(value) {
+ if (!this._pausePool) {
+ throw new Error("Object grip requested while not paused.");
+ }
+
+ return this.objectGrip(value, this._pausePool);
+ }
+
+ /**
+ * Extend the lifetime of the provided object actor to thread lifetime.
+ *
+ * @param actor object
+ * The object actor.
+ */
+ threadObjectGrip(actor) {
+ this.threadLifetimePool.manage(actor);
+ this.threadLifetimePool.objectActors.set(actor.obj, actor);
+ }
+
+ _onWindowReady({ isTopLevel, isBFCache, window }) {
+ // Note that this code relates to the disabling of Debugger API from will-navigate listener.
+ // And should only be triggered when the target actor doesn't follow WindowGlobal lifecycle.
+ // i.e. when the Thread Actor manages more than one top level WindowGlobal.
+ if (isTopLevel && this.state != STATES.DETACHED) {
+ this.sourcesManager.reset();
+ this.clearDebuggees();
+ this.dbg.enable();
+ // Update the global no matter if the debugger is on or off,
+ // otherwise the global will be wrong when enabled later.
+ this.global = window;
+ }
+
+ // Refresh the debuggee list when a new window object appears (top window or
+ // iframe).
+ if (this.attached) {
+ this.dbg.addDebuggees();
+ }
+
+ // BFCache navigations reuse old sources, so send existing sources to the
+ // client instead of waiting for onNewScript debugger notifications.
+ if (isBFCache) {
+ this.addAllSources();
+ }
+ }
+
+ _onWillNavigate({ isTopLevel }) {
+ if (!isTopLevel) {
+ return;
+ }
+
+ // Proceed normally only if the debuggee is not paused.
+ if (this.state == STATES.PAUSED) {
+ // If we were paused while navigating to a new page,
+ // we resume previous page execution, so that the document can be sucessfully unloaded.
+ // And we disable the Debugger API, so that we do not hit any breakpoint or trigger any
+ // thread actor feature. We will re-enable it just before the next page starts loading,
+ // from window-ready listener. That's for when the target doesn't follow WindowGlobal
+ // lifecycle.
+ // When the target follows the WindowGlobal lifecycle, we will stiff resume and disable
+ // this thread actor. It will soon be destroyed. And a new target will pick up
+ // the next WindowGlobal and spawn a new Debugger API, via ThreadActor.attach().
+ this.doResume();
+ this.dbg.disable();
+ }
+
+ this.removeAllWatchpoints();
+ this.disableAllBreakpoints();
+ this.dbg.onEnterFrame = undefined;
+ }
+
+ _onNavigate() {
+ if (this.state == STATES.RUNNING) {
+ this.dbg.enable();
+ }
+ }
+
+ // JS Debugger API hooks.
+ pauseForMutationBreakpoint(
+ mutationType,
+ targetNode,
+ ancestorNode,
+ action = "" // "add" or "remove"
+ ) {
+ if (
+ !["subtreeModified", "nodeRemoved", "attributeModified"].includes(
+ mutationType
+ )
+ ) {
+ throw new Error("Unexpected mutation breakpoint type");
+ }
+
+ const frame = this.dbg.getNewestFrame();
+ if (!frame) {
+ return undefined;
+ }
+
+ if (
+ this.skipBreakpointsOption ||
+ this.sourcesManager.isFrameBlackBoxed(frame)
+ ) {
+ return undefined;
+ }
+
+ const global = (targetNode.ownerDocument || targetNode).defaultView;
+ assert(global && this.dbg.hasDebuggee(global));
+
+ const targetObj = this.dbg
+ .makeGlobalObjectReference(global)
+ .makeDebuggeeValue(targetNode);
+
+ let ancestorObj = null;
+ if (ancestorNode) {
+ ancestorObj = this.dbg
+ .makeGlobalObjectReference(global)
+ .makeDebuggeeValue(ancestorNode);
+ }
+
+ return this._pauseAndRespond(
+ frame,
+ {
+ type: PAUSE_REASONS.MUTATION_BREAKPOINT,
+ mutationType,
+ message: `DOM Mutation: '${mutationType}'`,
+ },
+ pkt => {
+ // We have to add this here because `_pausePool` is `null` beforehand.
+ pkt.why.nodeGrip = this.objectGrip(targetObj, this._pausePool);
+ pkt.why.ancestorGrip = ancestorObj
+ ? this.objectGrip(ancestorObj, this._pausePool)
+ : null;
+ pkt.why.action = action;
+ return pkt;
+ }
+ );
+ }
+
+ /**
+ * A function that the engine calls when a debugger statement has been
+ * executed in the specified frame.
+ *
+ * @param frame Debugger.Frame
+ * The stack frame that contained the debugger statement.
+ */
+ onDebuggerStatement(frame) {
+ // Don't pause if
+ // 1. we have not moved since the last pause
+ // 2. breakpoints are disabled
+ // 3. the source is blackboxed
+ // 4. there is a breakpoint at the same location
+ if (
+ !this.hasMoved(frame, "debuggerStatement") ||
+ this.skipBreakpointsOption ||
+ this.sourcesManager.isFrameBlackBoxed(frame) ||
+ this.atBreakpointLocation(frame)
+ ) {
+ return undefined;
+ }
+
+ return this._pauseAndRespond(frame, {
+ type: PAUSE_REASONS.DEBUGGER_STATEMENT,
+ });
+ }
+
+ skipBreakpoints(skip) {
+ this._options.skipBreakpoints = skip;
+ return { skip };
+ }
+
+ // Bug 1686485 is meant to remove usages of this request
+ // in favor direct call to `reconfigure`
+ pauseOnExceptions(pauseOnExceptions, ignoreCaughtExceptions) {
+ this.reconfigure({
+ pauseOnExceptions,
+ ignoreCaughtExceptions,
+ });
+ return {};
+ }
+
+ /**
+ * A function that the engine calls when an exception has been thrown and has
+ * propagated to the specified frame.
+ *
+ * @param youngestFrame Debugger.Frame
+ * The youngest remaining stack frame.
+ * @param value object
+ * The exception that was thrown.
+ */
+ _onExceptionUnwind(youngestFrame, value) {
+ // Ignore any reported exception if we are already paused
+ if (this.isPaused()) {
+ return undefined;
+ }
+
+ let willBeCaught = false;
+ for (let frame = youngestFrame; frame != null; frame = frame.older) {
+ if (frame.script.isInCatchScope(frame.offset)) {
+ willBeCaught = true;
+ break;
+ }
+ }
+
+ if (willBeCaught && this._options.ignoreCaughtExceptions) {
+ return undefined;
+ }
+
+ if (
+ this._handledFrameExceptions.has(youngestFrame) &&
+ this._handledFrameExceptions.get(youngestFrame) === value
+ ) {
+ return undefined;
+ }
+
+ // NS_ERROR_NO_INTERFACE exceptions are a special case in browser code,
+ // since they're almost always thrown by QueryInterface functions, and
+ // handled cleanly by native code.
+ if (!isWorker && value == Cr.NS_ERROR_NO_INTERFACE) {
+ return undefined;
+ }
+
+ // Don't pause on exceptions thrown while inside an evaluation being done on
+ // behalf of the client.
+ if (this.insideClientEvaluation) {
+ return undefined;
+ }
+
+ if (
+ this.skipBreakpointsOption ||
+ this.sourcesManager.isFrameBlackBoxed(youngestFrame)
+ ) {
+ return undefined;
+ }
+
+ // Now that we've decided to pause, ignore this exception if it's thrown by
+ // any older frames.
+ for (let frame = youngestFrame.older; frame != null; frame = frame.older) {
+ this._handledFrameExceptions.set(frame, value);
+ }
+
+ try {
+ const packet = this._paused(youngestFrame);
+ if (!packet) {
+ return undefined;
+ }
+
+ packet.why = {
+ type: PAUSE_REASONS.EXCEPTION,
+ exception: createValueGrip(value, this._pausePool, this.objectGrip),
+ };
+ this.emit("paused", packet);
+
+ this._nestedEventLoop.enter();
+ } catch (e) {
+ reportException("TA_onExceptionUnwind", e);
+ }
+
+ return undefined;
+ }
+
+ /**
+ * A function that the engine calls when a new script has been loaded.
+ *
+ * @param script Debugger.Script
+ * The source script that has been loaded into a debuggee compartment.
+ */
+ onNewScript(script) {
+ this._addSource(script.source);
+
+ this._maybeTrackFirstStatementBreakpoint(script);
+ }
+
+ /**
+ * A function called when there's a new source from a thread actor's sources.
+ * Emits `newSource` on the thread actor.
+ *
+ * @param {SourceActor} source
+ */
+ onNewSourceEvent(source) {
+ // When this target is supported by the Watcher Actor,
+ // and we listen to SOURCE, we avoid emitting the newSource RDP event
+ // as it would be duplicated with the Resource/watchResources API.
+ // Could probably be removed once bug 1680280 is fixed.
+ if (!this._shouldEmitNewSource) {
+ return;
+ }
+
+ // Bug 1516197: New sources are likely detected due to either user
+ // interaction on the page, or devtools requests sent to the server.
+ // We use executeSoon because we don't want to block those operations
+ // by sending packets in the middle of them.
+ DevToolsUtils.executeSoon(() => {
+ if (this.isDestroyed()) {
+ return;
+ }
+ this.emit("newSource", {
+ source: source.form(),
+ });
+ });
+ }
+
+ // API used by the Watcher Actor to disable the newSource events
+ // Could probably be removed once bug 1680280 is fixed.
+ _shouldEmitNewSource = true;
+ disableNewSourceEvents() {
+ this._shouldEmitNewSource = false;
+ }
+
+ /**
+ * Filtering function to filter out sources for which we don't want to notify/create
+ * source actors
+ *
+ * @param {Debugger.Source} source
+ * The source to accept or ignore
+ * @param Boolean
+ * True, if we want to create a source actor.
+ */
+ _acceptSource(source) {
+ // We have some spurious source created by ExtensionContent.sys.mjs when debugging tabs.
+ // These sources are internal stuff injected by WebExt codebase to implement content
+ // scripts. We can't easily ignore them from Debugger API, so ignore them
+ // when debugging a tab (i.e. browser-element). As we still want to debug them
+ // from the browser toolbox.
+ if (
+ this._parent.sessionContext.type == "browser-element" &&
+ source.url.endsWith("ExtensionContent.sys.mjs")
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Add the provided source to the server cache.
+ *
+ * @param aSource Debugger.Source
+ * The source that will be stored.
+ */
+ _addSource(source) {
+ if (!this._acceptSource(source)) {
+ return;
+ }
+
+ // Preloaded WebExtension content scripts may be cached internally by
+ // ExtensionContent.jsm and ThreadActor would ignore them on a page reload
+ // because it finds them in the _debuggerSourcesSeen WeakSet,
+ // and so we also need to be sure that there is still a source actor for the source.
+ let sourceActor;
+ if (
+ this._debuggerSourcesSeen.has(source) &&
+ this.sourcesManager.hasSourceActor(source)
+ ) {
+ sourceActor = this.sourcesManager.getSourceActor(source);
+ sourceActor.resetDebuggeeScripts();
+ } else {
+ sourceActor = this.sourcesManager.createSourceActor(source);
+ }
+
+ const sourceUrl = sourceActor.url;
+ if (this._onLoadBreakpointURLs.has(sourceUrl)) {
+ // Immediately set a breakpoint on first line
+ // (note that this is only used by `./mach xpcshell-test --jsdebugger`)
+ this.setBreakpoint({ sourceUrl, line: 1 }, {});
+ // But also query asynchronously the first really breakable line
+ // as the first may not be valid and won't break.
+ (async () => {
+ const [firstLine] = await sourceActor.getBreakableLines();
+ if (firstLine != 1) {
+ this.setBreakpoint({ sourceUrl, line: firstLine }, {});
+ }
+ })();
+ }
+
+ const bpActors = this.breakpointActorMap
+ .findActors()
+ .filter(
+ actor =>
+ actor.location.sourceUrl && actor.location.sourceUrl == sourceUrl
+ );
+
+ for (const actor of bpActors) {
+ sourceActor.applyBreakpoint(actor);
+ }
+
+ this._debuggerSourcesSeen.add(source);
+ }
+
+ /**
+ * Create a new source by refetching the specified URL and instantiating all
+ * sources that were found in the result.
+ *
+ * @param url The URL string to fetch.
+ * @param existingInlineSources The inline sources for the URL the debugger knows about
+ * already, and that we shouldn't re-create (only used when
+ * url content type is text/html).
+ */
+ async _resurrectSource(url, existingInlineSources) {
+ let { content, contentType, sourceMapURL } =
+ await this.sourcesManager.urlContents(
+ url,
+ /* partial */ false,
+ /* canUseCache */ true
+ );
+
+ // Newlines in all sources should be normalized. Do this with HTML content
+ // to simplify the comparisons below.
+ content = content.replace(/\r\n?|\u2028|\u2029/g, "\n");
+
+ if (contentType == "text/html") {
+ // HTML files can contain any number of inline sources. We have to find
+ // all the inline sources and their start line without running any of the
+ // scripts on the page. The approach used here is approximate.
+ if (!this._parent.window) {
+ return;
+ }
+
+ // Find the offsets in the HTML at which inline scripts might start.
+ const scriptTagMatches = content.matchAll(/<script[^>]*>/gi);
+ const scriptStartOffsets = [...scriptTagMatches].map(
+ rv => rv.index + rv[0].length
+ );
+
+ // Find the script tags in this HTML page by parsing a new document from
+ // the contentand looking for its script elements.
+ const document = new DOMParser().parseFromString(content, "text/html");
+
+ // For each inline source found, see if there is a start offset for what
+ // appears to be a script tag, whose contents match the inline source.
+ [...document.scripts].forEach(script => {
+ const text = script.innerText;
+
+ // We only want to handle inline scripts
+ if (script.src) {
+ return;
+ }
+
+ // Don't create source for empty script tag
+ if (!text.trim()) {
+ return;
+ }
+
+ const scriptStartOffsetIndex = scriptStartOffsets.findIndex(
+ offset => content.substring(offset, offset + text.length) == text
+ );
+ // Bail if we couldn't find the start offset for the script
+ if (scriptStartOffsetIndex == -1) {
+ return;
+ }
+
+ const scriptStartOffset = scriptStartOffsets[scriptStartOffsetIndex];
+ // Remove the offset from the array to mitigate any issue we might with scripts
+ // sharing the same text content.
+ scriptStartOffsets.splice(scriptStartOffsetIndex, 1);
+
+ const allLineBreaks = [
+ ...content.substring(0, scriptStartOffset).matchAll("\n"),
+ ];
+ const startLine = 1 + allLineBreaks.length;
+ const startColumn =
+ scriptStartOffset -
+ (allLineBreaks.length ? allLineBreaks.at(-1).index - 1 : 0);
+
+ // Don't create a source if we already found one for this script
+ if (
+ existingInlineSources.find(
+ source =>
+ source.startLine == startLine && source.startColumn == startColumn
+ )
+ ) {
+ return;
+ }
+
+ try {
+ const global = this.dbg.getDebuggees()[0];
+ this._addSource(
+ global.createSource({
+ text,
+ url,
+ startLine,
+ startColumn,
+ isScriptElement: true,
+ })
+ );
+ } catch (e) {
+ // Ignore parse errors.
+ }
+ });
+
+ // If no scripts were found, we might have an inaccurate content type and
+ // the file is actually JavaScript. Fall through and add the entire file
+ // as the source.
+ if (document.scripts.length) {
+ return;
+ }
+ }
+
+ // Other files should only contain javascript, so add the file contents as
+ // the source itself.
+ try {
+ const global = this.dbg.getDebuggees()[0];
+ this._addSource(
+ global.createSource({
+ text: content,
+ url,
+ startLine: 1,
+ sourceMapURL,
+ })
+ );
+ } catch (e) {
+ // Ignore parse errors.
+ }
+ }
+
+ dumpThread() {
+ return {
+ pauseOnExceptions: this._options.pauseOnExceptions,
+ ignoreCaughtExceptions: this._options.ignoreCaughtExceptions,
+ logEventBreakpoints: this._options.logEventBreakpoints,
+ skipBreakpoints: this.skipBreakpointsOption,
+ breakpoints: this.breakpointActorMap.listKeys(),
+ };
+ }
+
+ // NOTE: dumpPools is defined in the Thread actor to avoid
+ // adding it to multiple target specs and actors.
+ dumpPools() {
+ return this.conn.dumpPools();
+ }
+
+ logLocation(prefix, frame) {
+ const loc = this.sourcesManager.getFrameLocation(frame);
+ dump(`${prefix} (${loc.line}, ${loc.column})\n`);
+ }
+}
+
+exports.ThreadActor = ThreadActor;
+
+/**
+ * Creates a PauseActor.
+ *
+ * PauseActors exist for the lifetime of a given debuggee pause. Used to
+ * scope pause-lifetime grips.
+ *
+ * @param {Pool} pool: The actor pool created for this pause.
+ */
+function PauseActor(pool) {
+ this.pool = pool;
+}
+
+PauseActor.prototype = {
+ typeName: "pause",
+};
+
+// Utility functions.
+
+/**
+ * Unwrap a global that is wrapped in a |Debugger.Object|, or if the global has
+ * become a dead object, return |undefined|.
+ *
+ * @param Debugger.Object wrappedGlobal
+ * The |Debugger.Object| which wraps a global.
+ *
+ * @returns {Object|undefined}
+ * Returns the unwrapped global object or |undefined| if unwrapping
+ * failed.
+ */
+exports.unwrapDebuggerObjectGlobal = wrappedGlobal => {
+ try {
+ // Because of bug 991399 we sometimes get nuked window references here. We
+ // just bail out in that case.
+ //
+ // Note that addon sandboxes have a DOMWindow as their prototype. So make
+ // sure that we can touch the prototype too (whatever it is), in case _it_
+ // is it a nuked window reference. We force stringification to make sure
+ // that any dead object proxies make themselves known.
+ const global = wrappedGlobal.unsafeDereference();
+ Object.getPrototypeOf(global) + "";
+ return global;
+ } catch (e) {
+ return undefined;
+ }
+};