147 lines
5.2 KiB
JavaScript
147 lines
5.2 KiB
JavaScript
/* 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 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 }) {
|
|
// The Browser Toolbox uses the Content Process target's Thread actor to debug all scripts
|
|
// running into a given process. This includes WindowGlobal scripts.
|
|
// Because of this, and in such configuration, we have to ignore the WindowGlobal targets.
|
|
if (
|
|
targetActor.sessionContext.type == "all" &&
|
|
!targetActor.sessionContext.enableWindowGlobalThreadActors &&
|
|
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([
|
|
{
|
|
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([
|
|
{
|
|
state: STATES.RESUMED,
|
|
},
|
|
]);
|
|
}
|
|
}
|
|
|
|
module.exports = BreakpointWatcher;
|