/* 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 {
ObjectActorPool,
} = require(
"resource://devtools/server/actors/object/ObjectActorPool.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 Targets = require(
"devtools/server/actors/targets/index");
loader.lazyRequireGetter(
this,
"logEvent",
"resource://devtools/server/actors/utils/logEvent.js",
true
);
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,
"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 {TargetActor} targetActor
* This `ThreadActor`'s parent actor. i.e. one of the many Target actors.
*/
constructor(targetActor) {
super(targetActor.conn, threadSpec);
// This attribute is used by various other actors to find the target actor
this.targetActor = targetActor;
this._state = STATES.DETACHED;
this._options = {
skipBreakpoints:
false,
};
this._gripDepth = 0;
this._targetActorClosed =
false;
this._observingNetwork =
false;
this._shouldShowPauseOverlay =
true;
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._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.targetActor.on(
"window-ready",
this._onWindowReady);
this.targetActor.on(
"will-navigate",
this._onWillNavigate);
this.targetActor.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.targetActor.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 pauseLifetimePool() {
return this._pausePool;
}
get threadLifetimePool() {
if (!
this._threadLifetimePool) {
this._threadLifetimePool =
new ObjectActorPool(
this,
"thread",
true);
this._threadLifetimePool.objectActors =
new WeakMap();
}
return this._threadLifetimePool;
}
getThreadLifetimeObject(raw) {
return this.threadLifetimePool.objectActors.get(raw);
}
promoteObjectToThreadLifetime(objectActor) {
this.threadLifetimePool.manage(objectActor);
this.threadLifetimePool.objectActors.set(objectActor.obj, objectActor);
}
get sourcesManager() {
return this.targetActor.sourcesManager;
}
get breakpoints() {
return this.targetActor.breakpoints;
}
get youngestFrame() {
if (
this.state != STATES.PAUSED) {
return null;
}
return this.dbg.getNewestFrame();
}
get shouldSkipAnyBreakpoint() {
return (
// Disable all types of breakpoints if:
// - the user explicitly requested it via the option
this._options.skipBreakpoints ||
// - or when we are evaluating some javascript via the console actor and disableBreaks
// has been set to true (which happens for most evaluating except the console input)
this.insideClientEvaluation?.disableBreaks
);
}
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.targetActor.off(
"window-ready",
this._onWindowReady);
this.targetActor.off(
"will-navigate",
this._onWillNavigate);
this.targetActor.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();
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.targetActor);
const highlighter =
new PausedDebuggerOverlay(env, {
resume: () =>
this.resume(
null),
stepOver: () =>
this.resume({ type:
"next" }),
});
this._pauseOverlay = highlighter;
return highlighter;
}
_canShowOverlay() {
// Only attempt to show on overlay on WindowGlobal targets, which displays a document.
// Workers and content processes can't display any overlay.
if (
this.targetActor.targetType != Targets.TYPES.FRAME) {
return false;
}
const { window } =
this.targetActor;
// 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._shouldShowPauseOverlay ||
!
this.isPaused() ||
!
this._canShowOverlay()
) {
return;
}
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) {
// Automatically initialize the thread actor if it wasn't yet done.
// Note that ideally, it should rather be done via reconfigure/thread configuration.
if (
this._state === STATES.DETACHED) {
this.attach({});
this.addAllSources();
}
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();
}
removeAllXHRBreakpoints() {
this._xhrBreakpoints = [];
return this._updateNetworkObserver();
}
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.targetActor.targetGlobal);
}
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.shouldSkipAnyBreakpoint) {
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 (
"pauseOverlay" in options) {
this._shouldShowPauseOverlay = !!options.pauseOverlay;
}
if (
"pauseWorkersUntilAttach" in options &&
this.targetActor.pauseWorkersUntilAttach
) {
this.targetActor.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);
}
// Only consider this options if an explicit boolean value is passed.
if (
typeof this._options.shouldPauseOnDebuggerStatement ==
"boolean") {
this.setPauseOnDebuggerStatement(
this._options.shouldPauseOnDebuggerStatement
);
}
this.setPauseOnExceptions(
this._options.pauseOnExceptions);
}
_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.shouldSkipAnyBreakpoint) {
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._targetActorClosed ?
null : undefined;
}
_makeOnEnterFrame() {
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;
// Conditional breakpoint are doing something weird as they are using "breakpoint" type
// unless they throw in which case they will be "breakpointConditionThrown".
if (
type == newType ||
(type ==
"breakpointConditionThrown" && newType ==
"breakpoint")
) {
return true;
}
const { line, column } =
this._priorPause.frame.where;
return line !== newLocation.line || column !== newLocation.column;
}
_makeOnStep({ pauseAndRespond, startFrame, 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;
}
packet.why.frameFinished = {};
if (completion.hasOwnProperty(
"return")) {
packet.why.frameFinished.
return =
this.createValueGrip(completion.
return);
}
else if (completion.hasOwnProperty(
"yield")) {
packet.why.frameFinished.
return =
this.createValueGrip(completion.yield);
}
else if (completion.hasOwnProperty(
"throw")) {
packet.why.frameFinished.
throw =
this.createValueGrip(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() {
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.
*
* Note that this is also called when evaluating conditional breakpoints.
*
* @param {Boolean} doPause
* Should watch for pause or not. `_onExceptionUnwind` function will
* then be notified about new caught or uncaught exception being fired.
*/
setPauseOnExceptions(doPause) {
if (doPause) {
this.dbg.onExceptionUnwind =
this._onExceptionUnwind;
}
else {
this.dbg.onExceptionUnwind = undefined;
}
}
/**
* Set the debugging hook to pause on debugger statement if configured to do so.
*
* Note that the thread actor will pause on exception by default.
* This method has to be called with a falsy value to disable it.
*
* @param {Boolean} doPause
* Controls whether we should or should not pause on debugger statement.
*/
setPauseOnDebuggerStatement(doPause) {
this.dbg.onDebuggerStatement = doPause
?
this.onDebuggerStatement
: undefined;
}
isPauseOnExceptionsEnabled() {
return this.dbg.onExceptionUnwind ==
this._onExceptionUnwind;
}
/**
* 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() {
// This method aims at instantiating Source Actors for all already existing
// sources (via `_addSource()`).
// This is called on each new target instantiation:
// * when a new document or debugging context is instantiated. This
// method should be a no-op as there should be no pre-existing sources.
// * when devtools open. This time there might be pre-existing sources.
//
// We are using Debugger API `findSources()` for instantating source actors
// of all still-active sources. But we want to also "resurrect" sources
// which ran before DevTools were opened and were garbaged collected.
// `findSources()` won't return them.
// Debugger API `findSourceURLs()` will return the source URLs of all the
// sources, GC-ed and still active ones.
//
// We are using `urlMap` to identify the GC-ed sources.
//
// We have two special edgecases:
//
// # HTML sources and inline <script> tags
//
// HTML sources will be specific to a given URL, but may relate to multiple
// inline <script> tag. Each script will be related to a given Debugger API
// source and a given DevTools Source Actor.
// We collect all active sources in `urlMap`'s `sources` array so that we
// only resurrect the GC-ed inline <script> and not the one which are still
// active.
//
// # asm.js / wasm
//
// DevTools toggles Debugger API `allowUnobservedAsmJS` and
// `allowUnobservedWasm` to false on opening. This changes how asm.js and
// Wasm sources are compiled. But only to sources created after DevTools
// are opened. This typically requires to reload the page.
//
// Before DevTools are opened, the asm.js functions are compiled into wasm
// instances, and they are visible as "wasm" sources in `findSources()`.
// The wasm instance doesn't keep the top-level normal JS script and the
// corresponding JS source alive. If only the "wasm" source is found for
// certain URL, the source needs to be re-compiled.
//
// Here, we should be careful to re-compile these sources the way they were
// compiled before DevTools opening. Otherwise the re-compilation will
// create Debugger.Script instances backed by normal JS functions for those
// asm.js functions, which results in an inconsistency between what's
// running in the debuggee and what's shown in DevTools.
//
// We are using `urlMap`'s `hasWasm` to flag them and instruct
// `resurrectSource()` to re-compile the sources as if DevTools was off and
// without debugging ability.
const urlMap = {};
for (
const url of
this.dbg.findSourceURLs()) {
if (url !==
"self-hosted") {
if (!urlMap[url]) {
urlMap[url] = { count: 0, sources: [], hasWasm:
false };
}
urlMap[url].count++;
}
}
const sources =
this.dbg.findSources();
for (
const source of sources) {
this._addSource(source);
if (source.introductionType ===
"wasm") {
const origURL = source.url.replace(/^wasm:/,
"");
if (urlMap[origURL]) {
urlMap[origURL].hasWasm =
true;
}
}
// 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, data.hasWasm);
}
}
}
sources() {
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.
const forms = [];
for (
const source of
this.sourcesManager.iter()) {
forms.push(source.form());
}
return forms;
}
/**
* 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();
}
}
removeAllBreakpoints() {
this.breakpointActorMap.removeAllBreakpoints();
}
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 ObjectActorPool(
this,
"pause",
true);
// 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 value.
* Depdending on if the thread is paused, the object actor may have a different lifetime:
* - when thread is paused, the object actor will be kept alive until the thread is resumed
* (which also happens when we step)
* - when thread is not paused, the object actor will be kept alive until the related target
* is destroyed (thread stops or devtools closes)
*
* @param value Debugger.Object|any
* A Debugger.Object for all JS objects, or any primitive JS type.
* @return The value's grip
* Primitive JS type, Object actor Form JSON object, or a JSON object to describe the value.
*/
createValueGrip(value) {
// When the thread is paused, all objects are stored in a transient pool
// which will be cleared on resume (which also happens when we step).
const pool =
this._pausePool ||
this.threadLifetimePool;
return createValueGrip(
this, value, pool);
}
_onWindowReady({ isTopLevel, isBFCache }) {
// 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();
}
// 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");
}
if (
this.shouldSkipAnyBreakpoint) {
return undefined;
}
const frame =
this.dbg.getNewestFrame();
if (!frame) {
return undefined;
}
if (
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 create the object actors late, from here because `_pausePool` is `null` beforehand,
// and the actors created by createValueGrip would otherwise be registered in the thread lifetime pool
pkt.why.nodeGrip =
this.createValueGrip(targetObj);
pkt.why.ancestorGrip = ancestorObj
?
this.createValueGrip(ancestorObj)
:
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. breakpoints are disabled
// 2. we have not moved since the last pause
// 3. the source is blackboxed
// 4. there is a breakpoint at the same location
if (
this.shouldSkipAnyBreakpoint ||
!
this.hasMoved(frame,
"debuggerStatement") ||
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;
}
// Ignore shouldSkipAnyBreakpoint if we are explicitly requested to do so.
// Typically, when we are evaluating conditional breakpoints, we want to report any exception.
if (
this.shouldSkipAnyBreakpoint &&
!
this.insideClientEvaluation?.reportExceptionsWhenBreaksAreDisabled
) {
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.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:
this.createValueGrip(value),
};
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
--> --------------------
--> maximum size reached
--> --------------------