Bug 1549999 - Use notification infrastructure to implement DOM event breakpoints. r=davidwalsh

Differential Revision: https://phabricator.services.mozilla.com/D31002

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Logan Smyth 2019-06-19 22:27:57 +00:00
parent 9bedd32999
commit bf92dc5ae0
18 changed files with 671 additions and 213 deletions

View File

@ -24,7 +24,7 @@ async function updateBreakpoints(dispatch, client, newEvents: string[]) {
active: newEvents,
};
client.setEventListenerBreakpoints(newEvents);
await client.setEventListenerBreakpoints(newEvents);
}
async function updateExpanded(dispatch, newExpanded: string[]) {
@ -46,7 +46,7 @@ export function addEventListenerBreakpoints(eventsToAdd: string[]) {
const newEvents = uniq([...eventsToAdd, ...activeListenerBreakpoints]);
updateBreakpoints(dispatch, client, newEvents);
await updateBreakpoints(dispatch, client, newEvents);
};
}
@ -59,7 +59,7 @@ export function removeEventListenerBreakpoints(eventsToRemove: string[]) {
event => !eventsToRemove.includes(event)
);
updateBreakpoints(dispatch, client, newEvents);
await updateBreakpoints(dispatch, client, newEvents);
};
}

View File

@ -48,7 +48,11 @@ export async function onConnect(connection: any, actions: Object) {
});
// Retrieve possible event listener breakpoints
actions.getEventListenerBreakpointTypes();
actions.getEventListenerBreakpointTypes().catch(e => console.error(e));
// Initialize the event breakpoints on the thread up front so that
// they are active once attached.
actions.addEventListenerBreakpoints([]).catch(e => console.error(e));
// In Firefox, we need to initially request all of the sources. This
// usually fires off individual `newSource` notifications as the

View File

@ -35,7 +35,10 @@ import type {
SourcesPacket,
} from "./types";
import type { EventListenerCategoryList } from "../../actions/types";
import type {
EventListenerCategoryList,
EventListenerActiveList,
} from "../../actions/types";
let workerClients: Object;
let threadClient: ThreadClient;
@ -43,6 +46,7 @@ let tabTarget: TabTarget;
let debuggerClient: DebuggerClient;
let sourceActors: { [ActorId]: SourceId };
let breakpoints: { [string]: Object };
let eventBreakpoints: ?EventListenerActiveList;
let supportsWasm: boolean;
let shouldWaitForWorkers = false;
@ -363,6 +367,8 @@ function interrupt(thread: string): Promise<*> {
}
async function setEventListenerBreakpoints(ids: string[]) {
eventBreakpoints = ids;
await threadClient.setActiveEventBreakpoints(ids);
await forEachWorkerThread(thread => thread.setActiveEventBreakpoints(ids));
}
@ -371,8 +377,7 @@ async function setEventListenerBreakpoints(ids: string[]) {
async function getEventListenerBreakpointTypes(): Promise<
EventListenerCategoryList
> {
const { value } = await threadClient.getAvailableEventBreakpoints();
return value;
return threadClient.getAvailableEventBreakpoints();
}
function pauseGrip(thread: string, func: Function): ObjectClient {
@ -406,6 +411,7 @@ async function fetchWorkers(): Promise<Worker[]> {
if (features.windowlessWorkers) {
const options = {
breakpoints,
eventBreakpoints,
observeAsmJS: true,
};

View File

@ -368,9 +368,7 @@ export type ThreadClient = {
request: (payload: Object) => Promise<*>,
url: string,
setActiveEventBreakpoints: (string[]) => void,
getAvailableEventBreakpoints: () => Promise<{|
value: EventListenerCategoryList,
|}>,
getAvailableEventBreakpoints: () => Promise<EventListenerCategoryList>,
skipBreakpoints: boolean => Promise<{| skip: boolean |}>,
};

View File

@ -91,7 +91,14 @@ class EventListeners extends Component<Props> {
<input
type="checkbox"
value={category.name}
onChange={e => this.onCategoryClick(category, e.target.checked)}
onChange={e => {
this.onCategoryClick(
category,
// Clicking an indeterminate checkbox should always have the
// effect of disabling any selected items.
indeterminate ? false : e.target.checked
);
}}
checked={checked}
ref={el => el && (el.indeterminate = indeterminate)}
/>

View File

@ -46,19 +46,19 @@ function update(
}
export function getActiveEventListeners(state: State): EventListenerActiveList {
return state.eventListenerBreakpoints.active;
return state.eventListenerBreakpoints.active || [];
}
export function getEventListenerBreakpointTypes(
state: State
): EventListenerCategoryList {
return state.eventListenerBreakpoints.categories;
return state.eventListenerBreakpoints.categories || [];
}
export function getEventListenerExpanded(
state: State
): EventListenerExpandedList {
return state.eventListenerBreakpoints.expanded;
return state.eventListenerBreakpoints.expanded || [];
}
export default update;

View File

@ -16,6 +16,7 @@ const reasons = {
exception: "whyPaused.exception",
resumeLimit: "whyPaused.resumeLimit",
breakpointConditionThrown: "whyPaused.breakpointConditionThrown",
eventBreakpoint: "whyPaused.eventBreakpoint",
// V8
DOM: "whyPaused.breakpoint",

View File

@ -593,6 +593,8 @@ support-files =
examples/html-breakpoints-slow.js
examples/sjs_slow-load.sjs
examples/fetch.js
examples/doc-event-breakpoints.html
examples/event-breakpoints.js
examples/doc-xhr.html
examples/doc-xhr-run-to-completion.html
examples/doc-scroll-run-to-completion.html
@ -806,6 +808,7 @@ skip-if = true
[browser_dbg-worker-scopes.js]
skip-if = (os == 'linux' && debug) || ccov #Bug 1456013
[browser_dbg-event-handler.js]
[browser_dbg-event-breakpoints.js]
[browser_dbg-eval-throw.js]
[browser_dbg-sourceURL-breakpoint.js]
[browser_dbg-old-breakpoint.js]

View File

@ -0,0 +1,48 @@
/* 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/>. */
function assertPauseLocation(dbg, line) {
const { location } = dbg.selectors.getVisibleSelectedFrame();
const source = findSource(dbg, "event-breakpoints.js");
is(location.sourceId, source.id, `correct sourceId`);
is(location.line, line, `correct line`);
assertPausedLocation(dbg);
}
add_task(async function() {
await pushPref("devtools.debugger.features.event-listeners-breakpoints", true);
const dbg = await initDebugger("doc-event-breakpoints.html", "event-breakpoints");
await selectSource(dbg, "event-breakpoints");
await waitForSelectedSource(dbg, "event-breakpoints");
await dbg.actions.addEventListenerBreakpoints([
"event.mouse.click",
"event.xhr.load",
"timer.timeout.set",
"timer.timeout.fire",
]);
invokeInTab("clickHandler");
await waitForPaused(dbg);
assertPauseLocation(dbg, 12);
await resume(dbg);
invokeInTab("xhrHandler");
await waitForPaused(dbg);
assertPauseLocation(dbg, 20);
await resume(dbg);
invokeInTab("timerHandler");
await waitForPaused(dbg);
assertPauseLocation(dbg, 27);
await resume(dbg);
await waitForPaused(dbg);
assertPauseLocation(dbg, 29);
await resume(dbg);
});

View File

@ -0,0 +1,20 @@
<!-- 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/. -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Debugger event bp test page</title>
</head>
<body>
<button id="click-button">Run Click Handler</button>
<button id="xhr-button">Run XHR Handler</button>
<button id="timer-button">Run Timer Handler</button>
<div id="click-target" style="margin: 50px; background-color: green;">
Click Target
</div>
<script src="event-breakpoints.js"></script>
</body>
</html>

View File

@ -0,0 +1,34 @@
/* 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/>. */
document.getElementById("click-button").onmousedown = clickHandler;
function clickHandler() {
document.getElementById("click-target").click();
}
document.getElementById("click-target").onclick = clickTargetClicked;
function clickTargetClicked() {
console.log("clicked");
}
document.getElementById("xhr-button").onmousedown = xhrHandler;
function xhrHandler() {
var xhr = new XMLHttpRequest();
xhr.open("GET", "doc-event-breakpoints.html", true);
xhr.onload = function(){
console.log("xhr load");
};
xhr.send();
}
document.getElementById("timer-button").onmousedown = timerHandler;
function timerHandler() {
setTimeout(
() => {
console.log("timer callback");
},
50
);
console.log("timer set");
}

View File

@ -983,6 +983,11 @@ whyPaused.debuggerStatement=Paused on debugger statement
# in a info block explaining how the debugger is currently paused on a breakpoint
whyPaused.breakpoint=Paused on breakpoint
# LOCALIZATION NOTE (whyPaused.eventBreakpoint): The text that is displayed
# in a info block explaining how the debugger is currently paused on an event
# breakpoint.
whyPaused.eventBreakpoint=Paused on event breakpoint
# LOCALIZATION NOTE (whyPaused.exception): The text that is displayed
# in a info block explaining how the debugger is currently paused on an exception
whyPaused.exception=Paused on exception

View File

@ -6,6 +6,7 @@
"use strict";
const DebuggerNotificationObserver = require("DebuggerNotificationObserver");
const Services = require("Services");
const { Cr, Ci } = require("chrome");
const { ActorPool } = require("devtools/server/actors/common");
@ -16,6 +17,8 @@ const { assert, dumpn } = DevToolsUtils;
const { threadSpec } = require("devtools/shared/specs/thread");
const {
getAvailableEventBreakpoints,
eventBreakpointForNotification,
makeEventBreakpointMessage,
} = require("devtools/server/actors/utils/event-breakpoints");
loader.lazyRequireGetter(this, "EnvironmentActor", "devtools/server/actors/environment", true);
@ -61,7 +64,8 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
this._scripts = null;
this._xhrBreakpoints = [];
this._observingNetwork = false;
this._eventBreakpoints = [];
this._activeEventBreakpoints = new Set();
this._activeEventPause = null;
this._priorPause = null;
@ -94,6 +98,10 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
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._eventBreakpointListener = this._eventBreakpointListener.bind(this);
this._debuggerNotificationObserver = new DebuggerNotificationObserver();
if (Services.obs) {
// Set a wrappedJSObject property so |this| can be sent via the observer svc
@ -112,6 +120,7 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
this._dbg.uncaughtExceptionHook = this.uncaughtExceptionHook;
this._dbg.onDebuggerStatement = this.onDebuggerStatement;
this._dbg.onNewScript = this.onNewScript;
this._dbg.onNewDebuggee = this._onNewDebuggee;
if (this._dbg.replaying) {
this._dbg.replayingOnForcedPause = this.replayingOnForcedPause.bind(this);
const sendProgress = throttle((recording, executionPoint) => {
@ -223,6 +232,16 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
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);
} catch (e) { }
}
this.sources.off("newSource", this.onNewSourceEvent);
this.clearDebuggees();
this.conn.removeActorPool(this._threadLifetimePool);
@ -285,6 +304,9 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
if (options.breakpoints) {
this._setBreakpointsOnAttach(options.breakpoints);
}
if (options.eventBreakpoints) {
this.setActiveEventBreakpoints(options.eventBreakpoints);
}
this.dbg.addDebuggees();
this.dbg.enabled = true;
@ -400,10 +422,24 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
return getAvailableEventBreakpoints();
},
getActiveEventBreakpoints: function() {
return this._eventBreakpoints;
return Array.from(this._activeEventBreakpoints);
},
setActiveEventBreakpoints: function(ids) {
this._eventBreakpoints = ids;
this._activeEventBreakpoints = new Set(ids);
if (this._activeEventBreakpoints.size === 0) {
this._debuggerNotificationObserver.removeListener(
this._eventBreakpointListener);
} else {
this._debuggerNotificationObserver.addListener(
this._eventBreakpointListener);
}
},
_onNewDebuggee(global) {
try {
this._debuggerNotificationObserver.connect(global);
} catch (e) { }
},
_updateNetworkObserver() {
@ -513,6 +549,81 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
return {};
},
_eventBreakpointListener(notification) {
if (this._state === "paused" || this._state === "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) {
const { sourceActor } = this.sources.getFrameLocation(frame);
const url = sourceActor.url;
if (this.sources.isBlackBoxed(url)) {
return;
}
this._pauseAndRespondEventBreakpoint(frame, eventBreakpoint);
}
}
},
_makeEventBreakpointEnterFrame(eventBreakpoint) {
return frame => {
const { sourceActor } = this.sources.getFrameLocation(frame);
const url = sourceActor.url;
if (this.sources.isBlackBoxed(url)) {
return undefined;
}
this._restoreDebuggerHooks(this._activeEventPause);
this._activeEventPause = null;
return this._pauseAndRespondEventBreakpoint(frame, eventBreakpoint);
};
},
_pauseAndRespondEventBreakpoint(frame, eventBreakpoint) {
if (this.skipBreakpoints) {
return undefined;
}
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.
@ -575,7 +686,7 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
return this._parentClosed ? null : undefined;
},
_makeOnEnterFrame: function({ thread, pauseAndRespond }) {
_makeOnEnterFrame: function({ pauseAndRespond }) {
return frame => {
const { sourceActor } = this.sources.getFrameLocation(frame);
@ -590,13 +701,13 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
}
// Continue forward until we get to a valid step target.
const { onStep, onPop } = thread._makeSteppingHooks(
const { onStep, onPop } = this._makeSteppingHooks(
null, "next", false, null
);
if (thread.dbg.replaying) {
if (this.dbg.replaying) {
const offsets =
thread._findReplayingStepOffsets(null, frame,
this._findReplayingStepOffsets(null, frame,
/* rewinding = */ false);
frame.setReplayingOnStep(onStep, offsets);
} else {
@ -608,7 +719,8 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
};
},
_makeOnPop: function({ thread, pauseAndRespond, startLocation, steppingType }) {
_makeOnPop: function({ pauseAndRespond, startLocation, steppingType }) {
const thread = this;
const result = function(completion) {
// onPop is called with 'this' set to the current frame.
const location = thread.sources.getFrameLocation(this);
@ -728,8 +840,9 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
return script.getOffsetMetadata(offset).isStepStart;
},
_makeOnStep: function({ thread, pauseAndRespond, startFrame,
_makeOnStep: function({ pauseAndRespond, startFrame,
startLocation, steppingType, completion, rewinding }) {
const thread = this;
return function() {
// onStep is called with 'this' set to the current frame.
@ -828,7 +941,6 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
{ type: "resumeLimit" },
onPacket
),
thread: this,
startFrame: this.youngestFrame,
startLocation: startLocation,
steppingType: steppingType,

View File

@ -6,6 +6,401 @@
"use strict";
function generalEvent(groupID, eventType) {
return {
id: `event.${groupID}.${eventType}`,
type: "event",
name: eventType,
message: `DOM '${eventType}' event`,
eventType,
filter: "general",
};
}
function nodeEvent(groupID, eventType) {
return {
...generalEvent(groupID, eventType),
filter: "node",
};
}
function mediaNodeEvent(groupID, eventType) {
return {
...generalEvent(groupID, eventType),
filter: "media",
};
}
function globalEvent(groupID, eventType) {
return {
...generalEvent(groupID, eventType),
message: `Global '${eventType}' event`,
filter: "global",
};
}
function xhrEvent(groupID, eventType) {
return {
...generalEvent(groupID, eventType),
message: `XHR '${eventType}' event`,
filter: "xhr",
};
}
function workerEvent(eventType) {
return {
...generalEvent("worker", eventType),
message: `Worker '${eventType}' event`,
filter: "worker",
};
}
function timerEvent(type, operation, name, notificationType) {
return {
id: `timer.${type}.${operation}`,
type: "simple",
name,
message: name,
notificationType,
};
}
function animationEvent(operation, name, notificationType) {
return {
id: `animationframe.${operation}`,
type: "simple",
name,
message: name,
notificationType,
};
}
const AVAILABLE_BREAKPOINTS = [
{
name: "Animation",
items: [
animationEvent(
"request",
"Request Animation Frame",
"requestAnimationFrame"
),
animationEvent(
"cancel",
"Cancel Animation Frame",
"cancelAnimationFrame"
),
animationEvent(
"fire",
"Animation Frame fired",
"requestAnimationFrameCallback"
),
],
},
{
name: "Clipboard",
items: [
generalEvent("clipboard", "copy"),
generalEvent("clipboard", "cut"),
generalEvent("clipboard", "paste"),
generalEvent("clipboard", "beforecopy"),
generalEvent("clipboard", "beforecut"),
generalEvent("clipboard", "beforepaste"),
],
},
{
name: "Control",
items: [
generalEvent("control", "resize"),
generalEvent("control", "scroll"),
generalEvent("control", "zoom"),
generalEvent("control", "focus"),
generalEvent("control", "blur"),
generalEvent("control", "select"),
generalEvent("control", "change"),
generalEvent("control", "submit"),
generalEvent("control", "reset"),
],
},
{
name: "DOM Mutation",
items: [
// Deprecated DOM events.
nodeEvent("dom-mutation", "DOMActivate"),
nodeEvent("dom-mutation", "DOMFocusIn"),
nodeEvent("dom-mutation", "DOMFocusOut"),
// Standard DOM mutation events.
nodeEvent("dom-mutation", "DOMAttrModified"),
nodeEvent("dom-mutation", "DOMCharacterDataModified"),
nodeEvent("dom-mutation", "DOMNodeInserted"),
nodeEvent("dom-mutation", "DOMNodeInsertedIntoDocument"),
nodeEvent("dom-mutation", "DOMNodeRemoved"),
nodeEvent("dom-mutation", "DOMNodeRemovedIntoDocument"),
nodeEvent("dom-mutation", "DOMSubtreeModified"),
// DOM load events.
nodeEvent("dom-mutation", "DOMContentLoaded"),
],
},
{
name: "Device",
items: [
globalEvent("device", "deviceorientation"),
globalEvent("device", "devicemotion"),
],
},
{
name: "Drag and Drop",
items: [
generalEvent("drag-and-drop", "drag"),
generalEvent("drag-and-drop", "dragstart"),
generalEvent("drag-and-drop", "dragend"),
generalEvent("drag-and-drop", "dragenter"),
generalEvent("drag-and-drop", "dragover"),
generalEvent("drag-and-drop", "dragleave"),
generalEvent("drag-and-drop", "drop"),
],
},
{
name: "Keyboard",
items: [
generalEvent("keyboard", "keydown"),
generalEvent("keyboard", "keyup"),
generalEvent("keyboard", "keypress"),
generalEvent("keyboard", "input"),
],
},
{
name: "Load",
items: [
globalEvent("load", "load"),
globalEvent("load", "beforeunload"),
globalEvent("load", "unload"),
globalEvent("load", "abort"),
globalEvent("load", "error"),
globalEvent("load", "hashchange"),
globalEvent("load", "popstate"),
],
},
{
name: "Media",
items: [
mediaNodeEvent("media", "play"),
mediaNodeEvent("media", "pause"),
mediaNodeEvent("media", "playing"),
mediaNodeEvent("media", "canplay"),
mediaNodeEvent("media", "canplaythrough"),
mediaNodeEvent("media", "seeking"),
mediaNodeEvent("media", "seeked"),
mediaNodeEvent("media", "timeupdate"),
mediaNodeEvent("media", "ended"),
mediaNodeEvent("media", "ratechange"),
mediaNodeEvent("media", "durationchange"),
mediaNodeEvent("media", "volumechange"),
mediaNodeEvent("media", "loadstart"),
mediaNodeEvent("media", "progress"),
mediaNodeEvent("media", "suspend"),
mediaNodeEvent("media", "abort"),
mediaNodeEvent("media", "error"),
mediaNodeEvent("media", "emptied"),
mediaNodeEvent("media", "stalled"),
mediaNodeEvent("media", "loadedmetadata"),
mediaNodeEvent("media", "loadeddata"),
mediaNodeEvent("media", "waiting"),
],
},
{
name: "Mouse",
items: [
generalEvent("mouse", "auxclick"),
generalEvent("mouse", "click"),
generalEvent("mouse", "dblclick"),
generalEvent("mouse", "mousedown"),
generalEvent("mouse", "mouseup"),
generalEvent("mouse", "mouseover"),
generalEvent("mouse", "mousemove"),
generalEvent("mouse", "mouseout"),
generalEvent("mouse", "mouseenter"),
generalEvent("mouse", "mouseleave"),
generalEvent("mouse", "mousewheel"),
generalEvent("mouse", "wheel"),
generalEvent("mouse", "contextmenu"),
],
},
{
name: "Pointer",
items: [
generalEvent("pointer", "pointerover"),
generalEvent("pointer", "pointerout"),
generalEvent("pointer", "pointerenter"),
generalEvent("pointer", "pointerleave"),
generalEvent("pointer", "pointerdown"),
generalEvent("pointer", "pointerup"),
generalEvent("pointer", "pointermove"),
generalEvent("pointer", "pointercancel"),
generalEvent("pointer", "gotpointercapture"),
generalEvent("pointer", "lostpointercapture"),
],
},
{
name: "Timer",
items: [
timerEvent("timeout", "set", "setTimeout", "setTimeout"),
timerEvent("timeout", "clear", "clearTimeout", "clearTimeout"),
timerEvent("timeout", "fire", "setTimeout fired", "setTimeoutCallback"),
timerEvent("interval", "set", "setInterval", "setInterval"),
timerEvent("interval", "clear", "clearInterval", "clearInterval"),
timerEvent(
"interval",
"fire",
"setInterval fired",
"setIntervalCallback"
),
],
},
{
name: "Touch",
items: [
generalEvent("touch", "touchstart"),
generalEvent("touch", "touchmove"),
generalEvent("touch", "touchend"),
generalEvent("touch", "touchcancel"),
],
},
{
name: "Worker",
items: [
workerEvent("message"),
workerEvent("messageerror"),
],
},
{
name: "XHR",
items: [
xhrEvent("xhr", "readystatechange"),
xhrEvent("xhr", "load"),
xhrEvent("xhr", "loadstart"),
xhrEvent("xhr", "loadend"),
xhrEvent("xhr", "abort"),
xhrEvent("xhr", "error"),
xhrEvent("xhr", "progress"),
xhrEvent("xhr", "timeout"),
],
},
];
const FLAT_EVENTS = [];
for (const category of AVAILABLE_BREAKPOINTS) {
for (const event of category.items) {
FLAT_EVENTS.push(event);
}
}
const EVENTS_BY_ID = {};
for (const event of FLAT_EVENTS) {
if (EVENTS_BY_ID[event.id]) {
throw new Error("Duplicate event ID detected: " + event.id);
}
EVENTS_BY_ID[event.id] = event;
}
const SIMPLE_EVENTS = {};
const DOM_EVENTS = {};
for (const eventBP of FLAT_EVENTS) {
if (eventBP.type === "simple") {
const { notificationType } = eventBP;
if (SIMPLE_EVENTS[notificationType]) {
throw new Error("Duplicate simple event");
}
SIMPLE_EVENTS[notificationType] = eventBP.id;
} else if (eventBP.type === "event") {
const { eventType, filter } = eventBP;
let targetTypes;
if (filter === "global") {
targetTypes = ["global"];
} else if (filter === "xhr") {
targetTypes = ["xhr"];
} else if (filter === "worker") {
targetTypes = ["worker"];
} else if (filter === "general") {
targetTypes = ["global", "node"];
} else if (filter === "node" || filter === "media") {
targetTypes = ["node"];
} else {
throw new Error("Unexpected filter type");
}
for (const targetType of targetTypes) {
let byEventType = DOM_EVENTS[targetType];
if (!byEventType) {
byEventType = {};
DOM_EVENTS[targetType] = byEventType;
}
if (byEventType[eventType]) {
throw new Error("Duplicate dom event: " + eventType);
}
byEventType[eventType] = eventBP.id;
}
} else {
throw new Error("Unknown type: " + eventBP.type);
}
}
exports.eventBreakpointForNotification = eventBreakpointForNotification;
function eventBreakpointForNotification(dbg, notification) {
const notificationType = notification.type;
if (notification.type === "domEvent") {
const domEventNotification = DOM_EVENTS[notification.targetType];
if (!domEventNotification) {
return null;
}
// The 'event' value is a cross-compartment wrapper for the DOM Event object.
// While we could use that directly in the main thread as an Xray wrapper,
// when debugging workers we can't, because it is an opaque wrapper.
// To make things work, we have to always interact with the Event object via
// the Debugger.Object interface.
const evt = dbg
.makeGlobalObjectReference(notification.global)
.makeDebuggeeValue(notification.event);
const eventType = evt.getProperty("type").return;
const id = domEventNotification[eventType];
if (!id) {
return null;
}
const eventBreakpoint = EVENTS_BY_ID[id];
if (eventBreakpoint.filter === "media") {
const currentTarget = evt.getProperty("currentTarget").return;
if (!currentTarget) {
return null;
}
const nodeType = currentTarget.getProperty("nodeType").return;
const namespaceURI = currentTarget.getProperty("namespaceURI").return;
if (
nodeType !== 1 /* ELEMENT_NODE */ ||
namespaceURI !== "http://www.w3.org/1999/xhtml"
) {
return null;
}
const nodeName =
currentTarget.getProperty("nodeName").return.toLowerCase();
if (nodeName !== "audio" && nodeName !== "video") {
return null;
}
}
return id;
}
return SIMPLE_EVENTS[notificationType] || null;
}
exports.makeEventBreakpointMessage = makeEventBreakpointMessage;
function makeEventBreakpointMessage(id) {
return EVENTS_BY_ID[id].message;
}
exports.getAvailableEventBreakpoints = getAvailableEventBreakpoints;
function getAvailableEventBreakpoints() {
const available = [];
@ -14,196 +409,9 @@ function getAvailableEventBreakpoints() {
name,
events: items.map(item => ({
id: item.id,
name: item.eventType,
name: item.name,
})),
});
}
return available;
}
function event(groupID, name, filter = "content") {
return {
id: `${groupID}.event.${name}`,
type: "event",
eventType: name,
filter,
};
}
const AVAILABLE_BREAKPOINTS = [
{
name: "Clipboard",
items: [
event("clipboard", "copy"),
event("clipboard", "cut"),
event("clipboard", "paste"),
event("clipboard", "beforecopy"),
event("clipboard", "beforecut"),
event("clipboard", "beforepaste"),
],
},
{
name: "Control",
items: [
event("control", "resize"),
event("control", "scroll"),
event("control", "zoom"),
event("control", "focus"),
event("control", "blur"),
event("control", "select"),
event("control", "change"),
event("control", "submit"),
event("control", "reset"),
],
},
{
name: "DOM Mutation",
items: [
// Deprecated DOM events.
event("dom-mutation", "DOMActivate"),
event("dom-mutation", "DOMFocusIn"),
event("dom-mutation", "DOMFocusOut"),
// Standard DOM mutation events.
event("dom-mutation", "DOMAttrModified"),
event("dom-mutation", "DOMCharacterDataModified"),
event("dom-mutation", "DOMNodeInserted"),
event("dom-mutation", "DOMNodeInsertedIntoDocument"),
event("dom-mutation", "DOMNodeRemoved"),
event("dom-mutation", "DOMNodeRemovedIntoDocument"),
event("dom-mutation", "DOMSubtreeModified"),
// DOM load events.
event("dom-mutation", "DOMContentLoaded"),
],
},
{
name: "Device",
items: [
event("device", "deviceorientation"),
event("device", "devicemotion"),
],
},
{
name: "Drag and Drop",
items: [
event("drag-and-drop", "drag"),
event("drag-and-drop", "dragstart"),
event("drag-and-drop", "dragend"),
event("drag-and-drop", "dragenter"),
event("drag-and-drop", "dragover"),
event("drag-and-drop", "dragleave"),
event("drag-and-drop", "drop"),
],
},
{
name: "Keyboard",
items: [
event("keyboard", "keydown"),
event("keyboard", "keyup"),
event("keyboard", "keypress"),
event("keyboard", "input"),
],
},
{
name: "Load",
items: [
event("load", "load", "global"),
event("load", "beforeunload", "global"),
event("load", "unload", "global"),
event("load", "abort", "global"),
event("load", "error", "global"),
event("load", "hashchange", "global"),
event("load", "popstate", "global"),
],
},
{
name: "Media",
items: [
event("media", "play", "media"),
event("media", "pause", "media"),
event("media", "playing", "media"),
event("media", "canplay", "media"),
event("media", "canplaythrough", "media"),
event("media", "seeking", "media"),
event("media", "seeked", "media"),
event("media", "timeupdate", "media"),
event("media", "ended", "media"),
event("media", "ratechange", "media"),
event("media", "durationchange", "media"),
event("media", "volumechange", "media"),
event("media", "loadstart", "media"),
event("media", "progress", "media"),
event("media", "suspend", "media"),
event("media", "abort", "media"),
event("media", "error", "media"),
event("media", "emptied", "media"),
event("media", "stalled", "media"),
event("media", "loadedmetadata", "media"),
event("media", "loadeddata", "media"),
event("media", "waiting", "media"),
],
},
{
name: "Mouse",
items: [
event("mouse", "auxclick"),
event("mouse", "click"),
event("mouse", "dblclick"),
event("mouse", "mousedown"),
event("mouse", "mouseup"),
event("mouse", "mouseover"),
event("mouse", "mousemove"),
event("mouse", "mouseout"),
event("mouse", "mouseenter"),
event("mouse", "mouseleave"),
event("mouse", "mousewheel"),
event("mouse", "wheel"),
event("mouse", "contextmenu"),
],
},
{
name: "Pointer",
items: [
event("pointer", "pointerover"),
event("pointer", "pointerout"),
event("pointer", "pointerenter"),
event("pointer", "pointerleave"),
event("pointer", "pointerdown"),
event("pointer", "pointerup"),
event("pointer", "pointermove"),
event("pointer", "pointercancel"),
event("pointer", "gotpointercapture"),
event("pointer", "lostpointercapture"),
],
},
{
name: "Touch",
items: [
event("touch", "touchstart"),
event("touch", "touchmove"),
event("touch", "touchend"),
event("touch", "touchcancel"),
],
},
{
name: "Worker",
items: [
event("worker", "message", "global"),
event("worker", "messageerror", "global"),
],
},
{
name: "XHR",
items: [
event("xhr", "readystatechange", "xhr"),
event("xhr", "load", "xhr"),
event("xhr", "loadstart", "xhr"),
event("xhr", "loadend", "xhr"),
event("xhr", "abort", "xhr"),
event("xhr", "error", "xhr"),
event("xhr", "progress", "xhr"),
event("xhr", "timeout", "xhr"),
],
},
];

View File

@ -64,15 +64,23 @@ module.exports = function makeDebugger({ findDebuggees, shouldAddNewGlobalAsDebu
dbg.allowUnobservedAsmJS = true;
dbg.uncaughtExceptionHook = reportDebuggerHookException;
function onNewDebuggee(global) {
if (dbg.onNewDebuggee) {
dbg.onNewDebuggee(global);
}
}
dbg.onNewGlobalObject = function(global) {
if (shouldAddNewGlobalAsDebuggee(global)) {
safeAddDebuggee(this, global);
onNewDebuggee(global);
}
};
dbg.addDebuggees = function() {
for (const global of findDebuggees(this)) {
safeAddDebuggee(this, global);
onNewDebuggee(global);
}
};

View File

@ -23,6 +23,7 @@ const systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
// Steal various globals only available in JSM scope (and not Sandbox one)
const {
console,
DebuggerNotificationObserver,
DOMPoint,
DOMQuad,
DOMRect,
@ -229,6 +230,7 @@ function lazyRequireGetter(obj, property, module, destructure) {
// List of pseudo modules exposed to all devtools modules.
exports.modules = {
ChromeUtils,
DebuggerNotificationObserver,
HeapSnapshot,
promise,
// Expose "chrome" Promise, which aren't related to any document

View File

@ -44,6 +44,7 @@ const UnsolicitedPauses = {
breakpoint: "breakpoint",
DOMEvent: "DOMEvent",
watchpoint: "watchpoint",
eventBreakpoint: "eventBreakpoint",
exception: "exception",
replayForcedPause: "replayForcedPause",
};

View File

@ -4,7 +4,7 @@
"use strict";
/* global worker */
/* global worker, DebuggerNotificationObserver */
// A CommonJS module loader that is designed to run inside a worker debugger.
// We can't simply use the SDK module loader, because it relies heavily on
@ -576,6 +576,7 @@ this.worker = new WorkerDebuggerLoader({
"chrome": chrome,
"xpcInspector": xpcInspector,
"ChromeUtils": ChromeUtils,
"DebuggerNotificationObserver": DebuggerNotificationObserver,
},
paths: {
// ⚠ DISCUSSION ON DEV-DEVELOPER-TOOLS REQUIRED BEFORE MODIFYING ⚠