/* 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 DebuggerNotificationObserver = require("DebuggerNotificationObserver"); const Services = require("Services"); const { Cr, Ci } = require("chrome"); const { Pool } = require("devtools/shared/protocol/Pool"); const { createValueGrip } = require("devtools/server/actors/object/utils"); const { ActorClassWithSpec, Actor } = require("devtools/shared/protocol"); const DevToolsUtils = require("devtools/shared/DevToolsUtils"); const Debugger = require("Debugger"); const { assert, dumpn, reportException } = DevToolsUtils; const { threadSpec } = require("devtools/shared/specs/thread"); const { getAvailableEventBreakpoints, eventBreakpointForNotification, eventsRequireNotifications, firstStatementBreakpointId, makeEventBreakpointMessage, } = require("devtools/server/actors/utils/event-breakpoints"); const { WatchpointMap, } = require("devtools/server/actors/utils/watchpoint-map"); const { getDebuggerSourceURL, } = require("devtools/server/actors/utils/source-url"); const { logEvent } = require("devtools/server/actors/utils/logEvent"); loader.lazyRequireGetter( this, "EnvironmentActor", "devtools/server/actors/environment", true ); loader.lazyRequireGetter( this, "BreakpointActorMap", "devtools/server/actors/utils/breakpoint-actor-map", true ); loader.lazyRequireGetter( this, "PauseScopedObjectActor", "devtools/server/actors/pause-scoped", true ); loader.lazyRequireGetter( this, "EventLoopStack", "devtools/server/actors/utils/event-loop", true ); loader.lazyRequireGetter( this, ["FrameActor", "getSavedFrameParent", "isValidSavedFrame"], "devtools/server/actors/frame", true ); loader.lazyRequireGetter( this, "HighlighterEnvironment", "devtools/server/actors/highlighters", true ); loader.lazyRequireGetter( this, "PausedDebuggerOverlay", "devtools/server/actors/highlighters/paused-debugger", 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 > 0 && (!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", }; /** * JSD2 actors. */ /** * Creates a ThreadActor. * * ThreadActors manage a JSInspector object and manage execution/inspection * of debuggees. * * @param aParent object * This |ThreadActor|'s parent actor. It must implement the following * properties: * - url: The URL string of the debuggee. * - window: The global window object. * - preNest: Function called before entering a nested event loop. * - postNest: Function called after exiting a nested event loop. * - dbg: a Debugger instance that manages its globals on its own. * @param aGlobal object [optional] * An optional (for content debugging only) reference to the content * window. */ const ThreadActor = ActorClassWithSpec(threadSpec, { initialize(parent, global) { Actor.prototype.initialize.call(this, parent.conn); this._state = STATES.DETACHED; this._frameActors = []; this._parent = parent; this._dbg = null; this._gripDepth = 0; this._threadLifetimePool = null; this._parentClosed = false; this._xhrBreakpoints = []; this._observingNetwork = false; this._activeEventBreakpoints = new Set(); this._activeEventPause = null; this._pauseOverlay = null; this._priorPause = null; this._frameActorMap = new WeakMap(); this._watchpointsMap = new WatchpointMap(this); this._options = { skipBreakpoints: false, }; this.breakpointActorMap = new BreakpointActorMap(this); 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.global = global; 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) ); }, /** * Keep track of all of the nested event loops we use to pause the debuggee * when we hit a breakpoint/debugger statement/etc in one place so we can * resolve them when we get resume packets. We have more than one (and keep * them in a stack) because we can pause within client evals. */ _threadPauseEventLoops: null, _pushThreadPause() { if (!this._threadPauseEventLoops) { this._threadPauseEventLoops = []; } const eventLoop = this._nestedEventLoops.push(); this._threadPauseEventLoops.push(eventLoop); eventLoop.enter(); }, _popThreadPause() { const eventLoop = this._threadPauseEventLoops.pop(); assert(eventLoop, "Should have an event loop."); eventLoop.resolve(); }, isPaused() { return this._state === STATES.PAUSED; }, /** * 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; Actor.prototype.destroy.call(this); }, // Request handlers attach(options) { 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); // Initialize an event loop stack. This can't be done in the constructor, // because this.conn is not yet initialized by the actor pool at that time. this._nestedEventLoops = new EventLoopStack({ thread: this, }); this.dbg.enable(); this.reconfigure(options); // Set everything up so that breakpoint can work this._state = STATES.RUNNING; // 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() { // The CanvasFrameAnonymousContentHelper class we're using to create the paused overlay // need to have access to a documentElement. // Accept only browsing context target which exposes such element, but ignore // privileged document (top level window, special about:* pages, …). return ( // We might have access to a non-chrome window getter that is a Sandox (e.g. in the // case of ContentProcessTargetActor). this._parent.window?.document?.documentElement && !this._parent.window.isChromeWindow ); }, async showOverlay() { if ( this._options.shouldShowOverlay && 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) { const 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(); }, getActiveEventBreakpoints() { return Array.from(this._activeEventBreakpoints); }, 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 > 0 && !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: "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 ( "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: "eventBreakpoint", 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 'allowSource' // check and thus has no actor, we do not bother pausing. return undefined; } packet.frame.where = { actor: sourceActor.actorID, line: line, column: 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._pushThreadPause(); } 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: "resumeLimit" }, 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 no-fallthrough case "finish": stepFrame.onStep = createStepForReactionTracking(stepFrame.onStep); // eslint-disable 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._nestedEventLoops.size && this._nestedEventLoops.lastPausedThreadActor && this._nestedEventLoops.lastPausedThreadActor !== this ) { 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._popThreadPause(); // 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(); }, /** * Spin up a nested event loop so we can synchronously resolve a promise. * * DON'T USE THIS UNLESS YOU ABSOLUTELY MUST! Nested event loops suck: the * world's state can change out from underneath your feet because JS is no * longer run-to-completion. * * @param p * The promise we want to resolve. * @returns The promise's resolution. */ unsafeSynchronize(p) { let needNest = true; let eventLoop; let returnVal; p.then(resolvedVal => { needNest = false; returnVal = resolvedVal; }) .catch(e => reportException("unsafeSynchronize", e)) .then(() => { if (eventLoop) { eventLoop.resolve(); } }); if (needNest) { eventLoop = this._nestedEventLoops.push(); eventLoop.enter(); } return returnVal; }, /** * 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) { const sourceActor = this.sourcesManager.createSourceActor( frame.script.source ); if (!sourceActor) { continue; } } 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") { urlMap[url] = 1 + (urlMap[url] || 0); } } const sources = this.dbg.findSources(); for (const source of sources) { this._addSource(source); const url = getDebuggerSourceURL(source); if (url) { urlMap[url]--; } } // Resurrect any URLs for which not all sources are accounted for. for (const [url, count] of Object.entries(urlMap)) { if (count > 0) { this._resurrectSource(url); } } }, 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: "alreadyPaused" }, }); 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: "interrupted", onNext: true }); }; this.dbg.onEnterFrame = onEnterFrame; this.emit("willInterrupt"); 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: "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._pushThreadPause(); // 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. const poppedFrames = 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); } if (poppedFrames) { packet.poppedFrames = poppedFrames; } return packet; }, /** * Expire frame actors for frames that have been popped. * * @returns A list of actor IDs whose frames have been popped. */ _updateFrames() { const popped = []; // 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); } else { popped.push(frameActor.actorID); } } // 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; return popped; }, _createFrameActor(frame, depth) { let actor = this._frameActorMap.get(frame); if (!actor) { 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 }) { 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) { this.unsafeSynchronize(Promise.resolve(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: "mutationBreakpoint", 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: "debuggerStatement" }); }, 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: "exception", exception: createValueGrip(value, this._pausePool, this.objectGrip), }; this.emit("paused", packet); this._pushThreadPause(); } 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; }, /** * Add the provided source to the server cache. * * @param aSource Debugger.Source * The source that will be stored. * @returns true, if the source was added; false otherwise. */ _addSource(source) { if (!this.sourcesManager.allowSource(source)) { return false; } // 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); return true; }, /** * 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. */ async _resurrectSource(url) { 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(/]*>/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. const scripts = document.querySelectorAll("script"); for (const script of scripts) { if (script.src) { continue; } const text = script.innerText; for (const offset of scriptStartOffsets) { if (content.substring(offset, offset + text.length) == text) { const allLineBreaks = content.substring(0, offset).matchAll("\n"); const startLine = 1 + [...allLineBreaks].length; try { const global = this.dbg.getDebuggees()[0]; this._addSource( global.createSource({ text, url, startLine, isScriptElement: true, }) ); } catch (e) { // Ignore parse errors. } break; } } } // 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 (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; } };