/* 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"; const { TYPES: { THREAD_STATE }, } = require("resource://devtools/server/actors/resources/index.js"); const Targets = require("resource://devtools/server/actors/targets/index.js"); const { PAUSE_REASONS, STATES: THREAD_STATES, } = require("resource://devtools/server/actors/thread.js"); // Possible values of breakpoint's resource's `state` attribute const STATES = { PAUSED: "paused", RESUMED: "resumed", }; /** * Emit THREAD_STATE resources, which is emitted each time the target's thread pauses or resumes. * So that there is two distinct values for this resource: pauses and resumes. * These values are distinguished by `state` attribute which can be either "paused" or "resumed". * * Resume events, won't expose any other attribute other than `resourceType` and `state`. * * Pause events will expose the following attributes: * - why {Object}: Description of why the thread pauses. See ThreadActor's PAUSE_REASONS definition for more information. * - frame {Object}: Description of the frame where we just paused. This is a FrameActor's form. */ class BreakpointWatcher { constructor() { this.onPaused = this.onPaused.bind(this); this.onResumed = this.onResumed.bind(this); } /** * Start watching for state changes of the thread actor. * This will notify whenever the thread actor pause and resume. * * @param TargetActor targetActor * The target actor from which we should observe breakpoints * @param Object options * Dictionary object with following attributes: * - onAvailable: mandatory function * This will be called for each resource. */ async watch(targetActor, { onAvailable }) { // When debugging the whole browser (via the Browser Toolbox), we instantiate both content process and window global (FRAME) targets. // But the debugger will only use the content process target's thread actor. // Thread actor, Sources and Breakpoints have to be only managed for the content process target, // and we should explicitly ignore the window global target. if ( targetActor.sessionContext.type == "all" && targetActor.targetType === Targets.TYPES.FRAME && targetActor.typeName != "parentProcessTarget" ) { return; } const { threadActor } = targetActor; this.threadActor = threadActor; this.onAvailable = onAvailable; // If this watcher is created during target creation, attach the thread actor automatically. // Otherwise it would not pause on anything (especially debugger statements). // However, do not attach the thread actor for Workers. They use a codepath // which releases the worker on `attach`. For them, the client will call `attach`. (bug 1691986) const isTargetCreation = this.threadActor.state == THREAD_STATES.DETACHED; if (isTargetCreation && !targetActor.targetType.endsWith("worker")) { await this.threadActor.attach({}); } this.isInterrupted = false; threadActor.on("paused", this.onPaused); threadActor.on("resumed", this.onResumed); // For top-level targets, the thread actor may have been attached by the frontend // on toolbox opening, and we start observing for thread state updates much later. // In which case, the thread actor may already be paused and we handle this here. // It will also occurs for all other targets once bug 1681698 lands, // as the thread actor will be initialized before the target starts loading. // And it will occur for all targets once bug 1686748 lands. // // Note that we have to check if we have a "lastPausedPacket", // because the thread Actor is immediately set as being paused, // but the pause packet is built asynchronously and available slightly later. // If the "lastPausedPacket" is null, while the thread actor is paused, // it is fine to ignore as the "paused" event will be fire later. if (threadActor.isPaused() && threadActor.lastPausedPacket()) { this.onPaused(threadActor.lastPausedPacket()); } } /** * Stop watching for breakpoints */ destroy() { if (!this.threadActor) { return; } this.threadActor.off("paused", this.onPaused); this.threadActor.off("resumed", this.onResumed); } onPaused(packet) { // If paused by an explicit interrupt, which are generated by the // slow script dialog and internal events such as setting // breakpoints, ignore the event. const { why } = packet; if (why.type === PAUSE_REASONS.INTERRUPTED && !why.onNext) { this.isInterrupted = true; return; } // Ignore attached events because they are not useful to the user. if (why.type == PAUSE_REASONS.ALREADY_PAUSED) { return; } this.onAvailable([ { resourceType: THREAD_STATE, state: STATES.PAUSED, why, frame: packet.frame.form(), }, ]); } onResumed() { // NOTE: resumed events are suppressed while interrupted // to prevent unintentional behavior. if (this.isInterrupted) { this.isInterrupted = false; return; } this.onAvailable([ { resourceType: THREAD_STATE, state: STATES.RESUMED, }, ]); } } module.exports = BreakpointWatcher;