summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/resources
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/resources')
-rw-r--r--devtools/server/actors/resources/console-messages.js278
-rw-r--r--devtools/server/actors/resources/css-changes.js42
-rw-r--r--devtools/server/actors/resources/css-messages.js139
-rw-r--r--devtools/server/actors/resources/document-event.js112
-rw-r--r--devtools/server/actors/resources/error-messages.js192
-rw-r--r--devtools/server/actors/resources/extensions-backgroundscript-status.js68
-rw-r--r--devtools/server/actors/resources/index.js438
-rw-r--r--devtools/server/actors/resources/last-private-context-exit.js46
-rw-r--r--devtools/server/actors/resources/moz.build39
-rw-r--r--devtools/server/actors/resources/network-events-content.js258
-rw-r--r--devtools/server/actors/resources/network-events-stacktraces.js207
-rw-r--r--devtools/server/actors/resources/network-events.js392
-rw-r--r--devtools/server/actors/resources/parent-process-document-event.js177
-rw-r--r--devtools/server/actors/resources/platform-messages.js60
-rw-r--r--devtools/server/actors/resources/reflow.js63
-rw-r--r--devtools/server/actors/resources/server-sent-events.js135
-rw-r--r--devtools/server/actors/resources/sources.js97
-rw-r--r--devtools/server/actors/resources/storage-cache.js19
-rw-r--r--devtools/server/actors/resources/storage-cookie.js19
-rw-r--r--devtools/server/actors/resources/storage-indexed-db.js19
-rw-r--r--devtools/server/actors/resources/storage-local-storage.js19
-rw-r--r--devtools/server/actors/resources/storage-session-storage.js19
-rw-r--r--devtools/server/actors/resources/stylesheets.js138
-rw-r--r--devtools/server/actors/resources/thread-states.js136
-rw-r--r--devtools/server/actors/resources/utils/content-process-storage.js458
-rw-r--r--devtools/server/actors/resources/utils/moz.build14
-rw-r--r--devtools/server/actors/resources/utils/nsi-console-listener-watcher.js192
-rw-r--r--devtools/server/actors/resources/utils/parent-process-storage.js584
-rw-r--r--devtools/server/actors/resources/websockets.js196
29 files changed, 4556 insertions, 0 deletions
diff --git a/devtools/server/actors/resources/console-messages.js b/devtools/server/actors/resources/console-messages.js
new file mode 100644
index 0000000000..8ad97b366c
--- /dev/null
+++ b/devtools/server/actors/resources/console-messages.js
@@ -0,0 +1,278 @@
+/* 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: { CONSOLE_MESSAGE },
+} = require("devtools/server/actors/resources/index");
+const Targets = require("devtools/server/actors/targets/index");
+
+const consoleAPIListenerModule = isWorker
+ ? "devtools/server/actors/webconsole/worker-listeners"
+ : "devtools/server/actors/webconsole/listeners/console-api";
+const { ConsoleAPIListener } = require(consoleAPIListenerModule);
+
+const { isArray } = require("devtools/server/actors/object/utils");
+
+const {
+ makeDebuggeeValue,
+ createValueGripForTarget,
+} = require("devtools/server/actors/object/utils");
+
+const {
+ getActorIdForInternalSourceId,
+} = require("devtools/server/actors/utils/dbg-source");
+
+const {
+ isSupportedByConsoleTable,
+} = require("devtools/shared/webconsole/messages");
+
+/**
+ * Start watching for all console messages related to a given Target Actor.
+ * This will notify about existing console messages, but also the one created in future.
+ *
+ * @param TargetActor targetActor
+ * The target actor from which we should observe console messages
+ * @param Object options
+ * Dictionary object with following attributes:
+ * - onAvailable: mandatory function
+ * This will be called for each resource.
+ */
+class ConsoleMessageWatcher {
+ async watch(targetActor, { onAvailable }) {
+ // Bug 1642297: Maybe we could merge ConsoleAPI Listener into this module?
+ const onConsoleAPICall = message => {
+ onAvailable([
+ {
+ resourceType: CONSOLE_MESSAGE,
+ message: prepareConsoleMessageForRemote(targetActor, message),
+ },
+ ]);
+ };
+
+ const isTargetActorContentProcess =
+ targetActor.targetType === Targets.TYPES.PROCESS;
+
+ // Only consider messages from a given window for all FRAME targets (this includes
+ // WebExt and ParentProcess which inherits from WindowGlobalTargetActor)
+ // But ParentProcess should be ignored as we want all messages emitted directly from
+ // that process (window and window-less).
+ // To do that we pass a null window and ConsoleAPIListener will catch everything.
+ // And also ignore WebExtension as we will filter out only by addonId, which is
+ // passed via consoleAPIListenerOptions. WebExtension may have multiple windows/documents
+ // but all of them will be flagged with the same addon ID.
+ const window =
+ targetActor.targetType === Targets.TYPES.FRAME &&
+ targetActor.typeName != "parentProcessTarget" &&
+ targetActor.typeName != "webExtensionTarget"
+ ? targetActor.window
+ : null;
+
+ const listener = new ConsoleAPIListener(window, onConsoleAPICall, {
+ excludeMessagesBoundToWindow: isTargetActorContentProcess,
+ matchExactWindow: targetActor.ignoreSubFrames,
+ ...(targetActor.consoleAPIListenerOptions || {}),
+ });
+ this.listener = listener;
+ listener.init();
+
+ // It can happen that the targetActor does not have a window reference (e.g. in worker
+ // thread, targetActor exposes a workerGlobal property)
+ const winStartTime =
+ targetActor.window?.performance?.timing?.navigationStart || 0;
+
+ const cachedMessages = listener.getCachedMessages(!targetActor.isRootActor);
+ const messages = [];
+ // Filter out messages that came from a ServiceWorker but happened
+ // before the page was requested.
+ for (const message of cachedMessages) {
+ if (
+ message.innerID === "ServiceWorker" &&
+ winStartTime > message.timeStamp
+ ) {
+ continue;
+ }
+ messages.push({
+ resourceType: CONSOLE_MESSAGE,
+ message: prepareConsoleMessageForRemote(targetActor, message),
+ });
+ }
+ onAvailable(messages);
+ }
+
+ /**
+ * Stop watching for console messages.
+ */
+ destroy() {
+ if (this.listener) {
+ this.listener.destroy();
+ }
+ }
+
+ /**
+ * Called by devtools/server/actors/utils/logEvent.js, whenever a new
+ * log point is triggered and request to spawn a console message
+ *
+ * @param Object message
+ * A fake nsIConsoleMessage, which looks like the one being generated by
+ * the platform API.
+ */
+ onLogPoint(message) {
+ if (!this.listener) {
+ throw new Error("This target actor isn't listening to console messages");
+ }
+ this.listener.handler(message);
+ }
+}
+
+module.exports = ConsoleMessageWatcher;
+
+/**
+ * Return the properties needed to display the appropriate table for a given
+ * console.table call.
+ * This function does a little more than creating an ObjectActor for the first
+ * parameter of the message. When layout out the console table in the output, we want
+ * to be able to look into sub-properties so the table can have a different layout (
+ * for arrays of arrays, objects with objects properties, arrays of objects, …).
+ * So here we need to retrieve the properties of the first parameter, and also all the
+ * sub-properties we might need.
+ *
+ * @param {TargetActor} targetActor: The Target Actor from which this object originates.
+ * @param {Object} result: The console.table message.
+ * @returns {Object} An object containing the properties of the first argument of the
+ * console.table call.
+ */
+function getConsoleTableMessageItems(targetActor, result) {
+ const [tableItemGrip] = result.arguments;
+ const dataType = tableItemGrip.class;
+ const needEntries = ["Map", "WeakMap", "Set", "WeakSet"].includes(dataType);
+ const ignoreNonIndexedProperties = isArray(tableItemGrip);
+
+ const tableItemActor = targetActor.getActorByID(tableItemGrip.actor);
+ if (!tableItemActor) {
+ return null;
+ }
+
+ // Retrieve the properties (or entries for Set/Map) of the console table first arg.
+ const iterator = needEntries
+ ? tableItemActor.enumEntries()
+ : tableItemActor.enumProperties({
+ ignoreNonIndexedProperties,
+ });
+ const { ownProperties } = iterator.all();
+
+ // The iterator returns a descriptor for each property, wherein the value could be
+ // in one of those sub-property.
+ const descriptorKeys = ["safeGetterValues", "getterValue", "value"];
+
+ Object.values(ownProperties).forEach(desc => {
+ if (typeof desc !== "undefined") {
+ descriptorKeys.forEach(key => {
+ if (desc && desc.hasOwnProperty(key)) {
+ const grip = desc[key];
+
+ // We need to load sub-properties as well to render the table in a nice way.
+ const actor = grip && targetActor.getActorByID(grip.actor);
+ if (actor) {
+ const res = actor
+ .enumProperties({
+ ignoreNonIndexedProperties: isArray(grip),
+ })
+ .all();
+ if (res?.ownProperties) {
+ desc[key].ownProperties = res.ownProperties;
+ }
+ }
+ }
+ });
+ }
+ });
+
+ return ownProperties;
+}
+
+/**
+ * Prepare a message from the console API to be sent to the remote Web Console
+ * instance.
+ *
+ * @param TargetActor targetActor
+ * The related target actor
+ * @param object message
+ * The original message received from the console storage listener.
+ * @return object
+ * The object that can be sent to the remote client.
+ */
+function prepareConsoleMessageForRemote(targetActor, message) {
+ const result = {
+ arguments: message.arguments
+ ? message.arguments.map(obj => {
+ const dbgObj = makeDebuggeeValue(targetActor, obj);
+ return createValueGripForTarget(targetActor, dbgObj);
+ })
+ : [],
+ columnNumber: message.columnNumber,
+ filename: message.filename,
+ level: message.level,
+ lineNumber: message.lineNumber,
+ // messages emitted from Console.sys.mjs don't have a microSecondTimeStamp property
+ timeStamp: message.microSecondTimeStamp
+ ? message.microSecondTimeStamp / 1000
+ : message.timeStamp,
+ sourceId: getActorIdForInternalSourceId(targetActor, message.sourceId),
+ innerWindowID: message.innerID,
+ };
+
+ // This can be a hot path when loading lots of messages, and it only make sense to
+ // include the following properties in the message when they have a meaningful value.
+ // Otherwise we simply don't include them so we save cycles in JSActor communication.
+ if (message.chromeContext) {
+ result.chromeContext = message.chromeContext;
+ }
+
+ if (message.counter) {
+ result.counter = message.counter;
+ }
+ if (message.private) {
+ result.private = message.private;
+ }
+ if (message.prefix) {
+ result.prefix = message.prefix;
+ }
+
+ if (message.stacktrace) {
+ result.stacktrace = message.stacktrace.map(frame => {
+ return {
+ ...frame,
+ sourceId: getActorIdForInternalSourceId(targetActor, frame.sourceId),
+ };
+ });
+ }
+
+ if (message.styles && message.styles.length) {
+ result.styles = message.styles.map(string => {
+ return createValueGripForTarget(targetActor, string);
+ });
+ }
+
+ if (message.timer) {
+ result.timer = message.timer;
+ }
+
+ if (message.level === "table") {
+ if (result && isSupportedByConsoleTable(result.arguments)) {
+ const tableItems = getConsoleTableMessageItems(targetActor, result);
+ if (tableItems) {
+ result.arguments[0].ownProperties = tableItems;
+ result.arguments[0].preview = null;
+
+ // Only return the 2 first params.
+ result.arguments = result.arguments.slice(0, 2);
+ }
+ }
+ // NOTE: See transformConsoleAPICallResource for not-supported case.
+ }
+
+ return result;
+}
diff --git a/devtools/server/actors/resources/css-changes.js b/devtools/server/actors/resources/css-changes.js
new file mode 100644
index 0000000000..e86503be87
--- /dev/null
+++ b/devtools/server/actors/resources/css-changes.js
@@ -0,0 +1,42 @@
+/* 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: { CSS_CHANGE },
+} = require("resource://devtools/server/actors/resources/index.js");
+const TrackChangeEmitter = require("resource://devtools/server/actors/utils/track-change-emitter.js");
+
+/**
+ * Start watching for all css changes related to a given Target Actor.
+ *
+ * @param TargetActor targetActor
+ * The target actor from which we should observe css changes.
+ * @param Object options
+ * Dictionary object with following attributes:
+ * - onAvailable: mandatory function
+ * This will be called for each resource.
+ */
+class CSSChangeWatcher {
+ constructor() {
+ this.onTrackChange = this.onTrackChange.bind(this);
+ }
+
+ async watch(targetActor, { onAvailable }) {
+ this.onAvailable = onAvailable;
+ TrackChangeEmitter.on("track-change", this.onTrackChange);
+ }
+
+ onTrackChange(change) {
+ change.resourceType = CSS_CHANGE;
+ this.onAvailable([change]);
+ }
+
+ destroy() {
+ TrackChangeEmitter.off("track-change", this.onTrackChange);
+ }
+}
+
+module.exports = CSSChangeWatcher;
diff --git a/devtools/server/actors/resources/css-messages.js b/devtools/server/actors/resources/css-messages.js
new file mode 100644
index 0000000000..6c4f28c646
--- /dev/null
+++ b/devtools/server/actors/resources/css-messages.js
@@ -0,0 +1,139 @@
+/* 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 nsIConsoleListenerWatcher = require("resource://devtools/server/actors/resources/utils/nsi-console-listener-watcher.js");
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const {
+ createStringGrip,
+} = require("resource://devtools/server/actors/object/utils.js");
+const {
+ getActorIdForInternalSourceId,
+} = require("resource://devtools/server/actors/utils/dbg-source.js");
+const {
+ WebConsoleUtils,
+} = require("resource://devtools/server/actors/webconsole/utils.js");
+
+const {
+ TYPES: { CSS_MESSAGE },
+} = require("resource://devtools/server/actors/resources/index.js");
+
+const { MESSAGE_CATEGORY } = require("resource://devtools/shared/constants.js");
+
+class CSSMessageWatcher extends nsIConsoleListenerWatcher {
+ /**
+ * Start watching for all CSS messages related to a given Target Actor.
+ * This will notify about existing messages, but also the one created in future.
+ *
+ * @param TargetActor targetActor
+ * The target actor from which we should observe messages
+ * @param Object options
+ * Dictionary object with following attributes:
+ * - onAvailable: mandatory function
+ * This will be called for each resource.
+ */
+ async watch(targetActor, { onAvailable }) {
+ super.watch(targetActor, { onAvailable });
+
+ // Calling ensureCSSErrorReportingEnabled will make the server parse the stylesheets to
+ // retrieve the warnings if the docShell wasn't already watching for CSS messages.
+ if (targetActor.ensureCSSErrorReportingEnabled) {
+ targetActor.ensureCSSErrorReportingEnabled();
+ }
+ }
+
+ /**
+ * Returns true if the message is considered a CSS message, and as a result, should
+ * be sent to the client.
+ *
+ * @param {nsIConsoleMessage|nsIScriptError} message
+ */
+ shouldHandleMessage(targetActor, message) {
+ // The listener we use can be called either with a nsIConsoleMessage or as nsIScriptError.
+ // In this file, we want to ignore anything but nsIScriptError.
+ if (
+ // We only care about CSS Parser nsIScriptError
+ !(message instanceof Ci.nsIScriptError) ||
+ message.category !== MESSAGE_CATEGORY.CSS_PARSER
+ ) {
+ return false;
+ }
+
+ // Filter specific to CONTENT PROCESS targets
+ // Process targets listen for everything but messages from private windows.
+ if (this.isProcessTarget(targetActor)) {
+ return !message.isFromPrivateWindow;
+ }
+
+ if (!message.innerWindowID) {
+ return false;
+ }
+
+ const ids = targetActor.windows.map(window =>
+ WebConsoleUtils.getInnerWindowId(window)
+ );
+ return ids.includes(message.innerWindowID);
+ }
+
+ /**
+ * Prepare an nsIScriptError to be sent to the client.
+ *
+ * @param nsIScriptError error
+ * The page error we need to send to the client.
+ * @return object
+ * The object you can send to the remote client.
+ */
+ buildResource(targetActor, error) {
+ const stack = this.prepareStackForRemote(targetActor, error.stack);
+ let lineText = error.sourceLine;
+ if (
+ lineText &&
+ lineText.length > DevToolsServer.LONG_STRING_INITIAL_LENGTH
+ ) {
+ lineText = lineText.substr(0, DevToolsServer.LONG_STRING_INITIAL_LENGTH);
+ }
+
+ const notesArray = this.prepareNotesForRemote(targetActor, error.notes);
+
+ // If there is no location information in the error but we have a stack,
+ // fill in the location with the first frame on the stack.
+ let { sourceName, sourceId, lineNumber, columnNumber } = error;
+ if (!sourceName && !sourceId && !lineNumber && !columnNumber && stack) {
+ sourceName = stack[0].filename;
+ sourceId = stack[0].sourceId;
+ lineNumber = stack[0].lineNumber;
+ columnNumber = stack[0].columnNumber;
+ }
+
+ const pageError = {
+ errorMessage: createStringGrip(targetActor, error.errorMessage),
+ sourceName,
+ sourceId: getActorIdForInternalSourceId(targetActor, sourceId),
+ lineText,
+ lineNumber,
+ columnNumber,
+ category: error.category,
+ innerWindowID: error.innerWindowID,
+ timeStamp: error.microSecondTimeStamp / 1000,
+ warning: !!(error.flags & error.warningFlag),
+ error: !(error.flags & (error.warningFlag | error.infoFlag)),
+ info: !!(error.flags & error.infoFlag),
+ private: error.isFromPrivateWindow,
+ stacktrace: stack,
+ notes: notesArray,
+ chromeContext: error.isFromChromeContext,
+ isForwardedFromContentProcess: error.isForwardedFromContentProcess,
+ };
+
+ return {
+ pageError,
+ resourceType: CSS_MESSAGE,
+ cssSelectors: error.cssSelectors,
+ };
+ }
+}
+module.exports = CSSMessageWatcher;
diff --git a/devtools/server/actors/resources/document-event.js b/devtools/server/actors/resources/document-event.js
new file mode 100644
index 0000000000..bd6667b2b5
--- /dev/null
+++ b/devtools/server/actors/resources/document-event.js
@@ -0,0 +1,112 @@
+/* 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: { DOCUMENT_EVENT },
+} = require("resource://devtools/server/actors/resources/index.js");
+const {
+ DocumentEventsListener,
+} = require("resource://devtools/server/actors/webconsole/listeners/document-events.js");
+
+class DocumentEventWatcher {
+ #abortController = new AbortController();
+ /**
+ * Start watching for all document event related to a given Target Actor.
+ *
+ * @param TargetActor targetActor
+ * The target actor from which we should observe document event
+ * @param Object options
+ * Dictionary object with following attributes:
+ * - onAvailable: mandatory function
+ * This will be called for each resource.
+ */
+ async watch(targetActor, { onAvailable }) {
+ if (isWorker) {
+ return;
+ }
+
+ const onDocumentEvent = (
+ name,
+ {
+ time,
+ // This will be `true` when the user selected a document in the frame picker tool,
+ // in the toolbox toolbar.
+ isFrameSwitching,
+ // This is only passed for dom-complete event
+ hasNativeConsoleAPI,
+ // This is only passed for will-navigate event
+ newURI,
+ } = {}
+ ) => {
+ // Ignore will-navigate as that's managed by parent-process-document-event.js.
+ // Except frame switching, when selecting an iframe document via the dropdown menu,
+ // this is handled by the target actor in the content process and the parent process
+ // doesn't know about it.
+ if (name == "will-navigate" && !isFrameSwitching) {
+ return;
+ }
+ onAvailable([
+ {
+ resourceType: DOCUMENT_EVENT,
+ name,
+ time,
+ isFrameSwitching,
+ // only send `title` on dom interactive (once the HTML was parsed) so we don't
+ // make the payload bigger for events where we either don't have a title yet,
+ // or where we already had a chance to get the title.
+ title: name === "dom-interactive" ? targetActor.title : undefined,
+ // only send `url` on dom loading and dom-interactive so we don't make the
+ // payload bigger for other events
+ url:
+ name === "dom-loading" || name === "dom-interactive"
+ ? targetActor.url
+ : undefined,
+ // only send `newURI` on will navigate so we don't make the payload bigger for
+ // other events
+ newURI: name === "will-navigate" ? newURI : null,
+ // only send `hasNativeConsoleAPI` on dom complete so we don't make the payload bigger for
+ // other events
+ hasNativeConsoleAPI:
+ name == "dom-complete" ? hasNativeConsoleAPI : null,
+ },
+ ]);
+ };
+
+ this.listener = new DocumentEventsListener(targetActor);
+
+ this.listener.on(
+ "will-navigate",
+ data => onDocumentEvent("will-navigate", data),
+ { signal: this.#abortController.signal }
+ );
+ this.listener.on(
+ "dom-loading",
+ data => onDocumentEvent("dom-loading", data),
+ { signal: this.#abortController.signal }
+ );
+ this.listener.on(
+ "dom-interactive",
+ data => onDocumentEvent("dom-interactive", data),
+ { signal: this.#abortController.signal }
+ );
+ this.listener.on(
+ "dom-complete",
+ data => onDocumentEvent("dom-complete", data),
+ { signal: this.#abortController.signal }
+ );
+
+ this.listener.listen();
+ }
+
+ destroy() {
+ this.#abortController.abort();
+ if (this.listener) {
+ this.listener.destroy();
+ }
+ }
+}
+
+module.exports = DocumentEventWatcher;
diff --git a/devtools/server/actors/resources/error-messages.js b/devtools/server/actors/resources/error-messages.js
new file mode 100644
index 0000000000..7628d7fd6d
--- /dev/null
+++ b/devtools/server/actors/resources/error-messages.js
@@ -0,0 +1,192 @@
+/* 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 nsIConsoleListenerWatcher = require("resource://devtools/server/actors/resources/utils/nsi-console-listener-watcher.js");
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const ErrorDocs = require("resource://devtools/server/actors/errordocs.js");
+const {
+ createStringGrip,
+ makeDebuggeeValue,
+ createValueGripForTarget,
+} = require("resource://devtools/server/actors/object/utils.js");
+const {
+ getActorIdForInternalSourceId,
+} = require("resource://devtools/server/actors/utils/dbg-source.js");
+const {
+ WebConsoleUtils,
+} = require("resource://devtools/server/actors/webconsole/utils.js");
+
+const {
+ TYPES: { ERROR_MESSAGE },
+} = require("resource://devtools/server/actors/resources/index.js");
+const Targets = require("resource://devtools/server/actors/targets/index.js");
+
+const { MESSAGE_CATEGORY } = require("resource://devtools/shared/constants.js");
+
+const PLATFORM_SPECIFIC_CATEGORIES = [
+ "XPConnect JavaScript",
+ "component javascript",
+ "chrome javascript",
+ "chrome registration",
+];
+
+class ErrorMessageWatcher extends nsIConsoleListenerWatcher {
+ shouldHandleMessage(targetActor, message, isCachedMessage = false) {
+ // The listener we use can be called either with a nsIConsoleMessage or a nsIScriptError.
+ // In this file, we only want to handle nsIScriptError.
+ if (
+ // We only care about nsIScriptError
+ !(message instanceof Ci.nsIScriptError) ||
+ !this.isCategoryAllowed(targetActor, message.category) ||
+ // Block any error that was triggered by eager evaluation
+ message.sourceName === "debugger eager eval code"
+ ) {
+ return false;
+ }
+
+ // Filter specific to CONTENT PROCESS targets
+ if (this.isProcessTarget(targetActor)) {
+ // Don't want to display cached messages from private windows.
+ const isCachedFromPrivateWindow =
+ isCachedMessage && message.isFromPrivateWindow;
+ if (isCachedFromPrivateWindow) {
+ return false;
+ }
+
+ // `ContentChild` forwards all errors to the parent process (via IPC) all errors up
+ // the parent process and sets a `isForwardedFromContentProcess` property on them.
+ // Ignore these forwarded messages as the original ones will be logged either in a
+ // content process target (if window-less message) or frame target (if related to a window)
+ if (message.isForwardedFromContentProcess) {
+ return false;
+ }
+
+ // Ignore all messages related to a given window for content process targets
+ // These messages will be handled by Watchers instantiated for the related frame targets
+ if (
+ targetActor.targetType == Targets.TYPES.PROCESS &&
+ message.innerWindowID
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ if (!message.innerWindowID) {
+ return false;
+ }
+
+ const ids = targetActor.windows.map(window =>
+ WebConsoleUtils.getInnerWindowId(window)
+ );
+ return ids.includes(message.innerWindowID);
+ }
+
+ /**
+ * Check if the given message category is allowed to be tracked or not.
+ * We ignore chrome-originating errors as we only care about content.
+ *
+ * @param string category
+ * The message category you want to check.
+ * @return boolean
+ * True if the category is allowed to be logged, false otherwise.
+ */
+ isCategoryAllowed(targetActor, category) {
+ // CSS Parser errors will be handled by the CSSMessageWatcher.
+ if (!category || category === MESSAGE_CATEGORY.CSS_PARSER) {
+ return false;
+ }
+
+ // We listen for everything on Process targets
+ if (this.isProcessTarget(targetActor)) {
+ return true;
+ }
+
+ // Don't restrict any categories in the Browser Toolbox/Browser Console
+ if (targetActor.sessionContext.type == "all") {
+ return true;
+ }
+
+ // For non-process targets in other toolboxes, we filter-out platform-specific errors.
+ return !PLATFORM_SPECIFIC_CATEGORIES.includes(category);
+ }
+
+ /**
+ * Prepare an nsIScriptError to be sent to the client.
+ *
+ * @param nsIScriptError error
+ * The page error we need to send to the client.
+ * @return object
+ * The object you can send to the remote client.
+ */
+ buildResource(targetActor, error) {
+ const stack = this.prepareStackForRemote(targetActor, error.stack);
+ let lineText = error.sourceLine;
+ if (
+ lineText &&
+ lineText.length > DevToolsServer.LONG_STRING_INITIAL_LENGTH
+ ) {
+ lineText = lineText.substr(0, DevToolsServer.LONG_STRING_INITIAL_LENGTH);
+ }
+
+ const notesArray = this.prepareNotesForRemote(targetActor, error.notes);
+
+ // If there is no location information in the error but we have a stack,
+ // fill in the location with the first frame on the stack.
+ let { sourceName, sourceId, lineNumber, columnNumber } = error;
+ if (!sourceName && !sourceId && !lineNumber && !columnNumber && stack) {
+ sourceName = stack[0].filename;
+ sourceId = stack[0].sourceId;
+ lineNumber = stack[0].lineNumber;
+ columnNumber = stack[0].columnNumber;
+ }
+
+ const pageError = {
+ errorMessage: createStringGrip(targetActor, error.errorMessage),
+ errorMessageName: error.errorMessageName,
+ exceptionDocURL: ErrorDocs.GetURL(error),
+ sourceName,
+ sourceId: getActorIdForInternalSourceId(targetActor, sourceId),
+ lineText,
+ lineNumber,
+ columnNumber,
+ category: error.category,
+ innerWindowID: error.innerWindowID,
+ timeStamp: error.microSecondTimeStamp / 1000,
+ warning: !!(error.flags & error.warningFlag),
+ error: !(error.flags & (error.warningFlag | error.infoFlag)),
+ info: !!(error.flags & error.infoFlag),
+ private: error.isFromPrivateWindow,
+ stacktrace: stack,
+ notes: notesArray,
+ chromeContext: error.isFromChromeContext,
+ isPromiseRejection: error.isPromiseRejection,
+ isForwardedFromContentProcess: error.isForwardedFromContentProcess,
+ };
+
+ // If the pageError does have an exception object, we want to return the grip for it,
+ // but only if we do manage to get the grip, as we're checking the property on the
+ // client to render things differently.
+ if (error.hasException) {
+ try {
+ const obj = makeDebuggeeValue(targetActor, error.exception);
+ if (obj?.class !== "DeadObject") {
+ pageError.exception = createValueGripForTarget(targetActor, obj);
+ pageError.hasException = true;
+ }
+ } catch (e) {}
+ }
+
+ return {
+ pageError,
+ resourceType: ERROR_MESSAGE,
+ };
+ }
+}
+module.exports = ErrorMessageWatcher;
diff --git a/devtools/server/actors/resources/extensions-backgroundscript-status.js b/devtools/server/actors/resources/extensions-backgroundscript-status.js
new file mode 100644
index 0000000000..08f51a23f5
--- /dev/null
+++ b/devtools/server/actors/resources/extensions-backgroundscript-status.js
@@ -0,0 +1,68 @@
+/* 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: { EXTENSIONS_BGSCRIPT_STATUS },
+} = require("resource://devtools/server/actors/resources/index.js");
+
+class ExtensionsBackgroundScriptStatusWatcher {
+ /**
+ * Start watching for the status updates related to a background
+ * scripts extension context (either an event page or a background
+ * service worker).
+ *
+ * This is used in about:debugging to update the background script
+ * row updated visible in Extensions details cards (only for extensions
+ * with a non persistent background script defined in the manifest)
+ * when the background contex is terminated on idle or started back
+ * to handle a persistent WebExtensions API event.
+ *
+ * @param RootActor rootActor
+ * The root actor in the parent process from which we should
+ * observe root resources.
+ * @param Object options
+ * Dictionary object with following attributes:
+ * - onAvailable: mandatory function
+ * This will be called for each resource.
+ */
+ async watch(rootActor, { onAvailable }) {
+ this.rootActor = rootActor;
+ this.onAvailable = onAvailable;
+
+ Services.obs.addObserver(this, "extension:background-script-status");
+ }
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "extension:background-script-status": {
+ const { addonId, isRunning } = subject.wrappedJSObject;
+ this.onBackgroundScriptStatus(addonId, isRunning);
+ break;
+ }
+ }
+ }
+
+ onBackgroundScriptStatus(addonId, isRunning) {
+ this.onAvailable([
+ {
+ resourceType: EXTENSIONS_BGSCRIPT_STATUS,
+ payload: {
+ addonId,
+ isRunning,
+ },
+ },
+ ]);
+ }
+
+ destroy() {
+ if (this.onAvailable) {
+ this.onAvailable = null;
+ Services.obs.removeObserver(this, "extension:background-script-status");
+ }
+ }
+}
+
+module.exports = ExtensionsBackgroundScriptStatusWatcher;
diff --git a/devtools/server/actors/resources/index.js b/devtools/server/actors/resources/index.js
new file mode 100644
index 0000000000..c3dee037f0
--- /dev/null
+++ b/devtools/server/actors/resources/index.js
@@ -0,0 +1,438 @@
+/* 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 TYPES = {
+ CONSOLE_MESSAGE: "console-message",
+ CSS_CHANGE: "css-change",
+ CSS_MESSAGE: "css-message",
+ DOCUMENT_EVENT: "document-event",
+ ERROR_MESSAGE: "error-message",
+ LAST_PRIVATE_CONTEXT_EXIT: "last-private-context-exit",
+ NETWORK_EVENT: "network-event",
+ NETWORK_EVENT_STACKTRACE: "network-event-stacktrace",
+ PLATFORM_MESSAGE: "platform-message",
+ REFLOW: "reflow",
+ SERVER_SENT_EVENT: "server-sent-event",
+ SOURCE: "source",
+ STYLESHEET: "stylesheet",
+ THREAD_STATE: "thread-state",
+ WEBSOCKET: "websocket",
+
+ // storage types
+ CACHE_STORAGE: "Cache",
+ COOKIE: "cookies",
+ INDEXED_DB: "indexed-db",
+ LOCAL_STORAGE: "local-storage",
+ SESSION_STORAGE: "session-storage",
+
+ // root types
+ EXTENSIONS_BGSCRIPT_STATUS: "extensions-backgroundscript-status",
+};
+exports.TYPES = TYPES;
+
+// Helper dictionaries, which will contain data specific to each resource type.
+// - `path` is the absolute path to the module defining the Resource Watcher class.
+//
+// Also see the attributes added by `augmentResourceDictionary` for each type:
+// - `watchers` is a weak map which will store Resource Watchers
+// (i.e. devtools/server/actors/resources/ class instances)
+// keyed by target actor -or- watcher actor.
+// - `WatcherClass` is a shortcut to the Resource Watcher module.
+// Each module exports a Resource Watcher class.
+//
+// These are several dictionaries, which depend how the resource watcher classes are instantiated.
+
+// Frame target resources are spawned via a BrowsingContext Target Actor.
+// Their watcher class receives a target actor as first argument.
+// They are instantiated for each observed BrowsingContext, from the content process where it runs.
+// They are meant to observe all resources related to a given Browsing Context.
+const FrameTargetResources = augmentResourceDictionary({
+ [TYPES.CACHE_STORAGE]: {
+ path: "devtools/server/actors/resources/storage-cache",
+ },
+ [TYPES.CONSOLE_MESSAGE]: {
+ path: "devtools/server/actors/resources/console-messages",
+ },
+ [TYPES.CSS_CHANGE]: {
+ path: "devtools/server/actors/resources/css-changes",
+ },
+ [TYPES.CSS_MESSAGE]: {
+ path: "devtools/server/actors/resources/css-messages",
+ },
+ [TYPES.DOCUMENT_EVENT]: {
+ path: "devtools/server/actors/resources/document-event",
+ },
+ [TYPES.ERROR_MESSAGE]: {
+ path: "devtools/server/actors/resources/error-messages",
+ },
+ [TYPES.LOCAL_STORAGE]: {
+ path: "devtools/server/actors/resources/storage-local-storage",
+ },
+ [TYPES.PLATFORM_MESSAGE]: {
+ path: "devtools/server/actors/resources/platform-messages",
+ },
+ [TYPES.SESSION_STORAGE]: {
+ path: "devtools/server/actors/resources/storage-session-storage",
+ },
+ [TYPES.STYLESHEET]: {
+ path: "devtools/server/actors/resources/stylesheets",
+ },
+ [TYPES.NETWORK_EVENT]: {
+ path: "devtools/server/actors/resources/network-events-content",
+ },
+ [TYPES.NETWORK_EVENT_STACKTRACE]: {
+ path: "devtools/server/actors/resources/network-events-stacktraces",
+ },
+ [TYPES.REFLOW]: {
+ path: "devtools/server/actors/resources/reflow",
+ },
+ [TYPES.SOURCE]: {
+ path: "devtools/server/actors/resources/sources",
+ },
+ [TYPES.THREAD_STATE]: {
+ path: "devtools/server/actors/resources/thread-states",
+ },
+ [TYPES.SERVER_SENT_EVENT]: {
+ path: "devtools/server/actors/resources/server-sent-events",
+ },
+ [TYPES.WEBSOCKET]: {
+ path: "devtools/server/actors/resources/websockets",
+ },
+});
+
+// Process target resources are spawned via a Process Target Actor.
+// Their watcher class receives a process target actor as first argument.
+// They are instantiated for each observed Process (parent and all content processes).
+// They are meant to observe all resources related to a given process.
+const ProcessTargetResources = augmentResourceDictionary({
+ [TYPES.CONSOLE_MESSAGE]: {
+ path: "devtools/server/actors/resources/console-messages",
+ },
+ [TYPES.ERROR_MESSAGE]: {
+ path: "devtools/server/actors/resources/error-messages",
+ },
+ [TYPES.PLATFORM_MESSAGE]: {
+ path: "devtools/server/actors/resources/platform-messages",
+ },
+ [TYPES.SOURCE]: {
+ path: "devtools/server/actors/resources/sources",
+ },
+ [TYPES.THREAD_STATE]: {
+ path: "devtools/server/actors/resources/thread-states",
+ },
+});
+
+// Worker target resources are spawned via a Worker Target Actor.
+// Their watcher class receives a worker target actor as first argument.
+// They are instantiated for each observed worker, from the worker thread.
+// They are meant to observe all resources related to a given worker.
+//
+// We'll only support a few resource types in Workers (console-message, source,
+// thread state, …) as error and platform messages are not supported since we need access
+// to Ci, which isn't available in worker context.
+// Errors are emitted from the content process main thread so the user would still get them.
+const WorkerTargetResources = augmentResourceDictionary({
+ [TYPES.CONSOLE_MESSAGE]: {
+ path: "devtools/server/actors/resources/console-messages",
+ },
+ [TYPES.SOURCE]: {
+ path: "devtools/server/actors/resources/sources",
+ },
+ [TYPES.THREAD_STATE]: {
+ path: "devtools/server/actors/resources/thread-states",
+ },
+});
+
+// Parent process resources are spawned via the Watcher Actor.
+// Their watcher class receives the watcher actor as first argument.
+// They are instantiated once per watcher from the parent process.
+// They are meant to observe all resources related to a given context designated by the Watcher (and its sessionContext)
+// they should be observed from the parent process.
+const ParentProcessResources = augmentResourceDictionary({
+ [TYPES.NETWORK_EVENT]: {
+ path: "devtools/server/actors/resources/network-events",
+ },
+ [TYPES.COOKIE]: {
+ path: "devtools/server/actors/resources/storage-cookie",
+ },
+ [TYPES.INDEXED_DB]: {
+ path: "devtools/server/actors/resources/storage-indexed-db",
+ },
+ [TYPES.DOCUMENT_EVENT]: {
+ path: "devtools/server/actors/resources/parent-process-document-event",
+ },
+ [TYPES.LAST_PRIVATE_CONTEXT_EXIT]: {
+ path: "devtools/server/actors/resources/last-private-context-exit",
+ },
+});
+
+// Root resources are spawned via the Root Actor.
+// Their watcher class receives the root actor as first argument.
+// They are instantiated only once from the parent process.
+// They are meant to observe anything easily observable from the parent process
+// that isn't related to any particular context/target.
+// This is especially useful when you need to observe something without having to instantiate a Watcher actor.
+const RootResources = augmentResourceDictionary({
+ [TYPES.EXTENSIONS_BGSCRIPT_STATUS]: {
+ path: "devtools/server/actors/resources/extensions-backgroundscript-status",
+ },
+});
+exports.RootResources = RootResources;
+
+function augmentResourceDictionary(dict) {
+ for (const resource of Object.values(dict)) {
+ resource.watchers = new WeakMap();
+
+ loader.lazyRequireGetter(resource, "WatcherClass", resource.path);
+ }
+ return dict;
+}
+
+/**
+ * For a given actor, return the related dictionary defined just before,
+ * that contains info about how to listen for a given resource type, from a given actor.
+ *
+ * @param Actor rootOrWatcherOrTargetActor
+ * Either a RootActor or WatcherActor or a TargetActor which can be listening to a resource.
+ */
+function getResourceTypeDictionary(rootOrWatcherOrTargetActor) {
+ const { typeName } = rootOrWatcherOrTargetActor;
+ if (typeName == "root") {
+ return RootResources;
+ }
+ if (typeName == "watcher") {
+ return ParentProcessResources;
+ }
+ const { targetType } = rootOrWatcherOrTargetActor;
+ return getResourceTypeDictionaryForTargetType(targetType);
+}
+
+/**
+ * For a targetType, return the related dictionary.
+ *
+ * @param String targetType
+ * A targetType string (See Targets.TYPES)
+ */
+function getResourceTypeDictionaryForTargetType(targetType) {
+ switch (targetType) {
+ case Targets.TYPES.FRAME:
+ return FrameTargetResources;
+ case Targets.TYPES.PROCESS:
+ return ProcessTargetResources;
+ case Targets.TYPES.WORKER:
+ return WorkerTargetResources;
+ default:
+ throw new Error(`Unsupported target actor typeName '${targetType}'`);
+ }
+}
+
+/**
+ * For a given actor, return the object stored in one of the previous dictionary
+ * that contains info about how to listen for a given resource type, from a given actor.
+ *
+ * @param Actor rootOrWatcherOrTargetActor
+ * Either a RootActor or WatcherActor or a TargetActor which can be listening to a resource.
+ * @param String resourceType
+ * The resource type to be observed.
+ */
+function getResourceTypeEntry(rootOrWatcherOrTargetActor, resourceType) {
+ const dict = getResourceTypeDictionary(rootOrWatcherOrTargetActor);
+ if (!(resourceType in dict)) {
+ throw new Error(
+ `Unsupported resource type '${resourceType}' for ${rootOrWatcherOrTargetActor.typeName}`
+ );
+ }
+ return dict[resourceType];
+}
+
+/**
+ * Start watching for a new list of resource types.
+ * This will also emit all already existing resources before resolving.
+ *
+ * @param Actor rootOrWatcherOrTargetActor
+ * Either a RootActor or WatcherActor or a TargetActor which can be listening to a resource:
+ * * RootActor will be used for resources observed from the parent process and aren't related to any particular
+ * context/descriptor. They can be observed right away when connecting to the RDP server
+ * without instantiating any actor other than the root actor.
+ * * WatcherActor will be used for resources listened from the parent process.
+ * * TargetActor will be used for resources listened from the content process.
+ * This actor:
+ * - defines what context to observe (browsing context, process, worker, ...)
+ * Via browsingContextID, windows, docShells attributes for the target actor.
+ * Via the `sessionContext` object for the watcher actor.
+ * (only for Watcher and Target actors. Root actor is context-less.)
+ * - exposes `notifyResources` method to be notified about all the resources updates
+ * This method will receive two arguments:
+ * - {String} updateType, which can be "available", "updated", or "destroyed"
+ * - {Array<Object>} resources, which will be the list of resource's forms
+ * or special update object for "updated" scenario.
+ * @param Array<String> resourceTypes
+ * List of all type of resource to listen to.
+ */
+async function watchResources(rootOrWatcherOrTargetActor, resourceTypes) {
+ // If we are given a target actor, filter out the resource types supported by the target.
+ // When using sharedData to pass types between processes, we are passing them for all target types.
+ const { targetType } = rootOrWatcherOrTargetActor;
+ // Only target actors usecase will have a target type.
+ // For Root and Watcher we process the `resourceTypes` list unfiltered.
+ if (targetType) {
+ resourceTypes = getResourceTypesForTargetType(resourceTypes, targetType);
+ }
+ for (const resourceType of resourceTypes) {
+ const { watchers, WatcherClass } = getResourceTypeEntry(
+ rootOrWatcherOrTargetActor,
+ resourceType
+ );
+
+ // Ignore resources we're already listening to
+ if (watchers.has(rootOrWatcherOrTargetActor)) {
+ continue;
+ }
+
+ // Don't watch for console messages from the worker target if worker messages are still
+ // being cloned to the main process, otherwise we'll get duplicated messages in the
+ // console output (See Bug 1778852).
+ if (
+ resourceType == TYPES.CONSOLE_MESSAGE &&
+ rootOrWatcherOrTargetActor.workerConsoleApiMessagesDispatchedToMainThread
+ ) {
+ continue;
+ }
+
+ const watcher = new WatcherClass();
+ await watcher.watch(rootOrWatcherOrTargetActor, {
+ onAvailable: rootOrWatcherOrTargetActor.notifyResources.bind(
+ rootOrWatcherOrTargetActor,
+ "available"
+ ),
+ onUpdated: rootOrWatcherOrTargetActor.notifyResources.bind(
+ rootOrWatcherOrTargetActor,
+ "updated"
+ ),
+ onDestroyed: rootOrWatcherOrTargetActor.notifyResources.bind(
+ rootOrWatcherOrTargetActor,
+ "destroyed"
+ ),
+ });
+ watchers.set(rootOrWatcherOrTargetActor, watcher);
+ }
+}
+exports.watchResources = watchResources;
+
+function getParentProcessResourceTypes(resourceTypes) {
+ return resourceTypes.filter(resourceType => {
+ return resourceType in ParentProcessResources;
+ });
+}
+exports.getParentProcessResourceTypes = getParentProcessResourceTypes;
+
+function getResourceTypesForTargetType(resourceTypes, targetType) {
+ const resourceDictionnary = getResourceTypeDictionaryForTargetType(
+ targetType
+ );
+ return resourceTypes.filter(resourceType => {
+ return resourceType in resourceDictionnary;
+ });
+}
+exports.getResourceTypesForTargetType = getResourceTypesForTargetType;
+
+function hasResourceTypesForTargets(resourceTypes) {
+ return resourceTypes.some(resourceType => {
+ return resourceType in FrameTargetResources;
+ });
+}
+exports.hasResourceTypesForTargets = hasResourceTypesForTargets;
+
+/**
+ * Stop watching for a list of resource types.
+ *
+ * @param Actor rootOrWatcherOrTargetActor
+ * The related actor, already passed to watchResources.
+ * @param Array<String> resourceTypes
+ * List of all type of resource to stop listening to.
+ */
+function unwatchResources(rootOrWatcherOrTargetActor, resourceTypes) {
+ for (const resourceType of resourceTypes) {
+ // Pull all info about this resource type from `Resources` global object
+ const { watchers } = getResourceTypeEntry(
+ rootOrWatcherOrTargetActor,
+ resourceType
+ );
+
+ const watcher = watchers.get(rootOrWatcherOrTargetActor);
+ if (watcher) {
+ watcher.destroy();
+ watchers.delete(rootOrWatcherOrTargetActor);
+ }
+ }
+}
+exports.unwatchResources = unwatchResources;
+
+/**
+ * Clear resources for a list of resource types.
+ *
+ * @param Actor rootOrWatcherOrTargetActor
+ * The related actor, already passed to watchResources.
+ * @param Array<String> resourceTypes
+ * List of all type of resource to clear.
+ */
+function clearResources(rootOrWatcherOrTargetActor, resourceTypes) {
+ for (const resourceType of resourceTypes) {
+ const { watchers } = getResourceTypeEntry(
+ rootOrWatcherOrTargetActor,
+ resourceType
+ );
+
+ const watcher = watchers.get(rootOrWatcherOrTargetActor);
+ if (watcher && typeof watcher.clear == "function") {
+ watcher.clear();
+ }
+ }
+}
+
+exports.clearResources = clearResources;
+
+/**
+ * Stop watching for all watched resources on a given actor.
+ *
+ * @param Actor rootOrWatcherOrTargetActor
+ * The related actor, already passed to watchResources.
+ */
+function unwatchAllResources(rootOrWatcherOrTargetActor) {
+ for (const { watchers } of Object.values(
+ getResourceTypeDictionary(rootOrWatcherOrTargetActor)
+ )) {
+ const watcher = watchers.get(rootOrWatcherOrTargetActor);
+ if (watcher) {
+ watcher.destroy();
+ watchers.delete(rootOrWatcherOrTargetActor);
+ }
+ }
+}
+exports.unwatchAllResources = unwatchAllResources;
+
+/**
+ * If we are watching for the given resource type,
+ * return the current ResourceWatcher instance used by this target actor
+ * in order to observe this resource type.
+ *
+ * @param Actor watcherOrTargetActor
+ * Either a WatcherActor or a TargetActor which can be listening to a resource.
+ * WatcherActor will be used for resources listened from the parent process,
+ * and TargetActor will be used for resources listened from the content process.
+ * @param String resourceType
+ * The resource type to query
+ * @return ResourceWatcher
+ * The resource watcher instance, defined in devtools/server/actors/resources/
+ */
+function getResourceWatcher(watcherOrTargetActor, resourceType) {
+ const { watchers } = getResourceTypeEntry(watcherOrTargetActor, resourceType);
+
+ return watchers.get(watcherOrTargetActor);
+}
+exports.getResourceWatcher = getResourceWatcher;
diff --git a/devtools/server/actors/resources/last-private-context-exit.js b/devtools/server/actors/resources/last-private-context-exit.js
new file mode 100644
index 0000000000..ec9ee6b91d
--- /dev/null
+++ b/devtools/server/actors/resources/last-private-context-exit.js
@@ -0,0 +1,46 @@
+/* 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: { LAST_PRIVATE_CONTEXT_EXIT },
+} = require("resource://devtools/server/actors/resources/index.js");
+
+class LastPrivateContextExitWatcher {
+ #onAvailable;
+
+ /**
+ * Start watching for all times where we close a private browsing top level window.
+ * Meaning we should clear the console for all logs generated from these private browsing contexts.
+ *
+ * @param WatcherActor watcherActor
+ * The watcher actor in the parent process from which we should
+ * observe these events.
+ * @param Object options
+ * Dictionary object with following attributes:
+ * - onAvailable: mandatory function
+ * This will be called for each resource.
+ */
+ async watch(watcherActor, { onAvailable }) {
+ this.#onAvailable = onAvailable;
+ Services.obs.addObserver(this, "last-pb-context-exited");
+ }
+
+ observe(subject, topic) {
+ if (topic === "last-pb-context-exited") {
+ this.#onAvailable([
+ {
+ resourceType: LAST_PRIVATE_CONTEXT_EXIT,
+ },
+ ]);
+ }
+ }
+
+ destroy() {
+ Services.obs.removeObserver(this, "last-pb-context-exited");
+ }
+}
+
+module.exports = LastPrivateContextExitWatcher;
diff --git a/devtools/server/actors/resources/moz.build b/devtools/server/actors/resources/moz.build
new file mode 100644
index 0000000000..c5f4c5e4c4
--- /dev/null
+++ b/devtools/server/actors/resources/moz.build
@@ -0,0 +1,39 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "utils",
+]
+
+DevToolsModules(
+ "console-messages.js",
+ "css-changes.js",
+ "css-messages.js",
+ "document-event.js",
+ "error-messages.js",
+ "extensions-backgroundscript-status.js",
+ "index.js",
+ "last-private-context-exit.js",
+ "network-events-content.js",
+ "network-events-stacktraces.js",
+ "network-events.js",
+ "parent-process-document-event.js",
+ "platform-messages.js",
+ "reflow.js",
+ "server-sent-events.js",
+ "sources.js",
+ "storage-cache.js",
+ "storage-cookie.js",
+ "storage-indexed-db.js",
+ "storage-local-storage.js",
+ "storage-session-storage.js",
+ "stylesheets.js",
+ "thread-states.js",
+ "websockets.js",
+)
+
+with Files("*-messages.js"):
+ BUG_COMPONENT = ("DevTools", "Console")
diff --git a/devtools/server/actors/resources/network-events-content.js b/devtools/server/actors/resources/network-events-content.js
new file mode 100644
index 0000000000..7d5607faec
--- /dev/null
+++ b/devtools/server/actors/resources/network-events-content.js
@@ -0,0 +1,258 @@
+/* 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";
+
+loader.lazyRequireGetter(
+ this,
+ "NetworkEventActor",
+ "resource://devtools/server/actors/network-monitor/network-event-actor.js",
+ true
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ NetworkUtils:
+ "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
+});
+
+/**
+ * Handles network events from the content process
+ * This currently only handles events for requests (js/css) blocked by CSP.
+ */
+class NetworkEventContentWatcher {
+ /**
+ * Start watching for all network events related to a given Target Actor.
+ *
+ * @param TargetActor targetActor
+ * The target actor in the content process from which we should
+ * observe network events.
+ * @param Object options
+ * Dictionary object with following attributes:
+ * - onAvailable: mandatory function
+ * This will be called for each resource.
+ * - onUpdated: optional function
+ * This would be called multiple times for each resource.
+ */
+ async watch(targetActor, { onAvailable, onUpdated }) {
+ this._networkEvents = new Map();
+
+ this.targetActor = targetActor;
+ this.onAvailable = onAvailable;
+ this.onUpdated = onUpdated;
+
+ this.httpFailedOpeningRequest = this.httpFailedOpeningRequest.bind(this);
+ this.httpOnImageCacheResponse = this.httpOnImageCacheResponse.bind(this);
+
+ Services.obs.addObserver(
+ this.httpFailedOpeningRequest,
+ "http-on-failed-opening-request"
+ );
+
+ Services.obs.addObserver(
+ this.httpOnImageCacheResponse,
+ "http-on-image-cache-response"
+ );
+ }
+ /**
+ * Allows clearing of network events
+ */
+ clear() {
+ this._networkEvents.clear();
+ }
+
+ get conn() {
+ return this.targetActor.conn;
+ }
+
+ httpFailedOpeningRequest(subject, topic) {
+ const channel = subject.QueryInterface(Ci.nsIHttpChannel);
+
+ // Ignore preload requests to avoid duplicity request entries in
+ // the Network panel. If a preload fails (for whatever reason)
+ // then the platform kicks off another 'real' request.
+ if (lazy.NetworkUtils.isPreloadRequest(channel)) {
+ return;
+ }
+
+ if (
+ !lazy.NetworkUtils.matchRequest(channel, {
+ targetActor: this.targetActor,
+ })
+ ) {
+ return;
+ }
+
+ this.onNetworkEventAvailable(channel, {
+ networkEventOptions: {
+ blockedReason: channel.loadInfo.requestBlockingReason,
+ },
+ resourceOverrides: null,
+ onNetworkEventUpdate: this.onFailedNetworkEventUpdated.bind(this),
+ });
+ }
+
+ httpOnImageCacheResponse(subject, topic) {
+ if (
+ topic != "http-on-image-cache-response" ||
+ !(subject instanceof Ci.nsIHttpChannel)
+ ) {
+ return;
+ }
+
+ const channel = subject.QueryInterface(Ci.nsIHttpChannel);
+
+ if (
+ !lazy.NetworkUtils.matchRequest(channel, {
+ targetActor: this.targetActor,
+ })
+ ) {
+ return;
+ }
+
+ // Only one network request should be created per URI for images from the cache
+ const hasNetworkEventForURI = Array.from(this._networkEvents.values()).find(
+ networkEvent => networkEvent.url === channel.URI.spec
+ );
+
+ if (hasNetworkEventForURI) {
+ return;
+ }
+
+ this.onNetworkEventAvailable(channel, {
+ networkEventOptions: { fromCache: true },
+ resourceOverrides: {
+ status: 200,
+ statusText: "OK",
+ totalTime: 0,
+ mimeType: channel.contentType,
+ contentSize: channel.contentLength,
+ },
+ onNetworkEventUpdate: this.onImageCacheNetworkEventUpdated.bind(this),
+ });
+ }
+
+ onNetworkEventAvailable(
+ channel,
+ { networkEventOptions, resourceOverrides, onNetworkEventUpdate }
+ ) {
+ const event = lazy.NetworkUtils.createNetworkEvent(
+ channel,
+ networkEventOptions
+ );
+
+ const actor = new NetworkEventActor(
+ this.conn,
+ this.targetActor.sessionContext,
+ {
+ onNetworkEventUpdate,
+ onNetworkEventDestroy: this.onNetworkEventDestroyed.bind(this),
+ },
+ event
+ );
+ this.targetActor.manage(actor);
+
+ const resource = actor.asResource();
+
+ this._networkEvents.set(resource.resourceId, {
+ browsingContextID: resource.browsingContextID,
+ innerWindowId: resource.innerWindowId,
+ resourceId: resource.resourceId,
+ resourceType: resource.resourceType,
+ url: resource.url,
+ types: [],
+ resourceUpdates: {},
+ });
+
+ // Override the default resource property values if need be
+ if (resourceOverrides) {
+ for (const prop in resourceOverrides) {
+ resource[prop] = resourceOverrides[prop];
+ }
+ }
+
+ this.onAvailable([resource]);
+ const {
+ cookies,
+ headers,
+ } = lazy.NetworkUtils.fetchRequestHeadersAndCookies(channel);
+ actor.addRequestHeaders(headers);
+ actor.addRequestCookies(cookies);
+ }
+
+ /*
+ * When an update is needed for a network event.
+ *
+ * @param {Object} updateResource
+ * The resource to be updated
+ * @param {Array} allRequiredUpdates
+ * The updates that are essential to be received before notifying
+ * the client. Other updates may or may not be available.
+ */
+
+ onNetworkEventUpdated(updateResource, allRequiredUpdates) {
+ const networkEvent = this._networkEvents.get(updateResource.resourceId);
+
+ if (!networkEvent) {
+ return;
+ }
+
+ const {
+ browsingContextID,
+ innerWindowId,
+ resourceId,
+ resourceType,
+ resourceUpdates,
+ types,
+ } = networkEvent;
+
+ resourceUpdates[`${updateResource.updateType}Available`] = true;
+ types.push(updateResource.updateType);
+
+ if (allRequiredUpdates.every(header => types.includes(header))) {
+ this.onUpdated([
+ {
+ browsingContextID,
+ innerWindowId,
+ resourceType,
+ resourceId,
+ resourceUpdates,
+ },
+ ]);
+ }
+ }
+
+ onFailedNetworkEventUpdated(updateResource) {
+ this.onNetworkEventUpdated(updateResource, [
+ "requestHeaders",
+ "requestCookies",
+ ]);
+ }
+
+ onImageCacheNetworkEventUpdated(updateResource) {
+ this.onNetworkEventUpdated(updateResource, ["requestHeaders"]);
+ }
+
+ onNetworkEventDestroyed(channelId) {
+ if (this._networkEvents.has(channelId)) {
+ this._networkEvents.delete(channelId);
+ }
+ }
+
+ destroy() {
+ this.clear();
+ Services.obs.removeObserver(
+ this.httpFailedOpeningRequest,
+ "http-on-failed-opening-request"
+ );
+
+ Services.obs.removeObserver(
+ this.httpOnImageCacheResponse,
+ "http-on-image-cache-response"
+ );
+ }
+}
+
+module.exports = NetworkEventContentWatcher;
diff --git a/devtools/server/actors/resources/network-events-stacktraces.js b/devtools/server/actors/resources/network-events-stacktraces.js
new file mode 100644
index 0000000000..075c7d3ccd
--- /dev/null
+++ b/devtools/server/actors/resources/network-events-stacktraces.js
@@ -0,0 +1,207 @@
+/* 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: { NETWORK_EVENT_STACKTRACE },
+} = require("resource://devtools/server/actors/resources/index.js");
+
+loader.lazyRequireGetter(
+ this,
+ "ChannelEventSinkFactory",
+ "resource://devtools/server/actors/network-monitor/channel-event-sink.js",
+ true
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ NetworkUtils:
+ "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
+});
+
+class NetworkEventStackTracesWatcher {
+ /**
+ * Start watching for all network event's stack traces related to a given Target actor.
+ *
+ * @param TargetActor targetActor
+ * The target actor from which we should observe the strack traces
+ * @param Object options
+ * Dictionary object with following attributes:
+ * - onAvailable: mandatory
+ * This will be called for each resource.
+ */
+ async watch(targetActor, { onAvailable }) {
+ this.stacktraces = new Map();
+ this.onStackTraceAvailable = onAvailable;
+ this.targetActor = targetActor;
+
+ Services.obs.addObserver(this, "http-on-opening-request");
+ Services.obs.addObserver(this, "document-on-opening-request");
+ Services.obs.addObserver(this, "network-monitor-alternate-stack");
+ ChannelEventSinkFactory.getService().registerCollector(this);
+ }
+
+ /**
+ * Allows clearing of network stacktrace resources
+ */
+ clear() {
+ this.stacktraces.clear();
+ }
+
+ /**
+ * Stop watching for network event's strack traces related to a given Target Actor.
+ *
+ * @param TargetActor targetActor
+ * The target actor from which we should stop observing the strack traces
+ */
+ destroy(targetActor) {
+ this.clear();
+ Services.obs.removeObserver(this, "http-on-opening-request");
+ Services.obs.removeObserver(this, "document-on-opening-request");
+ Services.obs.removeObserver(this, "network-monitor-alternate-stack");
+ ChannelEventSinkFactory.getService().unregisterCollector(this);
+ }
+
+ onChannelRedirect(oldChannel, newChannel, flags) {
+ // We can be called with any nsIChannel, but are interested only in HTTP channels
+ try {
+ oldChannel.QueryInterface(Ci.nsIHttpChannel);
+ newChannel.QueryInterface(Ci.nsIHttpChannel);
+ } catch (ex) {
+ return;
+ }
+
+ const oldId = oldChannel.channelId;
+ const stacktrace = this.stacktraces.get(oldId);
+ if (stacktrace) {
+ this._setStackTrace(newChannel.channelId, stacktrace);
+ }
+ }
+
+ observe(subject, topic, data) {
+ let channel, id;
+ try {
+ // We need to QI nsIHttpChannel in order to load the interface's
+ // methods / attributes for later code that could assume we are dealing
+ // with a nsIHttpChannel.
+ channel = subject.QueryInterface(Ci.nsIHttpChannel);
+ id = channel.channelId;
+ } catch (e1) {
+ try {
+ channel = subject.QueryInterface(Ci.nsIIdentChannel);
+ id = channel.channelId;
+ } catch (e2) {
+ // WebSocketChannels do not have IDs, so use the serial. When a WebSocket is
+ // opened in a content process, a channel is created locally but the HTTP
+ // channel for the connection lives entirely in the parent process. When
+ // the server code running in the parent sees that HTTP channel, it will
+ // look for the creation stack using the websocket's serial.
+ try {
+ channel = subject.QueryInterface(Ci.nsIWebSocketChannel);
+ id = channel.serial;
+ } catch (e3) {
+ // Channels which don't implement the above interfaces can appear here,
+ // such as nsIFileChannel. Ignore these channels.
+ return;
+ }
+ }
+ }
+
+ if (
+ !lazy.NetworkUtils.matchRequest(channel, {
+ targetActor: this.targetActor,
+ })
+ ) {
+ return;
+ }
+
+ if (this.stacktraces.has(id)) {
+ // We can get up to two stack traces for the same channel: one each from
+ // the two observer topics we are listening to. Use the first stack trace
+ // which is specified, and ignore any later one.
+ return;
+ }
+
+ const stacktrace = [];
+ switch (topic) {
+ case "http-on-opening-request":
+ case "document-on-opening-request": {
+ // The channel is being opened on the main thread, associate the current
+ // stack with it.
+ //
+ // Convert the nsIStackFrame XPCOM objects to a nice JSON that can be
+ // passed around through message managers etc.
+ let frame = Components.stack;
+ if (frame?.caller) {
+ frame = frame.caller;
+ while (frame) {
+ stacktrace.push({
+ filename: frame.filename,
+ lineNumber: frame.lineNumber,
+ columnNumber: frame.columnNumber,
+ functionName: frame.name,
+ asyncCause: frame.asyncCause,
+ });
+ frame = frame.caller || frame.asyncCaller;
+ }
+ }
+ break;
+ }
+ case "network-monitor-alternate-stack": {
+ // An alternate stack trace is being specified for this channel.
+ // The topic data is the JSON for the saved frame stack we should use,
+ // so convert this into the expected format.
+ //
+ // This topic is used in the following cases:
+ //
+ // - The HTTP channel is opened asynchronously or on a different thread
+ // from the code which triggered its creation, in which case the stack
+ // from Components.stack will be empty. The alternate stack will be
+ // for the point we want to associate with the channel.
+ //
+ // - The channel is not a nsIHttpChannel, and we will receive no
+ // opening request notification for it.
+ let frame = JSON.parse(data);
+ while (frame) {
+ stacktrace.push({
+ filename: frame.source,
+ lineNumber: frame.line,
+ columnNumber: frame.column,
+ functionName: frame.functionDisplayName,
+ asyncCause: frame.asyncCause,
+ });
+ frame = frame.parent || frame.asyncParent;
+ }
+ break;
+ }
+ default:
+ throw new Error("Unexpected observe() topic");
+ }
+
+ this._setStackTrace(id, stacktrace);
+ }
+
+ _setStackTrace(resourceId, stacktrace) {
+ this.stacktraces.set(resourceId, stacktrace);
+ this.onStackTraceAvailable([
+ {
+ resourceType: NETWORK_EVENT_STACKTRACE,
+ resourceId,
+ stacktraceAvailable: stacktrace && !!stacktrace.length,
+ lastFrame: stacktrace && stacktrace.length ? stacktrace[0] : undefined,
+ },
+ ]);
+ }
+
+ getStackTrace(id) {
+ let stacktrace = [];
+ if (this.stacktraces.has(id)) {
+ stacktrace = this.stacktraces.get(id);
+ }
+ return stacktrace;
+ }
+}
+module.exports = NetworkEventStackTracesWatcher;
diff --git a/devtools/server/actors/resources/network-events.js b/devtools/server/actors/resources/network-events.js
new file mode 100644
index 0000000000..c781ecc45f
--- /dev/null
+++ b/devtools/server/actors/resources/network-events.js
@@ -0,0 +1,392 @@
+/* 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 { Pool } = require("resource://devtools/shared/protocol/Pool.js");
+const { isWindowGlobalPartOfContext } = ChromeUtils.importESModule(
+ "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs"
+);
+const { WatcherRegistry } = ChromeUtils.importESModule(
+ "resource://devtools/server/actors/watcher/WatcherRegistry.sys.mjs",
+ {
+ // WatcherRegistry needs to be a true singleton and loads ActorManagerParent
+ // which also has to be a true singleton.
+ loadInDevToolsLoader: false,
+ }
+);
+const Targets = require("resource://devtools/server/actors/targets/index.js");
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ NetworkObserver:
+ "resource://devtools/shared/network-observer/NetworkObserver.sys.mjs",
+ NetworkUtils:
+ "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
+});
+
+loader.lazyRequireGetter(
+ this,
+ "NetworkEventActor",
+ "resource://devtools/server/actors/network-monitor/network-event-actor.js",
+ true
+);
+
+/**
+ * Handles network events from the parent process
+ */
+class NetworkEventWatcher {
+ /**
+ * Start watching for all network events related to a given Watcher Actor.
+ *
+ * @param WatcherActor watcherActor
+ * The watcher actor in the parent process from which we should
+ * observe network events.
+ * @param Object options
+ * Dictionary object with following attributes:
+ * - onAvailable: mandatory function
+ * This will be called for each resource.
+ * - onUpdated: optional function
+ * This would be called multiple times for each resource.
+ */
+ async watch(watcherActor, { onAvailable, onUpdated }) {
+ this.networkEvents = new Map();
+
+ this.watcherActor = watcherActor;
+ this.pool = new Pool(watcherActor.conn, "network-events");
+ this.watcherActor.manage(this.pool);
+ this.onNetworkEventAvailable = onAvailable;
+ this.onNetworkEventUpdated = onUpdated;
+ // Boolean to know if we keep previous document network events or not.
+ this.persist = false;
+ this.listener = new lazy.NetworkObserver({
+ ignoreChannelFunction: this.shouldIgnoreChannel.bind(this),
+ onNetworkEvent: this.onNetworkEvent.bind(this),
+ });
+
+ Services.obs.addObserver(this, "window-global-destroyed");
+ }
+
+ /**
+ * Clear all the network events and the related actors.
+ */
+ clear() {
+ this.networkEvents.clear();
+ this.listener.clear();
+ this.pool.destroy();
+ }
+
+ get conn() {
+ return this.watcherActor.conn;
+ }
+
+ /**
+ * Instruct to keep reference to previous document requests or not.
+ * If persist is disabled, we will clear all informations about previous document
+ * on each navigation.
+ * If persist is enabled, we will keep all informations for all documents, leading
+ * to lots of allocations!
+ *
+ * @param {Boolean} enabled
+ */
+ setPersist(enabled) {
+ this.persist = enabled;
+ }
+
+ /**
+ * Gets the throttle settings
+ *
+ * @return {*} data
+ *
+ */
+ getThrottleData() {
+ return this.listener.getThrottleData();
+ }
+
+ /**
+ * Sets the throttle data
+ *
+ * @param {*} data
+ *
+ */
+ setThrottleData(data) {
+ this.listener.setThrottleData(data);
+ }
+
+ /**
+ * Instruct to save or ignore request and response bodies
+ * @param {Boolean} save
+ */
+ setSaveRequestAndResponseBodies(save) {
+ this.listener.setSaveRequestAndResponseBodies(save);
+ }
+
+ /**
+ * Block requests based on the filters
+ * @param {Object} filters
+ */
+ blockRequest(filters) {
+ this.listener.blockRequest(filters);
+ }
+
+ /**
+ * Unblock requests based on the fitlers
+ * @param {Object} filters
+ */
+ unblockRequest(filters) {
+ this.listener.unblockRequest(filters);
+ }
+
+ /**
+ * Calls the listener to set blocked urls
+ *
+ * @param {Array} urls
+ * The urls to block
+ */
+
+ setBlockedUrls(urls) {
+ this.listener.setBlockedUrls(urls);
+ }
+
+ /**
+ * Calls the listener to get the blocked urls
+ *
+ * @return {Array} urls
+ * The blocked urls
+ */
+
+ getBlockedUrls() {
+ return this.listener.getBlockedUrls();
+ }
+
+ /**
+ * Watch for previous document being unloaded in order to clear
+ * all related network events, in case persist is disabled.
+ * (which is the default behavior)
+ */
+ observe(windowGlobal, topic) {
+ if (topic !== "window-global-destroyed") {
+ return;
+ }
+ // If we persist, we will keep all requests allocated.
+ // For now, consider that the Browser console and toolbox persist all the requests.
+ if (this.persist || this.watcherActor.sessionContext.type == "all") {
+ return;
+ }
+ // Only process WindowGlobals which are related to the debugged scope.
+ if (
+ !isWindowGlobalPartOfContext(
+ windowGlobal,
+ this.watcherActor.sessionContext
+ )
+ ) {
+ return;
+ }
+ const { innerWindowId } = windowGlobal;
+
+ for (const child of this.pool.poolChildren()) {
+ // Destroy all network events matching the destroyed WindowGlobal
+ if (!child.isNavigationRequest) {
+ if (child.innerWindowId == innerWindowId) {
+ child.destroy();
+ }
+ // Avoid destroying the navigation request, which is flagged with previous document's innerWindowId.
+ // When navigating, the WindowGlobal we navigate *from* will be destroyed and notified here.
+ // We should explicitly avoid destroying it here.
+ // But, we still want to eventually destroy them.
+ // So do this when navigating a second time, we will navigate from a distinct WindowGlobal
+ // and check that this is the top level window global and not an iframe one.
+ // So that we avoid clearing the top navigation when an iframe navigates
+ //
+ // Avoid destroying the request if innerWindowId isn't set. This happens when we reload many times in a row.
+ // The previous navigation request will be cancelled and because of that its innerWindowId will be null.
+ // But the frontend will receive it after the navigation begins (after will-navigate) and will display it
+ // and try to fetch extra data about it. So, avoid destroying its NetworkEventActor.
+ } else if (
+ child.innerWindowId &&
+ child.innerWindowId != innerWindowId &&
+ windowGlobal.browsingContext ==
+ this.watcherActor.browserElement?.browsingContext
+ ) {
+ child.destroy();
+ }
+ }
+ }
+
+ /**
+ * Called by NetworkObserver in order to know if the channel should be ignored
+ */
+ shouldIgnoreChannel(channel) {
+ // First of all, check if the channel matches the watcherActor's session.
+ const filters = { sessionContext: this.watcherActor.sessionContext };
+ if (!lazy.NetworkUtils.matchRequest(channel, filters)) {
+ return true;
+ }
+
+ // When we are in the browser toolbox in parent process scope,
+ // the session context is still "all", but we are no longer watching frame and process targets.
+ // In this case, we should ignore all requests belonging to a BrowsingContext that isn't in the parent process
+ // (i.e. the process where this Watcher runs)
+ const isParentProcessOnlyBrowserToolbox =
+ this.watcherActor.sessionContext.type == "all" &&
+ !WatcherRegistry.isWatchingTargets(
+ this.watcherActor,
+ Targets.TYPES.FRAME
+ );
+ if (isParentProcessOnlyBrowserToolbox) {
+ // We should ignore all requests coming from BrowsingContext running in another process
+ const browsingContextID = lazy.NetworkUtils.getChannelBrowsingContextID(
+ channel
+ );
+ const browsingContext = BrowsingContext.get(browsingContextID);
+ // We accept any request that isn't bound to any BrowsingContext.
+ // This is most likely a privileged request done from a JSM/C++.
+ // `isInProcess` will be true, when the document executes in the parent process.
+ //
+ // Note that we will still accept all requests that aren't bound to any BrowsingContext
+ // See browser_resources_network_events_parent_process.js test with privileged request
+ // made from the content processes.
+ // We miss some attribute on channel/loadInfo to know that it comes from the content process.
+ if (browsingContext?.currentWindowGlobal.isInProcess === false) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ onNetworkEvent(event) {
+ const { channelId } = event;
+
+ if (this.networkEvents.has(channelId)) {
+ throw new Error(
+ `Got notified about channel ${channelId} more than once.`
+ );
+ }
+
+ const actor = new NetworkEventActor(
+ this.watcherActor.conn,
+ this.watcherActor.sessionContext,
+ {
+ onNetworkEventUpdate: this.onNetworkEventUpdate.bind(this),
+ onNetworkEventDestroy: this.onNetworkEventDestroy.bind(this),
+ },
+ event
+ );
+ this.pool.manage(actor);
+
+ const resource = actor.asResource();
+
+ this.networkEvents.set(resource.resourceId, {
+ browsingContextID: resource.browsingContextID,
+ innerWindowId: resource.innerWindowId,
+ resourceId: resource.resourceId,
+ resourceType: resource.resourceType,
+ isBlocked: !!resource.blockedReason,
+ types: [],
+ resourceUpdates: {},
+ });
+
+ this.onNetworkEventAvailable([resource]);
+ return actor;
+ }
+
+ onNetworkEventUpdate(updateResource) {
+ const networkEvent = this.networkEvents.get(updateResource.resourceId);
+
+ if (!networkEvent) {
+ return;
+ }
+
+ const {
+ browsingContextID,
+ innerWindowId,
+ resourceId,
+ resourceType,
+ resourceUpdates,
+ types,
+ isBlocked,
+ } = networkEvent;
+
+ switch (updateResource.updateType) {
+ case "responseStart":
+ resourceUpdates.httpVersion = updateResource.httpVersion;
+ resourceUpdates.status = updateResource.status;
+ resourceUpdates.statusText = updateResource.statusText;
+ resourceUpdates.remoteAddress = updateResource.remoteAddress;
+ resourceUpdates.remotePort = updateResource.remotePort;
+ // The mimetype is only set when then the contentType is available
+ // in the _onResponseHeader and not for cached/service worker requests
+ // in _httpResponseExaminer.
+ resourceUpdates.mimeType = updateResource.mimeType;
+ resourceUpdates.waitingTime = updateResource.waitingTime;
+ break;
+ case "responseContent":
+ resourceUpdates.contentSize = updateResource.contentSize;
+ resourceUpdates.transferredSize = updateResource.transferredSize;
+ resourceUpdates.mimeType = updateResource.mimeType;
+ resourceUpdates.blockingExtension = updateResource.blockingExtension;
+ resourceUpdates.blockedReason = updateResource.blockedReason;
+ break;
+ case "eventTimings":
+ resourceUpdates.totalTime = updateResource.totalTime;
+ break;
+ case "securityInfo":
+ resourceUpdates.securityState = updateResource.state;
+ resourceUpdates.isRacing = updateResource.isRacing;
+ break;
+ }
+
+ resourceUpdates[`${updateResource.updateType}Available`] = true;
+ types.push(updateResource.updateType);
+
+ if (isBlocked) {
+ // Blocked requests
+ if (
+ !types.includes("requestHeaders") ||
+ !types.includes("requestCookies")
+ ) {
+ return;
+ }
+ } else if (
+ // Un-blocked requests
+ !types.includes("requestHeaders") ||
+ !types.includes("requestCookies") ||
+ !types.includes("eventTimings") ||
+ !types.includes("responseContent") ||
+ !types.includes("securityInfo")
+ ) {
+ return;
+ }
+
+ this.onNetworkEventUpdated([
+ {
+ resourceType,
+ resourceId,
+ resourceUpdates,
+ browsingContextID,
+ innerWindowId,
+ },
+ ]);
+ }
+
+ onNetworkEventDestroy(channelId) {
+ if (this.networkEvents.has(channelId)) {
+ this.networkEvents.delete(channelId);
+ }
+ }
+
+ /**
+ * Stop watching for network event related to a given Watcher Actor.
+ */
+ destroy() {
+ if (this.listener) {
+ this.clear();
+ this.listener.destroy();
+ Services.obs.removeObserver(this, "window-global-destroyed");
+ }
+ }
+}
+
+module.exports = NetworkEventWatcher;
diff --git a/devtools/server/actors/resources/parent-process-document-event.js b/devtools/server/actors/resources/parent-process-document-event.js
new file mode 100644
index 0000000000..7d7b14fbc5
--- /dev/null
+++ b/devtools/server/actors/resources/parent-process-document-event.js
@@ -0,0 +1,177 @@
+/* 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: { DOCUMENT_EVENT },
+} = require("resource://devtools/server/actors/resources/index.js");
+const isEveryFrameTargetEnabled = Services.prefs.getBoolPref(
+ "devtools.every-frame-target.enabled",
+ false
+);
+const { getAllBrowsingContextsForContext } = ChromeUtils.importESModule(
+ "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs"
+);
+const {
+ WILL_NAVIGATE_TIME_SHIFT,
+} = require("resource://devtools/server/actors/webconsole/listeners/document-events.js");
+
+class ParentProcessDocumentEventWatcher {
+ /**
+ * Start watching, from the parent process, for DOCUMENT_EVENT's "will-navigate" event related to a given Watcher Actor.
+ *
+ * All other DOCUMENT_EVENT events are implemented from another watcher class, running in the content process.
+ * Note that this other content process watcher will also emit one special edgecase of will-navigate
+ * retlated to the iframe dropdown menu.
+ *
+ * We have to move listen for navigation in the parent to better handle bfcache navigations
+ * and more generally all navigations which are initiated from the parent process.
+ * 'bfcacheInParent' feature enabled many types of navigations to be controlled from the parent process.
+ *
+ * This was especially important to have this implementation in the parent
+ * because the navigation event may be fired too late in the content process.
+ * Leading to will-navigate being emitted *after* the new target we navigate to is notified to the client.
+ *
+ * @param WatcherActor watcherActor
+ * The watcher actor from which we should observe document event
+ * @param Object options
+ * Dictionary object with following attributes:
+ * - onAvailable: mandatory function
+ * This will be called for each resource.
+ */
+ async watch(watcherActor, { onAvailable }) {
+ this.watcherActor = watcherActor;
+ this.onAvailable = onAvailable;
+
+ // List of listeners keyed by innerWindowId.
+ // Listeners are called as soon as we emitted the will-navigate
+ // resource for the related WindowGlobal.
+ this._onceWillNavigate = new Map();
+
+ // Filter browsing contexts to only have the top BrowsingContext of each tree of BrowsingContexts…
+ const topLevelBrowsingContexts = getAllBrowsingContextsForContext(
+ this.watcherActor.sessionContext
+ ).filter(browsingContext => browsingContext.top == browsingContext);
+
+ // Only register one WebProgressListener per BrowsingContext tree.
+ // We will be notified about children BrowsingContext navigations/state changes via the top level BrowsingContextWebProgressListener,
+ // and BrowsingContextWebProgress.browsingContext attribute will be updated dynamically everytime
+ // we get notified about a child BrowsingContext.
+ // Note that regular web page toolbox will only have one BrowsingContext tree, for the given tab.
+ // But the Browser Toolbox will have many trees to listen to, one per top-level Window, and also one per tab,
+ // as tabs's BrowsingContext context aren't children of their top level window!
+ //
+ // Also save the WebProgress and not the BrowsingContext because `BrowsingContext.webProgress` will be undefined in destroy(),
+ // while it is still valuable to call `webProgress.removeProgressListener`. Otherwise events keeps being notified!!
+ this.webProgresses = topLevelBrowsingContexts.map(
+ browsingContext => browsingContext.webProgress
+ );
+ this.webProgresses.forEach(webProgress => {
+ webProgress.addProgressListener(
+ this,
+ Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
+ );
+ });
+ }
+
+ /**
+ * Wait for the emission of will-navigate for a given WindowGlobal
+ *
+ * @param Number innerWindowId
+ * WindowGlobal's id we want to track
+ * @return Promise
+ * Resolves immediatly if the WindowGlobal isn't tracked by any target
+ * -or- resolve later, once the WindowGlobal navigates to another document
+ * and will-navigate has been emitted.
+ */
+ onceWillNavigateIsEmitted(innerWindowId) {
+ // Only delay the target-destroyed event if the target is for BrowsingContext for which we will emit will-navigate
+ const isTracked = this.webProgresses.find(
+ webProgress =>
+ webProgress.browsingContext.currentWindowGlobal.innerWindowId ==
+ innerWindowId
+ );
+ if (isTracked) {
+ return new Promise(resolve => {
+ this._onceWillNavigate.set(innerWindowId, resolve);
+ });
+ }
+ return Promise.resolve();
+ }
+
+ onStateChange(progress, request, flag, status) {
+ const isStart = flag & Ci.nsIWebProgressListener.STATE_START;
+ const isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
+ if (isDocument && isStart) {
+ const { browsingContext } = progress;
+ // Ignore navigation for same-process iframes when EFT is disabled
+ if (
+ !browsingContext.currentWindowGlobal.isProcessRoot &&
+ !isEveryFrameTargetEnabled
+ ) {
+ return;
+ }
+ // Ignore if we are still on the initial document,
+ // as that's the navigation from it (about:blank) to the actual first location.
+ // The target isn't created yet.
+ if (browsingContext.currentWindowGlobal.isInitialDocument) {
+ return;
+ }
+
+ // Only emit will-navigate for top-level targets.
+ if (
+ this.watcherActor.sessionContext.type == "all" &&
+ browsingContext.isContent
+ ) {
+ // Never emit will-navigate for content browsing contexts in the Browser Toolbox.
+ // They might verify `browsingContext.top == browsingContext` because of the chrome/content
+ // boundary, but they do not represent a top-level target for this DevTools session.
+ return;
+ }
+ const isTopLevel = browsingContext.top == browsingContext;
+ if (!isTopLevel) {
+ return;
+ }
+
+ const newURI = request instanceof Ci.nsIChannel ? request.URI.spec : null;
+ const { innerWindowId } = browsingContext.currentWindowGlobal;
+ this.onAvailable([
+ {
+ browsingContextID: browsingContext.id,
+ innerWindowId,
+ resourceType: DOCUMENT_EVENT,
+ name: "will-navigate",
+ time: Date.now() - WILL_NAVIGATE_TIME_SHIFT,
+ isFrameSwitching: false,
+ newURI,
+ },
+ ]);
+ const callback = this._onceWillNavigate.get(innerWindowId);
+ if (callback) {
+ this._onceWillNavigate.delete(innerWindowId);
+ callback();
+ }
+ }
+ }
+
+ get QueryInterface() {
+ return ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]);
+ }
+
+ destroy() {
+ this.webProgresses.forEach(webProgress => {
+ webProgress.removeProgressListener(
+ this,
+ Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
+ );
+ });
+ this.webProgresses = null;
+ }
+}
+
+module.exports = ParentProcessDocumentEventWatcher;
diff --git a/devtools/server/actors/resources/platform-messages.js b/devtools/server/actors/resources/platform-messages.js
new file mode 100644
index 0000000000..6d9750c0a2
--- /dev/null
+++ b/devtools/server/actors/resources/platform-messages.js
@@ -0,0 +1,60 @@
+/* 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 nsIConsoleListenerWatcher = require("resource://devtools/server/actors/resources/utils/nsi-console-listener-watcher.js");
+
+const {
+ TYPES: { PLATFORM_MESSAGE },
+} = require("resource://devtools/server/actors/resources/index.js");
+
+const {
+ createStringGrip,
+} = require("resource://devtools/server/actors/object/utils.js");
+
+class PlatformMessageWatcher extends nsIConsoleListenerWatcher {
+ shouldHandleTarget(targetActor) {
+ return this.isProcessTarget(targetActor);
+ }
+
+ /**
+ * Returns true if the message is considered a platform message, and as a result, should
+ * be sent to the client.
+ *
+ * @param {TargetActor} targetActor
+ * @param {nsIConsoleMessage} message
+ */
+ shouldHandleMessage(targetActor, message) {
+ // The listener we use can be called either with a nsIConsoleMessage or as nsIScriptError.
+ // In this file, we want to ignore nsIScriptError, which are handled by the
+ // error-messages resource handler (See Bug 1644186).
+ if (message instanceof Ci.nsIScriptError) {
+ return false;
+ }
+
+ // Ignore message that were forwarded from the content process to the parent process,
+ // since we're getting those directly from the content process.
+ if (message.isForwardedFromContentProcess) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns an object from the nsIConsoleMessage.
+ *
+ * @param {Actor} targetActor
+ * @param {nsIConsoleMessage} message
+ */
+ buildResource(targetActor, message) {
+ return {
+ message: createStringGrip(targetActor, message.message),
+ timeStamp: message.microSecondTimeStamp / 1000,
+ resourceType: PLATFORM_MESSAGE,
+ };
+ }
+}
+module.exports = PlatformMessageWatcher;
diff --git a/devtools/server/actors/resources/reflow.js b/devtools/server/actors/resources/reflow.js
new file mode 100644
index 0000000000..5be9d6e7b2
--- /dev/null
+++ b/devtools/server/actors/resources/reflow.js
@@ -0,0 +1,63 @@
+/* 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: { REFLOW },
+} = require("resource://devtools/server/actors/resources/index.js");
+const Targets = require("resource://devtools/server/actors/targets/index.js");
+
+const {
+ getLayoutChangesObserver,
+ releaseLayoutChangesObserver,
+} = require("resource://devtools/server/actors/reflow.js");
+
+class ReflowWatcher {
+ /**
+ * Start watching for reflows related to a given Target Actor.
+ *
+ * @param TargetActor targetActor
+ * The target actor from which we should observe reflows
+ * @param Object options
+ * Dictionary object with following attributes:
+ * - onAvailable: mandatory function
+ * This will be called for each resource.
+ */
+ async watch(targetActor, { onAvailable }) {
+ // Only track reflow for non-ParentProcess FRAME targets
+ if (
+ targetActor.targetType !== Targets.TYPES.FRAME ||
+ targetActor.typeName === "parentProcessTarget"
+ ) {
+ return;
+ }
+
+ this._targetActor = targetActor;
+
+ const onReflows = reflows => {
+ onAvailable([
+ {
+ resourceType: REFLOW,
+ reflows,
+ },
+ ]);
+ };
+
+ this._observer = getLayoutChangesObserver(targetActor);
+ this._offReflows = this._observer.on("reflows", onReflows);
+ this._observer.start();
+ }
+
+ destroy() {
+ releaseLayoutChangesObserver(this._targetActor);
+
+ if (this._offReflows) {
+ this._offReflows();
+ this._offReflows = null;
+ }
+ }
+}
+
+module.exports = ReflowWatcher;
diff --git a/devtools/server/actors/resources/server-sent-events.js b/devtools/server/actors/resources/server-sent-events.js
new file mode 100644
index 0000000000..5b16f8bb9f
--- /dev/null
+++ b/devtools/server/actors/resources/server-sent-events.js
@@ -0,0 +1,135 @@
+/* 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 {
+ LongStringActor,
+} = require("resource://devtools/server/actors/string.js");
+
+const {
+ TYPES: { SERVER_SENT_EVENT },
+} = require("resource://devtools/server/actors/resources/index.js");
+
+const eventSourceEventService = Cc[
+ "@mozilla.org/eventsourceevent/service;1"
+].getService(Ci.nsIEventSourceEventService);
+
+class ServerSentEventWatcher {
+ constructor() {
+ this.windowIds = new Set();
+ // Register for backend events.
+ this.onWindowReady = this.onWindowReady.bind(this);
+ this.onWindowDestroy = this.onWindowDestroy.bind(this);
+ }
+ /**
+ * Start watching for all server sent events related to a given Target Actor.
+ *
+ * @param TargetActor targetActor
+ * The target actor on which we should observe server sent events.
+ * @param Object options
+ * Dictionary object with following attributes:
+ * - onAvailable: mandatory function
+ * This will be called for each resource.
+ */
+ watch(targetActor, { onAvailable }) {
+ this.onAvailable = onAvailable;
+ this.targetActor = targetActor;
+
+ for (const window of this.targetActor.windows) {
+ const { innerWindowId } = window.windowGlobalChild;
+ this.startListening(innerWindowId);
+ }
+
+ // Listen for subsequent top-level-document reloads/navigations,
+ // new iframe additions or current iframe reloads/navigation.
+ this.targetActor.on("window-ready", this.onWindowReady);
+ this.targetActor.on("window-destroyed", this.onWindowDestroy);
+ }
+
+ static createResource(messageType, eventParams) {
+ return {
+ resourceType: SERVER_SENT_EVENT,
+ messageType,
+ ...eventParams,
+ };
+ }
+
+ static prepareFramePayload(targetActor, frame) {
+ const payload = new LongStringActor(targetActor.conn, frame);
+ targetActor.manage(payload);
+ return payload.form();
+ }
+
+ onWindowReady({ window }) {
+ const { innerWindowId } = window.windowGlobalChild;
+ this.startListening(innerWindowId);
+ }
+
+ onWindowDestroy({ id }) {
+ this.stopListening(id);
+ }
+
+ startListening(innerWindowId) {
+ if (!this.windowIds.has(innerWindowId)) {
+ this.windowIds.add(innerWindowId);
+ eventSourceEventService.addListener(innerWindowId, this);
+ }
+ }
+
+ stopListening(innerWindowId) {
+ if (this.windowIds.has(innerWindowId)) {
+ this.windowIds.delete(innerWindowId);
+ // The listener might have already been cleaned up on `window-destroy`.
+ if (!eventSourceEventService.hasListenerFor(innerWindowId)) {
+ console.warn(
+ "Already stopped listening to server sent events for this window."
+ );
+ return;
+ }
+ eventSourceEventService.removeListener(innerWindowId, this);
+ }
+ }
+
+ destroy() {
+ // cleanup any other listeners not removed on `window-destroy`
+ for (const id of this.windowIds) {
+ this.stopListening(id);
+ }
+ this.targetActor.off("window-ready", this.onWindowReady);
+ this.targetActor.off("window-destroyed", this.onWindowDestroy);
+ }
+
+ // nsIEventSourceEventService specific functions
+ eventSourceConnectionOpened(httpChannelId) {}
+
+ eventSourceConnectionClosed(httpChannelId) {
+ const resource = ServerSentEventWatcher.createResource(
+ "eventSourceConnectionClosed",
+ { httpChannelId }
+ );
+ this.onAvailable([resource]);
+ }
+
+ eventReceived(httpChannelId, eventName, lastEventId, data, retry, timeStamp) {
+ const payload = ServerSentEventWatcher.prepareFramePayload(
+ this.targetActor,
+ data
+ );
+ const resource = ServerSentEventWatcher.createResource("eventReceived", {
+ httpChannelId,
+ data: {
+ payload,
+ eventName,
+ lastEventId,
+ retry,
+ timeStamp,
+ },
+ });
+
+ this.onAvailable([resource]);
+ }
+}
+
+module.exports = ServerSentEventWatcher;
diff --git a/devtools/server/actors/resources/sources.js b/devtools/server/actors/resources/sources.js
new file mode 100644
index 0000000000..6076e333c9
--- /dev/null
+++ b/devtools/server/actors/resources/sources.js
@@ -0,0 +1,97 @@
+/* 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: { SOURCE },
+} = require("resource://devtools/server/actors/resources/index.js");
+const Targets = require("resource://devtools/server/actors/targets/index.js");
+
+const {
+ STATES: THREAD_STATES,
+} = require("resource://devtools/server/actors/thread.js");
+
+/**
+ * Start watching for all JS sources related to a given Target Actor.
+ * This will notify about existing sources, but also the ones created in future.
+ *
+ * @param TargetActor targetActor
+ * The target actor from which we should observe sources
+ * @param Object options
+ * Dictionary object with following attributes:
+ * - onAvailable: mandatory function
+ * This will be called for each resource.
+ */
+class SourceWatcher {
+ constructor() {
+ this.onNewSource = this.onNewSource.bind(this);
+ }
+
+ async watch(targetActor, { onAvailable }) {
+ // When debugging the whole browser, we instantiate both content process and browsing context targets.
+ // But sources will only be debugged the content process target, even browsing context sources.
+ if (
+ targetActor.sessionContext.type == "all" &&
+ targetActor.targetType === Targets.TYPES.FRAME &&
+ targetActor.typeName != "parentProcessTarget"
+ ) {
+ return;
+ }
+
+ const { threadActor } = targetActor;
+ this.sourcesManager = targetActor.sourcesManager;
+ this.onAvailable = onAvailable;
+
+ // Disable `ThreadActor.newSource` RDP event in order to avoid unnecessary traffic
+ threadActor.disableNewSourceEvents();
+
+ threadActor.sourcesManager.on("newSource", this.onNewSource);
+
+ // If the thread actors isn't bootstraped yet,
+ // (this might be the case when this watcher is created on target creation)
+ // attach the thread actor automatically.
+ // Otherwise it would not notify about future sources.
+ // 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)
+ // Content process targets don't have attach method or sequence.
+ // Instead their thread actor is instantiated immediately, when generating their
+ // form. Which is called immediately when we notify the target actor to the TargetList.
+ const isTargetCreation = threadActor.state == THREAD_STATES.DETACHED;
+ if (isTargetCreation && !targetActor.targetType.endsWith("worker")) {
+ await threadActor.attach({});
+ }
+
+ // Before fetching all sources, process existing ones.
+ // The ThreadActor is already up and running before this code runs
+ // and have sources already registered and for which newSource event already fired.
+ onAvailable(
+ threadActor.sourcesManager.iter().map(s => {
+ const resource = s.form();
+ resource.resourceType = SOURCE;
+ return resource;
+ })
+ );
+
+ // Requesting all sources should end up emitting newSource on threadActor.sourcesManager
+ threadActor.addAllSources();
+ }
+
+ /**
+ * Stop watching for sources
+ */
+ destroy() {
+ if (this.sourcesManager) {
+ this.sourcesManager.off("newSource", this.onNewSource);
+ }
+ }
+
+ onNewSource(source) {
+ const resource = source.form();
+ resource.resourceType = SOURCE;
+ this.onAvailable([resource]);
+ }
+}
+
+module.exports = SourceWatcher;
diff --git a/devtools/server/actors/resources/storage-cache.js b/devtools/server/actors/resources/storage-cache.js
new file mode 100644
index 0000000000..2a1153a962
--- /dev/null
+++ b/devtools/server/actors/resources/storage-cache.js
@@ -0,0 +1,19 @@
+/* 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: { CACHE_STORAGE },
+} = require("resource://devtools/server/actors/resources/index.js");
+
+const ContentProcessStorage = require("resource://devtools/server/actors/resources/utils/content-process-storage.js");
+
+class CacheWatcher extends ContentProcessStorage {
+ constructor() {
+ super("Cache", CACHE_STORAGE);
+ }
+}
+
+module.exports = CacheWatcher;
diff --git a/devtools/server/actors/resources/storage-cookie.js b/devtools/server/actors/resources/storage-cookie.js
new file mode 100644
index 0000000000..4f26c97039
--- /dev/null
+++ b/devtools/server/actors/resources/storage-cookie.js
@@ -0,0 +1,19 @@
+/* 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: { COOKIE },
+} = require("resource://devtools/server/actors/resources/index.js");
+
+const ParentProcessStorage = require("resource://devtools/server/actors/resources/utils/parent-process-storage.js");
+
+class CookiesWatcher extends ParentProcessStorage {
+ constructor() {
+ super("cookies", COOKIE);
+ }
+}
+
+module.exports = CookiesWatcher;
diff --git a/devtools/server/actors/resources/storage-indexed-db.js b/devtools/server/actors/resources/storage-indexed-db.js
new file mode 100644
index 0000000000..a3ba37d612
--- /dev/null
+++ b/devtools/server/actors/resources/storage-indexed-db.js
@@ -0,0 +1,19 @@
+/* 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: { INDEXED_DB },
+} = require("resource://devtools/server/actors/resources/index.js");
+
+const ParentProcessStorage = require("resource://devtools/server/actors/resources/utils/parent-process-storage.js");
+
+class IndexedDBWatcher extends ParentProcessStorage {
+ constructor() {
+ super("indexedDB", INDEXED_DB);
+ }
+}
+
+module.exports = IndexedDBWatcher;
diff --git a/devtools/server/actors/resources/storage-local-storage.js b/devtools/server/actors/resources/storage-local-storage.js
new file mode 100644
index 0000000000..26df65943d
--- /dev/null
+++ b/devtools/server/actors/resources/storage-local-storage.js
@@ -0,0 +1,19 @@
+/* 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: { LOCAL_STORAGE },
+} = require("resource://devtools/server/actors/resources/index.js");
+
+const ContentProcessStorage = require("resource://devtools/server/actors/resources/utils/content-process-storage.js");
+
+class LocalStorageWatcher extends ContentProcessStorage {
+ constructor() {
+ super("localStorage", LOCAL_STORAGE);
+ }
+}
+
+module.exports = LocalStorageWatcher;
diff --git a/devtools/server/actors/resources/storage-session-storage.js b/devtools/server/actors/resources/storage-session-storage.js
new file mode 100644
index 0000000000..a8930eb6b9
--- /dev/null
+++ b/devtools/server/actors/resources/storage-session-storage.js
@@ -0,0 +1,19 @@
+/* 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: { SESSION_STORAGE },
+} = require("resource://devtools/server/actors/resources/index.js");
+
+const ContentProcessStorage = require("resource://devtools/server/actors/resources/utils/content-process-storage.js");
+
+class SessionStorageWatcher extends ContentProcessStorage {
+ constructor() {
+ super("sessionStorage", SESSION_STORAGE);
+ }
+}
+
+module.exports = SessionStorageWatcher;
diff --git a/devtools/server/actors/resources/stylesheets.js b/devtools/server/actors/resources/stylesheets.js
new file mode 100644
index 0000000000..9c5e32cd3d
--- /dev/null
+++ b/devtools/server/actors/resources/stylesheets.js
@@ -0,0 +1,138 @@
+/* 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: { STYLESHEET },
+} = require("resource://devtools/server/actors/resources/index.js");
+
+loader.lazyRequireGetter(
+ this,
+ "CssLogic",
+ "resource://devtools/shared/inspector/css-logic.js"
+);
+
+class StyleSheetWatcher {
+ constructor() {
+ this._onApplicableStylesheetAdded = this._onApplicableStylesheetAdded.bind(
+ this
+ );
+ this._onStylesheetUpdated = this._onStylesheetUpdated.bind(this);
+ }
+
+ /**
+ * Start watching for all stylesheets related to a given Target Actor.
+ *
+ * @param TargetActor targetActor
+ * The target actor from which we should observe css changes.
+ * @param Object options
+ * Dictionary object with following attributes:
+ * - onAvailable: mandatory function
+ * This will be called for each resource.
+ */
+ async watch(targetActor, { onAvailable, onUpdated }) {
+ this._targetActor = targetActor;
+ this._onAvailable = onAvailable;
+ this._onUpdated = onUpdated;
+
+ this._styleSheetsManager = targetActor.getStyleSheetManager();
+
+ // Add event listener for new additions and updates
+ this._styleSheetsManager.on(
+ "applicable-stylesheet-added",
+ this._onApplicableStylesheetAdded
+ );
+ this._styleSheetsManager.on(
+ "stylesheet-updated",
+ this._onStylesheetUpdated
+ );
+
+ // startWatching will emit applicable-stylesheet-added for already existing stylesheet
+ await this._styleSheetsManager.startWatching();
+ }
+
+ _onApplicableStylesheetAdded(styleSheetData) {
+ return this._notifyResourcesAvailable([styleSheetData]);
+ }
+
+ _onStylesheetUpdated({ resourceId, updateKind, updates = {} }) {
+ this._notifyResourceUpdated(resourceId, updateKind, updates);
+ }
+
+ async _toResource(
+ styleSheet,
+ { isCreatedByDevTools = false, fileName = null, resourceId } = {}
+ ) {
+ const resource = {
+ resourceId,
+ resourceType: STYLESHEET,
+ disabled: styleSheet.disabled,
+ constructed: styleSheet.constructed,
+ fileName,
+ href: styleSheet.href,
+ isNew: isCreatedByDevTools,
+ atRules: await this._styleSheetsManager.getAtRules(styleSheet),
+ nodeHref: this._styleSheetsManager._getNodeHref(styleSheet),
+ ruleCount: styleSheet.cssRules.length,
+ sourceMapBaseURL: this._styleSheetsManager._getSourcemapBaseURL(
+ styleSheet
+ ),
+ sourceMapURL: styleSheet.sourceMapURL,
+ styleSheetIndex: this._styleSheetsManager._getStyleSheetIndex(styleSheet),
+ system: CssLogic.isAgentStylesheet(styleSheet),
+ title: styleSheet.title,
+ };
+
+ return resource;
+ }
+
+ async _notifyResourcesAvailable(styleSheets) {
+ const resources = await Promise.all(
+ styleSheets.map(async ({ resourceId, styleSheet, creationData }) => {
+ const resource = await this._toResource(styleSheet, {
+ resourceId,
+ isCreatedByDevTools: creationData?.isCreatedByDevTools,
+ fileName: creationData?.fileName,
+ });
+
+ return resource;
+ })
+ );
+
+ await this._onAvailable(resources);
+ }
+
+ _notifyResourceUpdated(
+ resourceId,
+ updateType,
+ { resourceUpdates, nestedResourceUpdates, event }
+ ) {
+ this._onUpdated([
+ {
+ browsingContextID: this._targetActor.browsingContextID,
+ innerWindowId: this._targetActor.innerWindowId,
+ resourceType: STYLESHEET,
+ resourceId,
+ updateType,
+ resourceUpdates,
+ nestedResourceUpdates,
+ event,
+ },
+ ]);
+ }
+
+ destroy() {
+ this._styleSheetsManager.off(
+ "applicable-stylesheet-added",
+ this._onApplicableStylesheetAdded
+ );
+ this._styleSheetsManager.off(
+ "stylesheet-updated",
+ this._onStylesheetUpdated
+ );
+ }
+}
+
+module.exports = StyleSheetWatcher;
diff --git a/devtools/server/actors/resources/thread-states.js b/devtools/server/actors/resources/thread-states.js
new file mode 100644
index 0000000000..9ac79088d2
--- /dev/null
+++ b/devtools/server/actors/resources/thread-states.js
@@ -0,0 +1,136 @@
+/* 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 {
+ 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 }) {
+ 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() {
+ 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(packet) {
+ // 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;
diff --git a/devtools/server/actors/resources/utils/content-process-storage.js b/devtools/server/actors/resources/utils/content-process-storage.js
new file mode 100644
index 0000000000..d4884a99f3
--- /dev/null
+++ b/devtools/server/actors/resources/utils/content-process-storage.js
@@ -0,0 +1,458 @@
+/* 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 {
+ storageTypePool,
+} = require("resource://devtools/server/actors/storage.js");
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ getAddonIdForWindowGlobal:
+ "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs",
+});
+
+// ms of delay to throttle updates
+const BATCH_DELAY = 200;
+
+// Filters "stores-update" response to only include events for
+// the storage type we desire
+function getFilteredStorageEvents(updates, storageType) {
+ const filteredUpdate = Object.create(null);
+
+ // updateType will be "added", "changed", or "deleted"
+ for (const updateType in updates) {
+ if (updates[updateType][storageType]) {
+ if (!filteredUpdate[updateType]) {
+ filteredUpdate[updateType] = {};
+ }
+ filteredUpdate[updateType][storageType] =
+ updates[updateType][storageType];
+ }
+ }
+
+ return Object.keys(filteredUpdate).length ? filteredUpdate : null;
+}
+
+class ContentProcessStorage {
+ constructor(storageKey, storageType) {
+ this.storageKey = storageKey;
+ this.storageType = storageType;
+
+ this.onStoresUpdate = this.onStoresUpdate.bind(this);
+ this.onStoresCleared = this.onStoresCleared.bind(this);
+ }
+
+ async watch(targetActor, { onAvailable }) {
+ const ActorConstructor = storageTypePool.get(this.storageKey);
+ const storageActor = new StorageActorMock(targetActor);
+ this.storageActor = storageActor;
+ this.actor = new ActorConstructor(storageActor);
+
+ // Some storage types require to prelist their stores
+ if (typeof this.actor.preListStores === "function") {
+ await this.actor.preListStores();
+ }
+
+ // We have to manage the actor manually, because ResourceCommand doesn't
+ // use the protocol.js specification.
+ // resource-available-form is typed as "json"
+ // So that we have to manually handle stuff that would normally be
+ // automagically done by procotol.js
+ // 1) Manage the actor in order to have an actorID on it
+ targetActor.manage(this.actor);
+ // 2) Convert to JSON "form"
+ const form = this.actor.form();
+
+ // NOTE: this is hoisted, so the `update` method above may use it.
+ const storage = form;
+
+ // All resources should have a resourceType, resourceId and resourceKey
+ // attributes, so available/updated/destroyed callbacks work properly.
+ storage.resourceType = this.storageType;
+ storage.resourceId = this.storageType;
+ storage.resourceKey = this.storageKey;
+
+ onAvailable([storage]);
+
+ // Maps global events from `storageActor` shared for all storage-types,
+ // down to storage-type's specific actor `storage`.
+ storageActor.on("stores-update", this.onStoresUpdate);
+
+ // When a store gets cleared
+ storageActor.on("stores-cleared", this.onStoresCleared);
+ }
+
+ onStoresUpdate(response) {
+ response = getFilteredStorageEvents(response, this.storageKey);
+ if (!response) {
+ return;
+ }
+ this.actor.emit("single-store-update", {
+ changed: response.changed,
+ added: response.added,
+ deleted: response.deleted,
+ });
+ }
+
+ onStoresCleared(response) {
+ const cleared = response[this.storageKey];
+
+ if (!cleared) {
+ return;
+ }
+
+ this.actor.emit("single-store-cleared", {
+ clearedHostsOrPaths: cleared,
+ });
+ }
+
+ destroy() {
+ this.actor?.destroy();
+ this.actor = null;
+ if (this.storageActor) {
+ this.storageActor.on("stores-update", this.onStoresUpdate);
+ this.storageActor.on("stores-cleared", this.onStoresCleared);
+ this.storageActor.destroy();
+ this.storageActor = null;
+ }
+ }
+}
+
+module.exports = ContentProcessStorage;
+
+// This class mocks what used to be implement in devtools/server/actors/storage.js: StorageActor
+// But without being a protocol.js actor, nor implement any RDP method/event.
+// An instance of this class is passed to each storage type actor and named `storageActor`.
+// Once we implement all storage type in watcher classes, we can get rid of the original
+// StorageActor in devtools/server/actors/storage.js
+class StorageActorMock extends EventEmitter {
+ constructor(targetActor) {
+ super();
+ // Storage classes fetch conn from storageActor
+ this.conn = targetActor.conn;
+ this.targetActor = targetActor;
+
+ this.childWindowPool = new Set();
+
+ // Fetch all the inner iframe windows in this tab.
+ this.fetchChildWindows(this.targetActor.docShell);
+
+ // Notifications that help us keep track of newly added windows and windows
+ // that got removed
+ Services.obs.addObserver(this, "content-document-global-created");
+ Services.obs.addObserver(this, "inner-window-destroyed");
+ this.onPageChange = this.onPageChange.bind(this);
+
+ const handler = targetActor.chromeEventHandler;
+ handler.addEventListener("pageshow", this.onPageChange, true);
+ handler.addEventListener("pagehide", this.onPageChange, true);
+
+ this.destroyed = false;
+ this.boundUpdate = {};
+ }
+
+ destroy() {
+ clearTimeout(this.batchTimer);
+ this.batchTimer = null;
+ // Remove observers
+ Services.obs.removeObserver(this, "content-document-global-created");
+ Services.obs.removeObserver(this, "inner-window-destroyed");
+ this.destroyed = true;
+ if (this.targetActor.browser) {
+ this.targetActor.browser.removeEventListener(
+ "pageshow",
+ this.onPageChange,
+ true
+ );
+ this.targetActor.browser.removeEventListener(
+ "pagehide",
+ this.onPageChange,
+ true
+ );
+ }
+ this.childWindowPool.clear();
+
+ this.childWindowPool = null;
+ this.targetActor = null;
+ this.boundUpdate = null;
+ }
+
+ get window() {
+ return this.targetActor.window;
+ }
+
+ get document() {
+ return this.targetActor.window.document;
+ }
+
+ get windows() {
+ return this.childWindowPool;
+ }
+
+ /**
+ * Given a docshell, recursively find out all the child windows from it.
+ *
+ * @param {nsIDocShell} item
+ * The docshell from which all inner windows need to be extracted.
+ */
+ fetchChildWindows(item) {
+ const docShell = item
+ .QueryInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIDocShellTreeItem);
+ if (!docShell.contentViewer) {
+ return null;
+ }
+ const window = docShell.contentViewer.DOMDocument.defaultView;
+ if (window.location.href == "about:blank") {
+ // Skip out about:blank windows as Gecko creates them multiple times while
+ // creating any global.
+ return null;
+ }
+ if (!this.isIncludedInTopLevelWindow(window)) {
+ return null;
+ }
+ this.childWindowPool.add(window);
+ for (let i = 0; i < docShell.childCount; i++) {
+ const child = docShell.getChildAt(i);
+ this.fetchChildWindows(child);
+ }
+ return null;
+ }
+
+ isIncludedInTargetExtension(subject) {
+ const addonId = lazy.getAddonIdForWindowGlobal(subject.windowGlobalChild);
+ return addonId && addonId === this.targetActor.addonId;
+ }
+
+ isIncludedInTopLevelWindow(window) {
+ return this.targetActor.windows.includes(window);
+ }
+
+ getWindowFromInnerWindowID(innerID) {
+ innerID = innerID.QueryInterface(Ci.nsISupportsPRUint64).data;
+ for (const win of this.childWindowPool.values()) {
+ const id = win.windowGlobalChild.innerWindowId;
+ if (id == innerID) {
+ return win;
+ }
+ }
+ return null;
+ }
+
+ getWindowFromHost(host) {
+ for (const win of this.childWindowPool.values()) {
+ const origin = win.document.nodePrincipal.originNoSuffix;
+ const url = win.document.URL;
+ if (origin === host || url === host) {
+ return win;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Event handler for any docshell update. This lets us figure out whenever
+ * any new window is added, or an existing window is removed.
+ */
+ observe(subject, topic) {
+ if (
+ subject.location &&
+ (!subject.location.href || subject.location.href == "about:blank")
+ ) {
+ return null;
+ }
+
+ // We don't want to try to find a top level window for an extension page, as
+ // in many cases (e.g. background page), it is not loaded in a tab, and
+ // 'isIncludedInTopLevelWindow' throws an error
+ if (
+ topic == "content-document-global-created" &&
+ (this.isIncludedInTargetExtension(subject) ||
+ this.isIncludedInTopLevelWindow(subject))
+ ) {
+ this.childWindowPool.add(subject);
+ this.emit("window-ready", subject);
+ } else if (topic == "inner-window-destroyed") {
+ const window = this.getWindowFromInnerWindowID(subject);
+ if (window) {
+ this.childWindowPool.delete(window);
+ this.emit("window-destroyed", window);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Called on "pageshow" or "pagehide" event on the chromeEventHandler of
+ * current tab.
+ *
+ * @param {event} The event object passed to the handler. We are using these
+ * three properties from the event:
+ * - target {document} The document corresponding to the event.
+ * - type {string} Name of the event - "pageshow" or "pagehide".
+ * - persisted {boolean} true if there was no
+ * "content-document-global-created" notification along
+ * this event.
+ */
+ onPageChange({ target, type, persisted }) {
+ if (this.destroyed) {
+ return;
+ }
+
+ const window = target.defaultView;
+
+ if (type == "pagehide" && this.childWindowPool.delete(window)) {
+ this.emit("window-destroyed", window);
+ } else if (
+ type == "pageshow" &&
+ persisted &&
+ window.location.href &&
+ window.location.href != "about:blank" &&
+ this.isIncludedInTopLevelWindow(window)
+ ) {
+ this.childWindowPool.add(window);
+ this.emit("window-ready", window);
+ }
+ }
+
+ /**
+ * This method is called by the registered storage types so as to tell the
+ * Storage Actor that there are some changes in the stores. Storage Actor then
+ * notifies the client front about these changes at regular (BATCH_DELAY)
+ * interval.
+ *
+ * @param {string} action
+ * The type of change. One of "added", "changed" or "deleted"
+ * @param {string} storeType
+ * The storage actor in which this change has occurred.
+ * @param {object} data
+ * The update object. This object is of the following format:
+ * - {
+ * <host1>: [<store_names1>, <store_name2>...],
+ * <host2>: [<store_names34>...],
+ * }
+ * Where host1, host2 are the host in which this change happened and
+ * [<store_namesX] is an array of the names of the changed store objects.
+ * Pass an empty array if the host itself was affected: either completely
+ * removed or cleared.
+ */
+ // eslint-disable-next-line complexity
+ update(action, storeType, data) {
+ if (action == "cleared") {
+ this.emit("stores-cleared", { [storeType]: data });
+ return null;
+ }
+
+ if (this.batchTimer) {
+ clearTimeout(this.batchTimer);
+ }
+ if (!this.boundUpdate[action]) {
+ this.boundUpdate[action] = {};
+ }
+ if (!this.boundUpdate[action][storeType]) {
+ this.boundUpdate[action][storeType] = {};
+ }
+ for (const host in data) {
+ if (!this.boundUpdate[action][storeType][host]) {
+ this.boundUpdate[action][storeType][host] = [];
+ }
+ for (const name of data[host]) {
+ if (!this.boundUpdate[action][storeType][host].includes(name)) {
+ this.boundUpdate[action][storeType][host].push(name);
+ }
+ }
+ }
+ if (action == "added") {
+ // If the same store name was previously deleted or changed, but now is
+ // added somehow, dont send the deleted or changed update.
+ this.removeNamesFromUpdateList("deleted", storeType, data);
+ this.removeNamesFromUpdateList("changed", storeType, data);
+ } else if (
+ action == "changed" &&
+ this.boundUpdate.added &&
+ this.boundUpdate.added[storeType]
+ ) {
+ // If something got added and changed at the same time, then remove those
+ // items from changed instead.
+ this.removeNamesFromUpdateList(
+ "changed",
+ storeType,
+ this.boundUpdate.added[storeType]
+ );
+ } else if (action == "deleted") {
+ // If any item got delete, or a host got delete, no point in sending
+ // added or changed update
+ this.removeNamesFromUpdateList("added", storeType, data);
+ this.removeNamesFromUpdateList("changed", storeType, data);
+
+ for (const host in data) {
+ if (
+ !data[host].length &&
+ this.boundUpdate.added &&
+ this.boundUpdate.added[storeType] &&
+ this.boundUpdate.added[storeType][host]
+ ) {
+ delete this.boundUpdate.added[storeType][host];
+ }
+ if (
+ !data[host].length &&
+ this.boundUpdate.changed &&
+ this.boundUpdate.changed[storeType] &&
+ this.boundUpdate.changed[storeType][host]
+ ) {
+ delete this.boundUpdate.changed[storeType][host];
+ }
+ }
+ }
+
+ this.batchTimer = setTimeout(() => {
+ clearTimeout(this.batchTimer);
+ this.emit("stores-update", this.boundUpdate);
+ this.boundUpdate = {};
+ }, BATCH_DELAY);
+
+ return null;
+ }
+
+ /**
+ * This method removes data from the this.boundUpdate object in the same
+ * manner like this.update() adds data to it.
+ *
+ * @param {string} action
+ * The type of change. One of "added", "changed" or "deleted"
+ * @param {string} storeType
+ * The storage actor for which you want to remove the updates data.
+ * @param {object} data
+ * The update object. This object is of the following format:
+ * - {
+ * <host1>: [<store_names1>, <store_name2>...],
+ * <host2>: [<store_names34>...],
+ * }
+ * Where host1, host2 are the hosts which you want to remove and
+ * [<store_namesX] is an array of the names of the store objects.
+ */
+ removeNamesFromUpdateList(action, storeType, data) {
+ for (const host in data) {
+ if (
+ this.boundUpdate[action] &&
+ this.boundUpdate[action][storeType] &&
+ this.boundUpdate[action][storeType][host]
+ ) {
+ for (const name in data[host]) {
+ const index = this.boundUpdate[action][storeType][host].indexOf(name);
+ if (index > -1) {
+ this.boundUpdate[action][storeType][host].splice(index, 1);
+ }
+ }
+ if (!this.boundUpdate[action][storeType][host].length) {
+ delete this.boundUpdate[action][storeType][host];
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/devtools/server/actors/resources/utils/moz.build b/devtools/server/actors/resources/utils/moz.build
new file mode 100644
index 0000000000..0e6f9d1baa
--- /dev/null
+++ b/devtools/server/actors/resources/utils/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "content-process-storage.js",
+ "nsi-console-listener-watcher.js",
+ "parent-process-storage.js",
+)
+
+with Files("nsi-console-listener-watcher.js"):
+ BUG_COMPONENT = ("DevTools", "Console")
diff --git a/devtools/server/actors/resources/utils/nsi-console-listener-watcher.js b/devtools/server/actors/resources/utils/nsi-console-listener-watcher.js
new file mode 100644
index 0000000000..9d88721900
--- /dev/null
+++ b/devtools/server/actors/resources/utils/nsi-console-listener-watcher.js
@@ -0,0 +1,192 @@
+/* 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 {
+ createStringGrip,
+} = require("resource://devtools/server/actors/object/utils.js");
+
+const {
+ getActorIdForInternalSourceId,
+} = require("resource://devtools/server/actors/utils/dbg-source.js");
+
+class nsIConsoleListenerWatcher {
+ /**
+ * Start watching for all messages related to a given Target Actor.
+ * This will notify about existing messages, as well as those created in the future.
+ *
+ * @param TargetActor targetActor
+ * The target actor from which we should observe messages
+ * @param Object options
+ * Dictionary object with following attributes:
+ * - onAvailable: mandatory function
+ * This will be called for each resource.
+ */
+ async watch(targetActor, { onAvailable }) {
+ if (!this.shouldHandleTarget(targetActor)) {
+ return;
+ }
+
+ let latestRetrievedCachedMessageTimestamp = -1;
+
+ // Create the consoleListener.
+ const listener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIConsoleListener"]),
+ observe: message => {
+ if (
+ message.microSecondTimeStamp <= latestRetrievedCachedMessageTimestamp
+ ) {
+ return;
+ }
+
+ if (!this.shouldHandleMessage(targetActor, message)) {
+ return;
+ }
+
+ onAvailable([this.buildResource(targetActor, message)]);
+ },
+ };
+
+ // Retrieve the cached messages and get the last cached message timestamp before
+ // registering the listener, so we can ignore messages we'd be notified about but that
+ // were already retrieved in the cache.
+ const cachedMessages = Services.console.getMessageArray() || [];
+ if (cachedMessages.length) {
+ latestRetrievedCachedMessageTimestamp = cachedMessages.at(-1)
+ .microSecondTimeStamp;
+ }
+
+ Services.console.registerListener(listener);
+ this.listener = listener;
+
+ // Remove unwanted cache messages and send an array of resources.
+ const messages = [];
+ for (const message of cachedMessages) {
+ if (!this.shouldHandleMessage(targetActor, message, true)) {
+ continue;
+ }
+
+ messages.push(this.buildResource(targetActor, message));
+ }
+ onAvailable(messages);
+ }
+
+ /**
+ * Return false if the watcher shouldn't be created.
+ *
+ * @param {TargetActor} targetActor
+ * @return {Boolean}
+ */
+ shouldHandleTarget(targetActor) {
+ return true;
+ }
+
+ /**
+ * Return true if you want the passed message to be handled by the watcher. This should
+ * be implemented on the child class.
+ *
+ * @param {TargetActor} targetActor
+ * @param {nsIScriptError|nsIConsoleMessage} message
+ * @return {Boolean}
+ */
+ shouldHandleMessage(targetActor, message) {
+ throw new Error(
+ "'shouldHandleMessage' should be implemented in the class that extends nsIConsoleListenerWatcher"
+ );
+ }
+
+ /**
+ * Prepare the resource to be sent to the client. This should be implemented on the
+ * child class.
+ *
+ * @param targetActor
+ * @param nsIScriptError|nsIConsoleMessage message
+ * @return object
+ * The object you can send to the remote client.
+ */
+ buildResource(targetActor, message) {
+ throw new Error(
+ "'buildResource' should be implemented in the class that extends nsIConsoleListenerWatcher"
+ );
+ }
+
+ /**
+ * Prepare a SavedFrame stack to be sent to the client.
+ *
+ * @param {TargetActor} targetActor
+ * @param {SavedFrame} errorStack
+ * Stack for an error we need to send to the client.
+ * @return object
+ * The object you can send to the remote client.
+ */
+ prepareStackForRemote(targetActor, errorStack) {
+ // Convert stack objects to the JSON attributes expected by client code
+ // Bug 1348885: If the global from which this error came from has been
+ // nuked, stack is going to be a dead wrapper.
+ if (!errorStack || (Cu && Cu.isDeadWrapper(errorStack))) {
+ return null;
+ }
+ const stack = [];
+ let s = errorStack;
+ while (s) {
+ stack.push({
+ filename: s.source,
+ sourceId: getActorIdForInternalSourceId(targetActor, s.sourceId),
+ lineNumber: s.line,
+ columnNumber: s.column,
+ functionName: s.functionDisplayName,
+ asyncCause: s.asyncCause ? s.asyncCause : undefined,
+ });
+ s = s.parent || s.asyncParent;
+ }
+ return stack;
+ }
+
+ /**
+ * Prepare error notes to be sent to the client.
+ *
+ * @param {TargetActor} targetActor
+ * @param {nsIArray<nsIScriptErrorNote>} errorNotes
+ * @return object
+ * The object you can send to the remote client.
+ */
+ prepareNotesForRemote(targetActor, errorNotes) {
+ if (!errorNotes?.length) {
+ return null;
+ }
+
+ const notes = [];
+ for (let i = 0, len = errorNotes.length; i < len; i++) {
+ const note = errorNotes.queryElementAt(i, Ci.nsIScriptErrorNote);
+ notes.push({
+ messageBody: createStringGrip(targetActor, note.errorMessage),
+ frame: {
+ source: note.sourceName,
+ sourceId: getActorIdForInternalSourceId(targetActor, note.sourceId),
+ line: note.lineNumber,
+ column: note.columnNumber,
+ },
+ });
+ }
+ return notes;
+ }
+
+ isProcessTarget(targetActor) {
+ const { typeName } = targetActor;
+ return (
+ typeName === "parentProcessTarget" || typeName === "contentProcessTarget"
+ );
+ }
+
+ /**
+ * Stop watching for messages.
+ */
+ destroy() {
+ if (this.listener) {
+ Services.console.unregisterListener(this.listener);
+ }
+ }
+}
+module.exports = nsIConsoleListenerWatcher;
diff --git a/devtools/server/actors/resources/utils/parent-process-storage.js b/devtools/server/actors/resources/utils/parent-process-storage.js
new file mode 100644
index 0000000000..ac897b565a
--- /dev/null
+++ b/devtools/server/actors/resources/utils/parent-process-storage.js
@@ -0,0 +1,584 @@
+/* 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 {
+ storageTypePool,
+} = require("resource://devtools/server/actors/storage.js");
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+const {
+ getAllBrowsingContextsForContext,
+ isWindowGlobalPartOfContext,
+} = ChromeUtils.importESModule(
+ "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs"
+);
+
+// ms of delay to throttle updates
+const BATCH_DELAY = 200;
+
+// Filters "stores-update" response to only include events for
+// the storage type we desire
+function getFilteredStorageEvents(updates, storageType) {
+ const filteredUpdate = Object.create(null);
+
+ // updateType will be "added", "changed", or "deleted"
+ for (const updateType in updates) {
+ if (updates[updateType][storageType]) {
+ if (!filteredUpdate[updateType]) {
+ filteredUpdate[updateType] = {};
+ }
+ filteredUpdate[updateType][storageType] =
+ updates[updateType][storageType];
+ }
+ }
+
+ return Object.keys(filteredUpdate).length ? filteredUpdate : null;
+}
+
+class ParentProcessStorage {
+ constructor(storageKey, storageType) {
+ this.storageKey = storageKey;
+ this.storageType = storageType;
+
+ this.onStoresUpdate = this.onStoresUpdate.bind(this);
+ this.onStoresCleared = this.onStoresCleared.bind(this);
+
+ this.observe = this.observe.bind(this);
+ // Notifications that help us keep track of newly added windows and windows
+ // that got removed
+ Services.obs.addObserver(this, "window-global-created");
+ Services.obs.addObserver(this, "window-global-destroyed");
+
+ // bfcacheInParent is only enabled when fission is enabled
+ // and when Session History In Parent is enabled. (all three modes should now enabled all together)
+ loader.lazyGetter(
+ this,
+ "isBfcacheInParentEnabled",
+ () =>
+ Services.appinfo.sessionHistoryInParent &&
+ Services.prefs.getBoolPref("fission.bfcacheInParent", false)
+ );
+ }
+
+ async watch(watcherActor, { onAvailable }) {
+ this.watcherActor = watcherActor;
+ this.onAvailable = onAvailable;
+
+ // When doing a bfcache navigation with Fission disabled or with Fission + bfCacheInParent enabled,
+ // we're not getting a the window-global-created events.
+ // In such case, the watcher emits specific events that we can use instead.
+ this._offPageShow = watcherActor.on(
+ "bf-cache-navigation-pageshow",
+ ({ windowGlobal }) => this._onNewWindowGlobal(windowGlobal, true)
+ );
+
+ if (watcherActor.sessionContext.type == "browser-element") {
+ const {
+ browsingContext,
+ innerWindowID: innerWindowId,
+ } = watcherActor.browserElement;
+ await this._spawnActor(browsingContext.id, innerWindowId);
+ } else if (watcherActor.sessionContext.type == "webextension") {
+ const {
+ addonBrowsingContextID,
+ addonInnerWindowId,
+ } = watcherActor.sessionContext;
+ await this._spawnActor(addonBrowsingContextID, addonInnerWindowId);
+ } else {
+ throw new Error(
+ "Unsupported session context type=" + watcherActor.sessionContext.type
+ );
+ }
+ }
+
+ onStoresUpdate(response) {
+ response = getFilteredStorageEvents(response, this.storageKey);
+ if (!response) {
+ return;
+ }
+ this.actor.emit("single-store-update", {
+ changed: response.changed,
+ added: response.added,
+ deleted: response.deleted,
+ });
+ }
+
+ onStoresCleared(response) {
+ const cleared = response[this.storageKey];
+
+ if (!cleared) {
+ return;
+ }
+
+ this.actor.emit("single-store-cleared", {
+ clearedHostsOrPaths: cleared,
+ });
+ }
+
+ destroy() {
+ // Remove observers
+ Services.obs.removeObserver(this, "window-global-created");
+ Services.obs.removeObserver(this, "window-global-destroyed");
+ this._offPageShow();
+ this._cleanActor();
+ }
+
+ async _spawnActor(browsingContextID, innerWindowId) {
+ const ActorConstructor = storageTypePool.get(this.storageKey);
+
+ const storageActor = new StorageActorMock(this.watcherActor);
+ this.storageActor = storageActor;
+ this.actor = new ActorConstructor(storageActor);
+
+ // Some storage types require to prelist their stores
+ if (typeof this.actor.preListStores === "function") {
+ try {
+ await this.actor.preListStores();
+ } catch (e) {
+ // It can happen that the actor gets destroyed while preListStores is being
+ // executed.
+ if (this.actor) {
+ throw e;
+ }
+ }
+ }
+
+ // If the actor was destroyed, we don't need to go further.
+ if (!this.actor) {
+ return;
+ }
+
+ // We have to manage the actor manually, because ResourceCommand doesn't
+ // use the protocol.js specification.
+ // resource-available-form is typed as "json"
+ // So that we have to manually handle stuff that would normally be
+ // automagically done by procotol.js
+ // 1) Manage the actor in order to have an actorID on it
+ this.watcherActor.manage(this.actor);
+ // 2) Convert to JSON "form"
+ const storage = this.actor.form();
+
+ // All resources should have a resourceType, resourceId and resourceKey
+ // attributes, so available/updated/destroyed callbacks work properly.
+ storage.resourceType = this.storageType;
+ storage.resourceId = `${this.storageType}-${innerWindowId}`;
+ storage.resourceKey = this.storageKey;
+ // NOTE: the resource command needs this attribute
+ storage.browsingContextID = browsingContextID;
+
+ this.onAvailable([storage]);
+
+ // Maps global events from `storageActor` shared for all storage-types,
+ // down to storage-type's specific actor `storage`.
+ storageActor.on("stores-update", this.onStoresUpdate);
+
+ // When a store gets cleared
+ storageActor.on("stores-cleared", this.onStoresCleared);
+ }
+
+ _cleanActor() {
+ this.actor?.destroy();
+ this.actor = null;
+ if (this.storageActor) {
+ this.storageActor.off("stores-update", this.onStoresUpdate);
+ this.storageActor.off("stores-cleared", this.onStoresCleared);
+ this.storageActor.destroy();
+ this.storageActor = null;
+ }
+ }
+
+ /**
+ * Event handler for any docshell update. This lets us figure out whenever
+ * any new window is added, or an existing window is removed.
+ */
+ observe(subject, topic) {
+ if (topic === "window-global-created") {
+ this._onNewWindowGlobal(subject);
+ }
+ }
+
+ /**
+ * Handle WindowGlobal received via:
+ * - <window-global-created> (to cover regular navigations, with brand new documents)
+ * - <bf-cache-navigation-pageshow> (to cover history navications)
+ *
+ * @param {WindowGlobal} windowGlobal
+ * @param {Boolean} isBfCacheNavigation
+ */
+ async _onNewWindowGlobal(windowGlobal, isBfCacheNavigation) {
+ // Only process WindowGlobals which are related to the debugged scope.
+ if (
+ !isWindowGlobalPartOfContext(
+ windowGlobal,
+ this.watcherActor.sessionContext,
+ { acceptNoWindowGlobal: true, acceptSameProcessIframes: true }
+ )
+ ) {
+ return;
+ }
+
+ // Ignore about:blank
+ if (windowGlobal.documentURI.displaySpec === "about:blank") {
+ return;
+ }
+
+ // Only process top BrowsingContext (ignore same-process iframe ones)
+ const isTopContext =
+ windowGlobal.browsingContext.top == windowGlobal.browsingContext;
+ if (!isTopContext) {
+ return;
+ }
+
+ // We only want to spawn a new StorageActor if a new target is being created, i.e.
+ // - target switching is enabled and we're notified about a new top-level window global,
+ // via window-global-created
+ // - target switching is enabled OR bfCacheInParent is enabled, and a bfcache navigation
+ // is performed (See handling of "pageshow" event in DevToolsFrameChild)
+ const isNewTargetBeingCreated =
+ this.watcherActor.sessionContext.isServerTargetSwitchingEnabled ||
+ (isBfCacheNavigation && this.isBfcacheInParentEnabled);
+
+ if (!isNewTargetBeingCreated) {
+ return;
+ }
+
+ // When server side target switching is enabled, we replace the StorageActor
+ // with a new one.
+ // On the frontend, the navigation will destroy the previous target, which
+ // will destroy the previous storage front, so we must notify about a new one.
+
+ // When we are target switching we keep the storage watcher, so we need
+ // to send a new resource to the client.
+ // However, we must ensure that we do this when the new target is
+ // already available, so we check innerWindowId to do it.
+ await new Promise(resolve => {
+ const listener = targetActorForm => {
+ if (targetActorForm.innerWindowId != windowGlobal.innerWindowId) {
+ return;
+ }
+ this.watcherActor.off("target-available-form", listener);
+ resolve();
+ };
+ this.watcherActor.on("target-available-form", listener);
+ });
+
+ this._cleanActor();
+ this._spawnActor(
+ windowGlobal.browsingContext.id,
+ windowGlobal.innerWindowId
+ );
+ }
+}
+
+module.exports = ParentProcessStorage;
+
+class StorageActorMock extends EventEmitter {
+ constructor(watcherActor) {
+ super();
+
+ this.conn = watcherActor.conn;
+ this.watcherActor = watcherActor;
+
+ this.boundUpdate = {};
+
+ // Notifications that help us keep track of newly added windows and windows
+ // that got removed
+ this.observe = this.observe.bind(this);
+ Services.obs.addObserver(this, "window-global-created");
+ Services.obs.addObserver(this, "window-global-destroyed");
+
+ // When doing a bfcache navigation with Fission disabled or with Fission + bfCacheInParent enabled,
+ // we're not getting a the window-global-created/window-global-destroyed events.
+ // In such case, the watcher emits specific events that we can use as equivalent to
+ // window-global-created/window-global-destroyed.
+ // We only need to react to those events here if target switching is not enabled; when
+ // it is enabled, ParentProcessStorage will spawn a whole new actor which will allow
+ // the client to get the information it needs.
+ if (!this.watcherActor.sessionContext.isServerTargetSwitchingEnabled) {
+ this._offPageShow = watcherActor.on(
+ "bf-cache-navigation-pageshow",
+ ({ windowGlobal }) => {
+ // if a new target is created in the content process as a result of the bfcache
+ // navigation, we don't need to emit window-ready as a new StorageActorMock will
+ // be created by ParentProcessStorage.
+ // When server targets are disabled, this only happens when bfcache in parent is enabled.
+ if (this.isBfcacheInParentEnabled) {
+ return;
+ }
+ const windowMock = { location: windowGlobal.documentURI };
+ this.emit("window-ready", windowMock);
+ }
+ );
+
+ this._offPageHide = watcherActor.on(
+ "bf-cache-navigation-pagehide",
+ ({ windowGlobal }) => {
+ const windowMock = { location: windowGlobal.documentURI };
+ // The listener of this events usually check that there are no other windows
+ // with the same host before notifying the client that it can remove it from
+ // the UI. The windows are retrieved from the `windows` getter, and in this case
+ // we still have a reference to the window we're navigating away from.
+ // We pass a `dontCheckHost` parameter alongside the window-destroyed event to
+ // always notify the client.
+ this.emit("window-destroyed", windowMock, { dontCheckHost: true });
+ }
+ );
+ }
+ }
+
+ destroy() {
+ // clear update throttle timeout
+ clearTimeout(this.batchTimer);
+ this.batchTimer = null;
+ // Remove observers
+ Services.obs.removeObserver(this, "window-global-created");
+ Services.obs.removeObserver(this, "window-global-destroyed");
+ if (this._offPageShow) {
+ this._offPageShow();
+ }
+ if (this._offPageHide) {
+ this._offPageHide();
+ }
+ }
+
+ get windows() {
+ return (
+ getAllBrowsingContextsForContext(this.watcherActor.sessionContext, {
+ acceptSameProcessIframes: true,
+ })
+ .map(x => {
+ const uri = x.currentWindowGlobal.documentURI;
+ return { location: uri };
+ })
+ // NOTE: we are removing about:blank because we might get them for iframes
+ // whose src attribute has not been set yet.
+ .filter(x => x.location.displaySpec !== "about:blank")
+ );
+ }
+
+ // NOTE: this uri argument is not a real window.Location, but the
+ // `currentWindowGlobal.documentURI` object passed from `windows` getter.
+ getHostName(uri) {
+ switch (uri.scheme) {
+ case "about":
+ case "file":
+ case "javascript":
+ case "resource":
+ return uri.displaySpec;
+ case "moz-extension":
+ case "http":
+ case "https":
+ return uri.prePath;
+ default:
+ // chrome: and data: do not support storage
+ return null;
+ }
+ }
+
+ getWindowFromHost(host) {
+ const hostBrowsingContext = getAllBrowsingContextsForContext(
+ this.watcherActor.sessionContext,
+ { acceptSameProcessIframes: true }
+ ).find(x => {
+ const hostName = this.getHostName(x.currentWindowGlobal.documentURI);
+ return hostName === host;
+ });
+ // In case of WebExtension or BrowserToolbox, we may pass privileged hosts
+ // which don't relate to any particular window.
+ // Like "indexeddb+++fx-devtools" or "chrome".
+ // (callsites of this method are used to handle null returned values)
+ if (!hostBrowsingContext) {
+ return null;
+ }
+
+ const principal =
+ hostBrowsingContext.currentWindowGlobal.documentStoragePrincipal;
+
+ return { document: { effectiveStoragePrincipal: principal } };
+ }
+
+ get parentActor() {
+ return { isRootActor: this.watcherActor.sessionContext.type == "all" };
+ }
+
+ /**
+ * Event handler for any docshell update. This lets us figure out whenever
+ * any new window is added, or an existing window is removed.
+ */
+ async observe(windowGlobal, topic) {
+ // Only process WindowGlobals which are related to the debugged scope.
+ if (
+ !isWindowGlobalPartOfContext(
+ windowGlobal,
+ this.watcherActor.sessionContext,
+ { acceptNoWindowGlobal: true, acceptSameProcessIframes: true }
+ )
+ ) {
+ return;
+ }
+
+ // Ignore about:blank
+ if (windowGlobal.documentURI.displaySpec === "about:blank") {
+ return;
+ }
+
+ // Only notify about remote iframe windows when JSWindowActor based targets are enabled
+ // We will create a new StorageActor for the top level tab documents when server side target
+ // switching is enabled
+ const isTopContext =
+ windowGlobal.browsingContext.top == windowGlobal.browsingContext;
+ if (
+ isTopContext &&
+ this.watcherActor.sessionContext.isServerTargetSwitchingEnabled
+ ) {
+ return;
+ }
+
+ // emit window-wready and window-destroyed events when needed
+ const windowMock = { location: windowGlobal.documentURI };
+ if (topic === "window-global-created") {
+ this.emit("window-ready", windowMock);
+ } else if (topic === "window-global-destroyed") {
+ this.emit("window-destroyed", windowMock);
+ }
+ }
+
+ /**
+ * This method is called by the registered storage types so as to tell the
+ * Storage Actor that there are some changes in the stores. Storage Actor then
+ * notifies the client front about these changes at regular (BATCH_DELAY)
+ * interval.
+ *
+ * @param {string} action
+ * The type of change. One of "added", "changed" or "deleted"
+ * @param {string} storeType
+ * The storage actor in which this change has occurred.
+ * @param {object} data
+ * The update object. This object is of the following format:
+ * - {
+ * <host1>: [<store_names1>, <store_name2>...],
+ * <host2>: [<store_names34>...],
+ * }
+ * Where host1, host2 are the host in which this change happened and
+ * [<store_namesX] is an array of the names of the changed store objects.
+ * Pass an empty array if the host itself was affected: either completely
+ * removed or cleared.
+ */
+ // eslint-disable-next-line complexity
+ update(action, storeType, data) {
+ if (action == "cleared") {
+ this.emit("stores-cleared", { [storeType]: data });
+ return null;
+ }
+
+ if (this.batchTimer) {
+ clearTimeout(this.batchTimer);
+ }
+ if (!this.boundUpdate[action]) {
+ this.boundUpdate[action] = {};
+ }
+ if (!this.boundUpdate[action][storeType]) {
+ this.boundUpdate[action][storeType] = {};
+ }
+ for (const host in data) {
+ if (!this.boundUpdate[action][storeType][host]) {
+ this.boundUpdate[action][storeType][host] = [];
+ }
+ for (const name of data[host]) {
+ if (!this.boundUpdate[action][storeType][host].includes(name)) {
+ this.boundUpdate[action][storeType][host].push(name);
+ }
+ }
+ }
+ if (action == "added") {
+ // If the same store name was previously deleted or changed, but now is
+ // added somehow, dont send the deleted or changed update.
+ this.removeNamesFromUpdateList("deleted", storeType, data);
+ this.removeNamesFromUpdateList("changed", storeType, data);
+ } else if (
+ action == "changed" &&
+ this.boundUpdate.added &&
+ this.boundUpdate.added[storeType]
+ ) {
+ // If something got added and changed at the same time, then remove those
+ // items from changed instead.
+ this.removeNamesFromUpdateList(
+ "changed",
+ storeType,
+ this.boundUpdate.added[storeType]
+ );
+ } else if (action == "deleted") {
+ // If any item got delete, or a host got delete, no point in sending
+ // added or changed update
+ this.removeNamesFromUpdateList("added", storeType, data);
+ this.removeNamesFromUpdateList("changed", storeType, data);
+
+ for (const host in data) {
+ if (
+ !data[host].length &&
+ this.boundUpdate.added &&
+ this.boundUpdate.added[storeType] &&
+ this.boundUpdate.added[storeType][host]
+ ) {
+ delete this.boundUpdate.added[storeType][host];
+ }
+ if (
+ !data[host].length &&
+ this.boundUpdate.changed &&
+ this.boundUpdate.changed[storeType] &&
+ this.boundUpdate.changed[storeType][host]
+ ) {
+ delete this.boundUpdate.changed[storeType][host];
+ }
+ }
+ }
+
+ this.batchTimer = setTimeout(() => {
+ clearTimeout(this.batchTimer);
+ this.emit("stores-update", this.boundUpdate);
+ this.boundUpdate = {};
+ }, BATCH_DELAY);
+
+ return null;
+ }
+
+ /**
+ * This method removes data from the this.boundUpdate object in the same
+ * manner like this.update() adds data to it.
+ *
+ * @param {string} action
+ * The type of change. One of "added", "changed" or "deleted"
+ * @param {string} storeType
+ * The storage actor for which you want to remove the updates data.
+ * @param {object} data
+ * The update object. This object is of the following format:
+ * - {
+ * <host1>: [<store_names1>, <store_name2>...],
+ * <host2>: [<store_names34>...],
+ * }
+ * Where host1, host2 are the hosts which you want to remove and
+ * [<store_namesX] is an array of the names of the store objects.
+ */
+ removeNamesFromUpdateList(action, storeType, data) {
+ for (const host in data) {
+ if (
+ this.boundUpdate[action] &&
+ this.boundUpdate[action][storeType] &&
+ this.boundUpdate[action][storeType][host]
+ ) {
+ for (const name of data[host]) {
+ const index = this.boundUpdate[action][storeType][host].indexOf(name);
+ if (index > -1) {
+ this.boundUpdate[action][storeType][host].splice(index, 1);
+ }
+ }
+ if (!this.boundUpdate[action][storeType][host].length) {
+ delete this.boundUpdate[action][storeType][host];
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/devtools/server/actors/resources/websockets.js b/devtools/server/actors/resources/websockets.js
new file mode 100644
index 0000000000..5845357a9c
--- /dev/null
+++ b/devtools/server/actors/resources/websockets.js
@@ -0,0 +1,196 @@
+/* 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 {
+ LongStringActor,
+} = require("resource://devtools/server/actors/string.js");
+
+const {
+ TYPES: { WEBSOCKET },
+} = require("resource://devtools/server/actors/resources/index.js");
+
+const webSocketEventService = Cc[
+ "@mozilla.org/websocketevent/service;1"
+].getService(Ci.nsIWebSocketEventService);
+
+class WebSocketWatcher {
+ constructor() {
+ this.windowIds = new Set();
+ // Maintains a map of all the connection channels per websocket
+ // The map item is keyed on the `webSocketSerialID` and stores
+ // the `httpChannelId` as value.
+ this.connections = new Map();
+ this.onWindowReady = this.onWindowReady.bind(this);
+ this.onWindowDestroy = this.onWindowDestroy.bind(this);
+ }
+
+ static createResource(wsMessageType, eventParams) {
+ return {
+ resourceType: WEBSOCKET,
+ wsMessageType,
+ ...eventParams,
+ };
+ }
+
+ static prepareFramePayload(targetActor, frame) {
+ const payload = new LongStringActor(targetActor.conn, frame.payload);
+ targetActor.manage(payload);
+ return payload.form();
+ }
+
+ watch(targetActor, { onAvailable }) {
+ this.targetActor = targetActor;
+ this.onAvailable = onAvailable;
+
+ for (const window of this.targetActor.windows) {
+ const { innerWindowId } = window.windowGlobalChild;
+ this.startListening(innerWindowId);
+ }
+
+ // On navigate/reload we should re-start listening with the
+ // new `innerWindowID`
+ this.targetActor.on("window-ready", this.onWindowReady);
+ this.targetActor.on("window-destroyed", this.onWindowDestroy);
+ }
+
+ onWindowReady({ window }) {
+ if (!this.targetActor.followWindowGlobalLifeCycle) {
+ const { innerWindowId } = window.windowGlobalChild;
+ this.startListening(innerWindowId);
+ }
+ }
+
+ onWindowDestroy({ id }) {
+ this.stopListening(id);
+ }
+
+ startListening(innerWindowId) {
+ if (!this.windowIds.has(innerWindowId)) {
+ this.windowIds.add(innerWindowId);
+ webSocketEventService.addListener(innerWindowId, this);
+ }
+ }
+
+ stopListening(innerWindowId) {
+ if (this.windowIds.has(innerWindowId)) {
+ this.windowIds.delete(innerWindowId);
+ if (!webSocketEventService.hasListenerFor(innerWindowId)) {
+ // The listener might have already been cleaned up on `window-destroy`.
+ console.warn(
+ "Already stopped listening to websocket events for this window."
+ );
+ return;
+ }
+ webSocketEventService.removeListener(innerWindowId, this);
+ }
+ }
+
+ destroy() {
+ for (const id of this.windowIds) {
+ this.stopListening(id);
+ }
+ this.targetActor.off("window-ready", this.onWindowReady);
+ this.targetActor.off("window-destroyed", this.onWindowDestroy);
+ }
+
+ // methods for the nsIWebSocketEventService
+ webSocketCreated(webSocketSerialID, uri, protocols) {}
+
+ webSocketOpened(
+ webSocketSerialID,
+ effectiveURI,
+ protocols,
+ extensions,
+ httpChannelId
+ ) {
+ this.connections.set(webSocketSerialID, httpChannelId);
+ const resource = WebSocketWatcher.createResource("webSocketOpened", {
+ httpChannelId,
+ effectiveURI,
+ protocols,
+ extensions,
+ });
+
+ this.onAvailable([resource]);
+ }
+
+ webSocketMessageAvailable(webSocketSerialID, data, messageType) {}
+
+ webSocketClosed(webSocketSerialID, wasClean, code, reason) {
+ const httpChannelId = this.connections.get(webSocketSerialID);
+ this.connections.delete(webSocketSerialID);
+
+ const resource = WebSocketWatcher.createResource("webSocketClosed", {
+ httpChannelId,
+ wasClean,
+ code,
+ reason,
+ });
+
+ this.onAvailable([resource]);
+ }
+
+ frameReceived(webSocketSerialID, frame) {
+ const httpChannelId = this.connections.get(webSocketSerialID);
+ if (!httpChannelId) {
+ return;
+ }
+
+ const payload = WebSocketWatcher.prepareFramePayload(
+ this.targetActor,
+ frame
+ );
+ const resource = WebSocketWatcher.createResource("frameReceived", {
+ httpChannelId,
+ data: {
+ type: "received",
+ payload,
+ timeStamp: frame.timeStamp,
+ finBit: frame.finBit,
+ rsvBit1: frame.rsvBit1,
+ rsvBit2: frame.rsvBit2,
+ rsvBit3: frame.rsvBit3,
+ opCode: frame.opCode,
+ mask: frame.mask,
+ maskBit: frame.maskBit,
+ },
+ });
+
+ this.onAvailable([resource]);
+ }
+
+ frameSent(webSocketSerialID, frame) {
+ const httpChannelId = this.connections.get(webSocketSerialID);
+
+ if (!httpChannelId) {
+ return;
+ }
+
+ const payload = WebSocketWatcher.prepareFramePayload(
+ this.targetActor,
+ frame
+ );
+ const resource = WebSocketWatcher.createResource("frameSent", {
+ httpChannelId,
+ data: {
+ type: "sent",
+ payload,
+ timeStamp: frame.timeStamp,
+ finBit: frame.finBit,
+ rsvBit1: frame.rsvBit1,
+ rsvBit2: frame.rsvBit2,
+ rsvBit3: frame.rsvBit3,
+ opCode: frame.opCode,
+ mask: frame.mask,
+ maskBit: frame.maskBit,
+ },
+ });
+
+ this.onAvailable([resource]);
+ }
+}
+
+module.exports = WebSocketWatcher;