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.js247
-rw-r--r--devtools/server/actors/resources/css-changes.js42
-rw-r--r--devtools/server/actors/resources/css-messages.js133
-rw-r--r--devtools/server/actors/resources/document-event.js52
-rw-r--r--devtools/server/actors/resources/error-messages.js158
-rw-r--r--devtools/server/actors/resources/index.js301
-rw-r--r--devtools/server/actors/resources/moz.build28
-rw-r--r--devtools/server/actors/resources/network-events-stacktraces.js203
-rw-r--r--devtools/server/actors/resources/network-events.js233
-rw-r--r--devtools/server/actors/resources/platform-messages.js53
-rw-r--r--devtools/server/actors/resources/sources.js69
-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.js703
-rw-r--r--devtools/server/actors/resources/utils/content-process-storage.js218
-rw-r--r--devtools/server/actors/resources/utils/moz.build13
-rw-r--r--devtools/server/actors/resources/utils/nsi-console-listener-watcher.js187
17 files changed, 2678 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..e5d92d729c
--- /dev/null
+++ b/devtools/server/actors/resources/console-messages.js
@@ -0,0 +1,247 @@
+/* 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 { WebConsoleUtils } = require("devtools/server/actors/webconsole/utils");
+
+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");
+
+/**
+ * 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 }) {
+ // The following code expects the ThreadActor to be instantiated, via:
+ // prepareConsoleMessageForRemote > SourcesManager.getActorIdForInternalSourceId
+ // The Thread Actor is instantiated via Target.attach, but we should
+ // probably review this and only instantiate the actor instead of attaching the target.
+ if (!targetActor.threadActor) {
+ targetActor.attach();
+ }
+
+ // Bug 1642297: Maybe we could merge ConsoleAPI Listener into this module?
+ const onConsoleAPICall = message => {
+ onAvailable([
+ {
+ resourceType: CONSOLE_MESSAGE,
+ message: prepareConsoleMessageForRemote(targetActor, message),
+ },
+ ]);
+ };
+
+ // Create the consoleAPIListener
+ // (and apply the filtering options defined in the target actor).
+ const listener = new ConsoleAPIListener(
+ targetActor.window,
+ onConsoleAPICall,
+ 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) {
+ if (
+ !result ||
+ !Array.isArray(result.arguments) ||
+ result.arguments.length == 0
+ ) {
+ return null;
+ }
+
+ 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 console-api-log-event.
+ * @return object
+ * The object that can be sent to the remote client.
+ */
+function prepareConsoleMessageForRemote(targetActor, message) {
+ const result = WebConsoleUtils.cloneObject(message);
+
+ result.workerType = WebConsoleUtils.getWorkerType(result) || "none";
+ result.sourceId = getActorIdForInternalSourceId(targetActor, result.sourceId);
+
+ delete result.wrappedJSObject;
+ delete result.ID;
+ delete result.innerID;
+ delete result.consoleID;
+
+ if (result.stacktrace) {
+ result.stacktrace = result.stacktrace.map(frame => {
+ return {
+ ...frame,
+ sourceId: getActorIdForInternalSourceId(targetActor, frame.sourceId),
+ };
+ });
+ }
+
+ result.arguments = (message.arguments || []).map(obj => {
+ const dbgObj = makeDebuggeeValue(targetActor, obj);
+ return createValueGripForTarget(targetActor, dbgObj);
+ });
+
+ result.styles = (message.styles || []).map(string => {
+ return createValueGripForTarget(targetActor, string);
+ });
+
+ if (result.level === "table") {
+ 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);
+ }
+
+ result.category = message.category || "webdev";
+ result.innerWindowID = message.innerID;
+
+ 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..ba4d424ffb
--- /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("devtools/server/actors/resources/index");
+const TrackChangeEmitter = require("devtools/server/actors/utils/track-change-emitter");
+
+/**
+ * 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..1e3f901880
--- /dev/null
+++ b/devtools/server/actors/resources/css-messages.js
@@ -0,0 +1,133 @@
+/* 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("devtools/server/actors/resources/utils/nsi-console-listener-watcher");
+const { Ci } = require("chrome");
+const { DevToolsServer } = require("devtools/server/devtools-server");
+const { createStringGrip } = require("devtools/server/actors/object/utils");
+const {
+ getActorIdForInternalSourceId,
+} = require("devtools/server/actors/utils/dbg-source");
+
+const {
+ TYPES: { CSS_MESSAGE },
+} = require("devtools/server/actors/resources/index");
+
+const { MESSAGE_CATEGORY } = require("devtools/shared/constants");
+
+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;
+ }
+
+ // Process targets listen for everything but messages from private windows.
+ if (this.isProcessTarget(targetActor)) {
+ return !message.isFromPrivateWindow;
+ }
+
+ if (!message.innerWindowID) {
+ return false;
+ }
+
+ const { window } = targetActor;
+ const win = window?.WindowGlobalChild?.getByInnerWindowId(
+ message.innerWindowID
+ );
+ return targetActor.browserId === win?.browsingContext?.browserId;
+ }
+
+ /**
+ * 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.timeStamp,
+ 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..f39d7f3f9a
--- /dev/null
+++ b/devtools/server/actors/resources/document-event.js
@@ -0,0 +1,52 @@
+/* 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("devtools/server/actors/resources/index");
+const {
+ DocumentEventsListener,
+} = require("devtools/server/actors/webconsole/listeners/document-events");
+
+class DocumentEventWatcher {
+ /**
+ * 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) => {
+ onAvailable([
+ {
+ resourceType: DOCUMENT_EVENT,
+ name,
+ time,
+ },
+ ]);
+ };
+
+ this.listener = new DocumentEventsListener(targetActor);
+ this.listener.on("*", onDocumentEvent);
+ this.listener.listen();
+ }
+
+ destroy() {
+ 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..bd388bb415
--- /dev/null
+++ b/devtools/server/actors/resources/error-messages.js
@@ -0,0 +1,158 @@
+/* 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("devtools/server/actors/resources/utils/nsi-console-listener-watcher");
+const { Ci } = require("chrome");
+const { DevToolsServer } = require("devtools/server/devtools-server");
+const ErrorDocs = require("devtools/server/actors/errordocs");
+const {
+ createStringGrip,
+ makeDebuggeeValue,
+ createValueGripForTarget,
+} = require("devtools/server/actors/object/utils");
+const {
+ getActorIdForInternalSourceId,
+} = require("devtools/server/actors/utils/dbg-source");
+
+const {
+ TYPES: { ERROR_MESSAGE },
+} = require("devtools/server/actors/resources/index");
+const { MESSAGE_CATEGORY } = require("devtools/shared/constants");
+
+const PLATFORM_SPECIFIC_CATEGORIES = [
+ "XPConnect JavaScript",
+ "component javascript",
+ "chrome javascript",
+ "chrome registration",
+];
+
+class ErrorMessageWatcher extends nsIConsoleListenerWatcher {
+ shouldHandleMessage(targetActor, message) {
+ // 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;
+ }
+
+ // Process targets listen for everything but messages from private windows
+ if (this.isProcessTarget(targetActor)) {
+ return !message.isFromPrivateWindow;
+ }
+
+ if (!message.innerWindowID) {
+ return false;
+ }
+
+ const { window } = targetActor;
+ const win = window?.WindowGlobalChild?.getByInnerWindowId(
+ message.innerWindowID
+ );
+ return targetActor.browserId === win?.browsingContext?.browserId;
+ }
+
+ /**
+ * 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;
+ }
+
+ // For non-process targets, 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.timeStamp,
+ 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/index.js b/devtools/server/actors/resources/index.js
new file mode 100644
index 0000000000..3d5adf98f5
--- /dev/null
+++ b/devtools/server/actors/resources/index.js
@@ -0,0 +1,301 @@
+/* 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("devtools/server/actors/targets/index");
+
+const TYPES = {
+ CONSOLE_MESSAGE: "console-message",
+ CSS_CHANGE: "css-change",
+ CSS_MESSAGE: "css-message",
+ DOCUMENT_EVENT: "document-event",
+ ERROR_MESSAGE: "error-message",
+ LOCAL_STORAGE: "local-storage",
+ PLATFORM_MESSAGE: "platform-message",
+ NETWORK_EVENT: "network-event",
+ SESSION_STORAGE: "session-storage",
+ STYLESHEET: "stylesheet",
+ NETWORK_EVENT_STACKTRACE: "network-event-stacktrace",
+ SOURCE: "source",
+};
+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 lists are specific for the parent process and each target type.
+const FrameTargetResources = augmentResourceDictionary({
+ [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_STACKTRACE]: {
+ path: "devtools/server/actors/resources/network-events-stacktraces",
+ },
+ [TYPES.SOURCE]: {
+ path: "devtools/server/actors/resources/sources",
+ },
+});
+const ProcessTargetResources = augmentResourceDictionary({
+ [TYPES.CONSOLE_MESSAGE]: {
+ path: "devtools/server/actors/resources/console-messages",
+ },
+ [TYPES.CSS_MESSAGE]: {
+ path: "devtools/server/actors/resources/css-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",
+ },
+});
+
+// We'll only support a few resource types in Workers (console-message, source,
+// breakpoints, …) 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",
+ },
+});
+
+const ParentProcessResources = augmentResourceDictionary({
+ [TYPES.NETWORK_EVENT]: {
+ path: "devtools/server/actors/resources/network-events",
+ },
+});
+
+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 watcherOrTargetActor
+ * Either a WatcherActor or a TargetActor which can be listening to a resource.
+ */
+function getResourceTypeDictionary(watcherOrTargetActor) {
+ const { typeName } = watcherOrTargetActor;
+ if (typeName == "watcher") {
+ return ParentProcessResources;
+ }
+ const { targetType } = watcherOrTargetActor;
+ 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 watcherOrTargetActor
+ * Either a WatcherActor or a TargetActor which can be listening to a resource.
+ * @param String resourceType
+ * The resource type to be observed.
+ */
+function getResourceTypeEntry(watcherOrTargetActor, resourceType) {
+ const dict = getResourceTypeDictionary(watcherOrTargetActor);
+ if (!(resourceType in dict)) {
+ throw new Error(
+ `Unsupported resource type '${resourceType}' for ${watcherOrTargetActor.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 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.
+ * This actor:
+ * - defines what context to observe (browsing context, process, worker, ...)
+ * Via browsingContextID, windows, docShells attributes for the target actor.
+ * Via browserId or browserElement attributes for the watcher actor.
+ * - exposes `notifyResourceAvailable` method to be notified about the available resources
+ * @param Array<String> resourceTypes
+ * List of all type of resource to listen to.
+ */
+async function watchResources(watcherOrTargetActor, 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 } = watcherOrTargetActor;
+ if (targetType) {
+ resourceTypes = getResourceTypesForTargetType(resourceTypes, targetType);
+ }
+ for (const resourceType of resourceTypes) {
+ const { watchers, WatcherClass } = getResourceTypeEntry(
+ watcherOrTargetActor,
+ resourceType
+ );
+
+ // Ignore resources we're already listening to
+ if (watchers.has(watcherOrTargetActor)) {
+ continue;
+ }
+
+ const watcher = new WatcherClass();
+ await watcher.watch(watcherOrTargetActor, {
+ onAvailable: watcherOrTargetActor.notifyResourceAvailable,
+ onDestroyed: watcherOrTargetActor.notifyResourceDestroyed,
+ onUpdated: watcherOrTargetActor.notifyResourceUpdated,
+ });
+ watchers.set(watcherOrTargetActor, 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 watcherOrTargetActor
+ * The related actor, already passed to watchResources.
+ * @param Array<String> resourceTypes
+ * List of all type of resource to stop listening to.
+ */
+function unwatchResources(watcherOrTargetActor, resourceTypes) {
+ for (const resourceType of resourceTypes) {
+ // Pull all info about this resource type from `Resources` global object
+ const { watchers } = getResourceTypeEntry(
+ watcherOrTargetActor,
+ resourceType
+ );
+
+ const watcher = watchers.get(watcherOrTargetActor);
+ if (watcher) {
+ watcher.destroy();
+ watchers.delete(watcherOrTargetActor);
+ }
+ }
+}
+exports.unwatchResources = unwatchResources;
+
+/**
+ * Stop watching for all watched resources on a given actor.
+ *
+ * @param Actor watcherOrTargetActor
+ * The related actor, already passed to watchResources.
+ */
+function unwatchAllTargetResources(watcherOrTargetActor) {
+ for (const { watchers } of Object.values(
+ getResourceTypeDictionary(watcherOrTargetActor)
+ )) {
+ const watcher = watchers.get(watcherOrTargetActor);
+ if (watcher) {
+ watcher.destroy();
+ watchers.delete(watcherOrTargetActor);
+ }
+ }
+}
+exports.unwatchAllTargetResources = unwatchAllTargetResources;
+
+/**
+ * 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/moz.build b/devtools/server/actors/resources/moz.build
new file mode 100644
index 0000000000..964456ee63
--- /dev/null
+++ b/devtools/server/actors/resources/moz.build
@@ -0,0 +1,28 @@
+# -*- 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",
+ "index.js",
+ "network-events-stacktraces.js",
+ "network-events.js",
+ "platform-messages.js",
+ "sources.js",
+ "storage-local-storage.js",
+ "storage-session-storage.js",
+ "stylesheets.js",
+)
+
+with Files("*-messages.js"):
+ BUG_COMPONENT = ("DevTools", "Console")
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..4dbaa55ebd
--- /dev/null
+++ b/devtools/server/actors/resources/network-events-stacktraces.js
@@ -0,0 +1,203 @@
+/* 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("devtools/server/actors/resources/index");
+
+const { Ci, components } = require("chrome");
+const Services = require("Services");
+
+loader.lazyRequireGetter(
+ this,
+ "ChannelEventSinkFactory",
+ "devtools/server/actors/network-monitor/channel-event-sink",
+ true
+);
+
+loader.lazyRequireGetter(
+ this,
+ "matchRequest",
+ "devtools/server/actors/network-monitor/network-observer",
+ true
+);
+
+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 }) {
+ 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);
+
+ this.targetActor = targetActor;
+ this.onStackTraceAvailable = onAvailable;
+
+ this.stacktraces = new Map();
+ }
+
+ /**
+ * 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) {
+ 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;
+ }
+ }
+ }
+
+ // XXX: is window the best filter to use?
+ if (!matchRequest(channel, { window: this.targetActor.window })) {
+ 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,
+ targetFront: this.targetFront,
+ stacktraceAvailable: stacktrace && stacktrace.length > 0,
+ lastFrame:
+ stacktrace && stacktrace.length > 0 ? stacktrace[0] : undefined,
+ },
+ ]);
+ }
+
+ getStackTrace(id) {
+ let stacktrace = [];
+ if (this.stacktraces.has(id)) {
+ stacktrace = this.stacktraces.get(id);
+ this.stacktraces.delete(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..306af8c7da
--- /dev/null
+++ b/devtools/server/actors/resources/network-events.js
@@ -0,0 +1,233 @@
+/* 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,
+ "NetworkObserver",
+ "devtools/server/actors/network-monitor/network-observer",
+ true
+);
+
+loader.lazyRequireGetter(
+ this,
+ "NetworkEventActor",
+ "devtools/server/actors/network-monitor/network-event-actor",
+ true
+);
+
+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.onNetworkEventAvailable = onAvailable;
+ this.onNetworkEventUpdated = onUpdated;
+
+ this.listener = new NetworkObserver(
+ { browserId: watcherActor.browserId },
+ { onNetworkEvent: this.onNetworkEvent.bind(this) }
+ );
+
+ this.listener.init();
+ }
+
+ /**
+ * Gets the throttle settings
+ *
+ * @return {*} data
+ *
+ */
+ getThrottleData() {
+ return this.listener.throttleData;
+ }
+
+ /**
+ * Sets the throttle data
+ *
+ * @param {*} data
+ *
+ */
+ setThrottleData(data) {
+ this.listener.throttleData = data;
+ }
+
+ /**
+ * 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();
+ }
+
+ 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,
+ {
+ onNetworkEventUpdate: this.onNetworkEventUpdate.bind(this),
+ onNetworkEventDestroy: this.onNetworkEventDestroy.bind(this),
+ },
+ event
+ );
+ this.watcherActor.manage(actor);
+
+ const resource = actor.asResource();
+
+ this.networkEvents.set(resource.resourceId, {
+ 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 {
+ 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")
+ ) {
+ return;
+ }
+
+ this.onNetworkEventUpdated([
+ {
+ resourceType,
+ resourceId,
+ resourceUpdates,
+ },
+ ]);
+ }
+
+ onNetworkEventDestroy(channelId) {
+ if (this.networkEvents.has(channelId)) {
+ this.networkEvents.delete(channelId);
+ }
+ }
+
+ /**
+ * Stop watching for network event related to a given Watcher Actor.
+ *
+ * @param WatcherActor watcherActor
+ * The watcher actor from which we should stop observing network events
+ */
+ destroy(watcherActor) {
+ if (this.listener) {
+ this.listener.destroy();
+ }
+ }
+}
+
+module.exports = NetworkEventWatcher;
diff --git a/devtools/server/actors/resources/platform-messages.js b/devtools/server/actors/resources/platform-messages.js
new file mode 100644
index 0000000000..9e2e4d1aa2
--- /dev/null
+++ b/devtools/server/actors/resources/platform-messages.js
@@ -0,0 +1,53 @@
+/* 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("devtools/server/actors/resources/utils/nsi-console-listener-watcher");
+const { Ci } = require("chrome");
+
+const {
+ TYPES: { PLATFORM_MESSAGE },
+} = require("devtools/server/actors/resources/index");
+
+const { createStringGrip } = require("devtools/server/actors/object/utils");
+
+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;
+ }
+
+ 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.timeStamp,
+ resourceType: PLATFORM_MESSAGE,
+ };
+ }
+}
+module.exports = PlatformMessageWatcher;
diff --git a/devtools/server/actors/resources/sources.js b/devtools/server/actors/resources/sources.js
new file mode 100644
index 0000000000..04ee4a1cc7
--- /dev/null
+++ b/devtools/server/actors/resources/sources.js
@@ -0,0 +1,69 @@
+/* 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("devtools/server/actors/resources/index");
+
+/**
+ * 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 }) {
+ // Force attaching the target in order to ensure it instantiates the ThreadActor
+ targetActor.attach();
+
+ const { threadActor } = targetActor;
+ this.threadActor = threadActor;
+ this.onAvailable = onAvailable;
+
+ // Disable `ThreadActor.newSource` RDP event in order to avoid unnecessary traffic
+ threadActor.disableNewSourceEvents();
+
+ threadActor.sourcesManager.on("newSource", this.onNewSource);
+
+ // 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() {
+ this.threadActor.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-local-storage.js b/devtools/server/actors/resources/storage-local-storage.js
new file mode 100644
index 0000000000..0a582cba79
--- /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("devtools/server/actors/resources/index");
+
+const ContentProcessStorage = require("devtools/server/actors/resources/utils/content-process-storage");
+
+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..e6ad3eef88
--- /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("devtools/server/actors/resources/index");
+
+const ContentProcessStorage = require("devtools/server/actors/resources/utils/content-process-storage");
+
+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..40d3bf33f1
--- /dev/null
+++ b/devtools/server/actors/resources/stylesheets.js
@@ -0,0 +1,703 @@
+/* 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 { Ci } = require("chrome");
+const { fetch } = require("devtools/shared/DevToolsUtils");
+const InspectorUtils = require("InspectorUtils");
+const {
+ getSourcemapBaseURL,
+} = require("devtools/server/actors/utils/source-map-utils");
+
+const {
+ TYPES: { STYLESHEET },
+} = require("devtools/server/actors/resources/index");
+
+loader.lazyRequireGetter(
+ this,
+ "CssLogic",
+ "devtools/shared/inspector/css-logic"
+);
+
+loader.lazyRequireGetter(
+ this,
+ ["addPseudoClassLock", "removePseudoClassLock"],
+ "devtools/server/actors/highlighters/utils/markup",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "loadSheet",
+ "devtools/shared/layout/utils",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ ["UPDATE_GENERAL", "UPDATE_PRESERVING_RULES"],
+ "devtools/server/actors/style-sheet",
+ true
+);
+
+const TRANSITION_PSEUDO_CLASS = ":-moz-styleeditor-transitioning";
+const TRANSITION_DURATION_MS = 500;
+const TRANSITION_BUFFER_MS = 1000;
+const TRANSITION_RULE_SELECTOR = `:root${TRANSITION_PSEUDO_CLASS}, :root${TRANSITION_PSEUDO_CLASS} *`;
+const TRANSITION_SHEET =
+ "data:text/css;charset=utf-8," +
+ encodeURIComponent(`
+ ${TRANSITION_RULE_SELECTOR} {
+ transition-duration: ${TRANSITION_DURATION_MS}ms !important;
+ transition-delay: 0ms !important;
+ transition-timing-function: ease-out !important;
+ transition-property: all !important;
+ }
+`);
+
+// If the user edits a stylesheet, we stash a copy of the edited text
+// here, keyed by the stylesheet. This way, if the tools are closed
+// and then reopened, the edited text will be available. A weak map
+// is used so that navigation by the user will eventually cause the
+// edited text to be collected.
+const modifiedStyleSheets = new WeakMap();
+
+class StyleSheetWatcher {
+ constructor() {
+ this._resourceCount = 0;
+ // The _styleSheetMap maps resourceId and following value.
+ // {
+ // styleSheet: Raw StyleSheet object.
+ // }
+ this._styleSheetMap = new Map();
+ // List of all watched media queries. Change listeners are being registered from _getMediaRules.
+ this._mqlList = [];
+
+ this._onApplicableStateChanged = this._onApplicableStateChanged.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;
+
+ // Listen for new stylesheet being added via StyleSheetApplicableStateChanged
+ this._targetActor.chromeEventHandler.addEventListener(
+ "StyleSheetApplicableStateChanged",
+ this._onApplicableStateChanged,
+ true
+ );
+
+ const styleSheets = [];
+
+ for (const window of this._targetActor.windows) {
+ // We have to set this flag in order to get the
+ // StyleSheetApplicableStateChanged events. See Document.webidl.
+ window.document.styleSheetChangeEventsEnabled = true;
+
+ styleSheets.push(...(await this._getStyleSheets(window)));
+ }
+
+ await this._notifyResourcesAvailable(styleSheets);
+ }
+
+ /**
+ * Create a new style sheet in the document with the given text.
+ *
+ * @param {Document} document
+ * Document that the new style sheet belong to.
+ * @param {string} text
+ * Content of style sheet.
+ * @param {string} fileName
+ * If the stylesheet adding is from file, `fileName` indicates the path.
+ */
+ async addStyleSheet(document, text, fileName) {
+ const parent = document.documentElement;
+ const style = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "style"
+ );
+ style.setAttribute("type", "text/css");
+
+ if (text) {
+ style.appendChild(document.createTextNode(text));
+ }
+
+ // This triggers StyleSheetApplicableStateChanged event.
+ parent.appendChild(style);
+
+ // This promise will be resolved when the resource for this stylesheet is available.
+ let resolve = null;
+ const promise = new Promise(r => {
+ resolve = r;
+ });
+
+ if (!this._stylesheetCreationData) {
+ this._stylesheetCreationData = new WeakMap();
+ }
+ this._stylesheetCreationData.set(style.sheet, {
+ isCreatedByDevTools: true,
+ fileName,
+ resolve,
+ });
+
+ await promise;
+
+ return style.sheet;
+ }
+
+ async ensureResourceAvailable(styleSheet) {
+ if (this.getResourceId(styleSheet)) {
+ return;
+ }
+
+ await this._notifyResourcesAvailable([styleSheet]);
+ }
+
+ /**
+ * Return resourceId of the given style sheet.
+ */
+ getResourceId(styleSheet) {
+ for (const [resourceId, value] of this._styleSheetMap.entries()) {
+ if (styleSheet === value.styleSheet) {
+ return resourceId;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Return owner node of the style sheet of the given resource id.
+ */
+ getOwnerNode(resourceId) {
+ const { styleSheet } = this._styleSheetMap.get(resourceId);
+ return styleSheet.ownerNode;
+ }
+
+ /**
+ * Return the style sheet of the given resource id.
+ */
+ getStyleSheet(resourceId) {
+ const { styleSheet } = this._styleSheetMap.get(resourceId);
+ return styleSheet;
+ }
+
+ /**
+ * Return the index of given stylesheet of the given resource id.
+ */
+ getStyleSheetIndex(resourceId) {
+ const { styleSheet } = this._styleSheetMap.get(resourceId);
+ return this._getStyleSheetIndex(styleSheet);
+ }
+
+ /**
+ * Protocol method to get the text of stylesheet of resourceId.
+ */
+ async getText(resourceId) {
+ const { styleSheet } = this._styleSheetMap.get(resourceId);
+
+ const modifiedText = modifiedStyleSheets.get(styleSheet);
+
+ // modifiedText is the content of the stylesheet updated by update function.
+ // In case not updating, this is undefined.
+ if (modifiedText !== undefined) {
+ return modifiedText;
+ }
+
+ if (!styleSheet.href) {
+ // this is an inline <style> sheet
+ return styleSheet.ownerNode.textContent;
+ }
+
+ return this._fetchStylesheet(styleSheet);
+ }
+
+ /**
+ * Toggle the disabled property of the stylesheet
+ *
+ * @return {Boolean} the disabled state after toggling.
+ */
+ toggleDisabled(resourceId) {
+ const { styleSheet } = this._styleSheetMap.get(resourceId);
+ styleSheet.disabled = !styleSheet.disabled;
+
+ this._notifyPropertyChanged(resourceId, "disabled", styleSheet.disabled);
+
+ return styleSheet.disabled;
+ }
+
+ /**
+ * Update the style sheet in place with new text.
+ *
+ * @param {String} resourceId
+ * @param {String} text
+ * New text.
+ * @param {Boolean} transition
+ * Whether to do CSS transition for change.
+ * @param {Number} kind
+ * Either UPDATE_PRESERVING_RULES or UPDATE_GENERAL
+ * @param {String} cause
+ * Indicates the cause of this update (e.g. "styleeditor") if this was called
+ * from the stylesheet to be edited by the user from the StyleEditor.
+ */
+ async update(
+ resourceId,
+ text,
+ transition,
+ kind = UPDATE_GENERAL,
+ cause = ""
+ ) {
+ const { styleSheet } = this._styleSheetMap.get(resourceId);
+ InspectorUtils.parseStyleSheet(styleSheet, text);
+ modifiedStyleSheets.set(styleSheet, text);
+
+ if (kind !== UPDATE_PRESERVING_RULES) {
+ this._notifyPropertyChanged(
+ resourceId,
+ "ruleCount",
+ styleSheet.cssRules.length
+ );
+ }
+
+ if (transition) {
+ this._startTransition(resourceId, kind, cause);
+ } else {
+ this._notifyResourceUpdated(resourceId, "style-applied", {
+ event: { kind, cause },
+ });
+ }
+
+ // Remove event handler from all media query list we set to.
+ for (const mql of this._mqlList) {
+ mql.onchange = null;
+ }
+
+ const mediaRules = await this._getMediaRules(resourceId, styleSheet);
+ this._notifyResourceUpdated(resourceId, "media-rules-changed", {
+ resourceUpdates: { mediaRules },
+ });
+ }
+
+ _startTransition(resourceId, kind, cause) {
+ const { styleSheet } = this._styleSheetMap.get(resourceId);
+ const document = styleSheet.ownerNode.ownerDocument;
+ const window = styleSheet.ownerNode.ownerGlobal;
+
+ if (!this._transitionSheetLoaded) {
+ this._transitionSheetLoaded = true;
+ // We don't remove this sheet. It uses an internal selector that
+ // we only apply via locks, so there's no need to load and unload
+ // it all the time.
+ loadSheet(window, TRANSITION_SHEET);
+ }
+
+ addPseudoClassLock(document.documentElement, TRANSITION_PSEUDO_CLASS);
+
+ // Set up clean up and commit after transition duration (+buffer)
+ // @see _onTransitionEnd
+ window.clearTimeout(this._transitionTimeout);
+ this._transitionTimeout = window.setTimeout(
+ this._onTransitionEnd.bind(this, resourceId, kind, cause),
+ TRANSITION_DURATION_MS + TRANSITION_BUFFER_MS
+ );
+ }
+
+ _onTransitionEnd(resourceId, kind, cause) {
+ const { styleSheet } = this._styleSheetMap.get(resourceId);
+ const document = styleSheet.ownerNode.ownerDocument;
+
+ this._transitionTimeout = null;
+ removePseudoClassLock(document.documentElement, TRANSITION_PSEUDO_CLASS);
+ this._notifyResourceUpdated(resourceId, "style-applied", {
+ event: { kind, cause },
+ });
+ }
+
+ async _fetchStylesheet(styleSheet) {
+ const href = styleSheet.href;
+
+ const options = {
+ loadFromCache: true,
+ policy: Ci.nsIContentPolicy.TYPE_INTERNAL_STYLESHEET,
+ charset: this._getCSSCharset(styleSheet),
+ };
+
+ // Bug 1282660 - We use the system principal to load the default internal
+ // stylesheets instead of the content principal since such stylesheets
+ // require system principal to load. At meanwhile, we strip the loadGroup
+ // for preventing the assertion of the userContextId mismatching.
+
+ // chrome|file|resource|moz-extension protocols rely on the system principal.
+ const excludedProtocolsRe = /^(chrome|file|resource|moz-extension):\/\//;
+ if (!excludedProtocolsRe.test(href)) {
+ // Stylesheets using other protocols should use the content principal.
+ if (styleSheet.ownerNode) {
+ // eslint-disable-next-line mozilla/use-ownerGlobal
+ options.window = styleSheet.ownerNode.ownerDocument.defaultView;
+ options.principal = styleSheet.ownerNode.ownerDocument.nodePrincipal;
+ }
+ }
+
+ let result;
+
+ try {
+ result = await fetch(href, options);
+ } catch (e) {
+ // The list of excluded protocols can be missing some protocols, try to use the
+ // system principal if the first fetch failed.
+ console.error(
+ `stylesheets: fetch failed for ${href},` +
+ ` using system principal instead.`
+ );
+ options.window = undefined;
+ options.principal = undefined;
+ result = await fetch(href, options);
+ }
+
+ return result.content;
+ }
+
+ _getCSSCharset(styleSheet) {
+ if (styleSheet) {
+ // charset attribute of <link> or <style> element, if it exists
+ if (styleSheet.ownerNode?.getAttribute) {
+ const linkCharset = styleSheet.ownerNode.getAttribute("charset");
+ if (linkCharset != null) {
+ return linkCharset;
+ }
+ }
+
+ // charset of referring document.
+ if (styleSheet.ownerNode?.ownerDocument.characterSet) {
+ return styleSheet.ownerNode.ownerDocument.characterSet;
+ }
+ }
+
+ return "UTF-8";
+ }
+
+ _getCSSRules(styleSheet) {
+ try {
+ return styleSheet.cssRules;
+ } catch (e) {
+ // sheet isn't loaded yet
+ }
+
+ if (!styleSheet.ownerNode) {
+ return Promise.resolve([]);
+ }
+
+ return new Promise(resolve => {
+ styleSheet.ownerNode.addEventListener(
+ "load",
+ () => resolve(styleSheet.cssRules),
+ { once: true }
+ );
+ });
+ }
+
+ async _getImportedStyleSheets(document, styleSheet) {
+ const importedStyleSheets = [];
+
+ for (const rule of await this._getCSSRules(styleSheet)) {
+ if (rule.type == CSSRule.IMPORT_RULE) {
+ // With the Gecko style system, the associated styleSheet may be null
+ // if it has already been seen because an import cycle for the same
+ // URL. With Stylo, the styleSheet will exist (which is correct per
+ // the latest CSSOM spec), so we also need to check ancestors for the
+ // same URL to avoid cycles.
+ if (
+ !rule.styleSheet ||
+ this._haveAncestorWithSameURL(rule.styleSheet) ||
+ !this._shouldListSheet(rule.styleSheet)
+ ) {
+ continue;
+ }
+
+ importedStyleSheets.push(rule.styleSheet);
+
+ // recurse imports in this stylesheet as well
+ const children = await this._getImportedStyleSheets(
+ document,
+ rule.styleSheet
+ );
+ importedStyleSheets.push(...children);
+ } else if (rule.type != CSSRule.CHARSET_RULE) {
+ // @import rules must precede all others except @charset
+ break;
+ }
+ }
+
+ return importedStyleSheets;
+ }
+
+ async _getMediaRules(resourceId, styleSheet) {
+ this._mqlList = [];
+
+ const mediaRules = Array.from(await this._getCSSRules(styleSheet)).filter(
+ rule => rule.type === CSSRule.MEDIA_RULE
+ );
+
+ return mediaRules.map((rule, index) => {
+ let matches = false;
+
+ try {
+ const window = styleSheet.ownerNode.ownerGlobal;
+ const mql = window.matchMedia(rule.media.mediaText);
+ matches = mql.matches;
+ mql.onchange = this._onMatchesChange.bind(this, resourceId, index);
+ this._mqlList.push(mql);
+ } catch (e) {
+ // Ignored
+ }
+
+ return {
+ mediaText: rule.media.mediaText,
+ conditionText: rule.conditionText,
+ matches,
+ line: InspectorUtils.getRuleLine(rule),
+ column: InspectorUtils.getRuleColumn(rule),
+ };
+ });
+ }
+
+ _onMatchesChange(resourceId, index, mql) {
+ this._notifyResourceUpdated(resourceId, "matches-change", {
+ nestedResourceUpdates: [
+ {
+ path: ["mediaRules", index, "matches"],
+ value: mql.matches,
+ },
+ ],
+ });
+ }
+
+ _getNodeHref(styleSheet) {
+ const { ownerNode } = styleSheet;
+ if (!ownerNode) {
+ return null;
+ }
+
+ if (ownerNode.nodeType == ownerNode.DOCUMENT_NODE) {
+ return ownerNode.location.href;
+ } else if (ownerNode.ownerDocument?.location) {
+ return ownerNode.ownerDocument.location.href;
+ }
+
+ return null;
+ }
+
+ _getSourcemapBaseURL(styleSheet) {
+ // When the style is imported, `styleSheet.ownerNode` is null,
+ // so retrieve the topmost parent style sheet which has an ownerNode
+ let parentStyleSheet = styleSheet;
+ while (parentStyleSheet.parentStyleSheet) {
+ parentStyleSheet = parentStyleSheet.parentStyleSheet;
+ }
+
+ // When the style is injected via nsIDOMWindowUtils.loadSheet, even
+ // the parent style sheet has no owner, so default back to target actor
+ // document
+ const ownerDocument = parentStyleSheet.ownerNode
+ ? parentStyleSheet.ownerNode.ownerDocument
+ : this._targetActor.window;
+
+ return getSourcemapBaseURL(
+ // Technically resolveSourceURL should be used here alongside
+ // "this.rawSheet.sourceURL", but the style inspector does not support
+ // /*# sourceURL=*/ in CSS, so we're omitting it here (bug 880831).
+ styleSheet.href || this._getNodeHref(styleSheet),
+ ownerDocument
+ );
+ }
+
+ _getStyleSheetIndex(styleSheet) {
+ const styleSheets = InspectorUtils.getAllStyleSheets(
+ this._targetActor.window.document,
+ true
+ );
+ return styleSheets.indexOf(styleSheet);
+ }
+
+ async _getStyleSheets({ document }) {
+ const documentOnly = !document.nodePrincipal.isSystemPrincipal;
+
+ const styleSheets = [];
+
+ for (const styleSheet of InspectorUtils.getAllStyleSheets(
+ document,
+ documentOnly
+ )) {
+ if (!this._shouldListSheet(styleSheet)) {
+ continue;
+ }
+
+ styleSheets.push(styleSheet);
+
+ // Get all sheets, including imported ones
+ const importedStyleSheets = await this._getImportedStyleSheets(
+ document,
+ styleSheet
+ );
+ styleSheets.push(...importedStyleSheets);
+ }
+
+ return styleSheets;
+ }
+
+ _haveAncestorWithSameURL(styleSheet) {
+ const href = styleSheet.href;
+ while (styleSheet.parentStyleSheet) {
+ if (styleSheet.parentStyleSheet.href == href) {
+ return true;
+ }
+ styleSheet = styleSheet.parentStyleSheet;
+ }
+ return false;
+ }
+
+ _notifyPropertyChanged(resourceId, property, value) {
+ this._notifyResourceUpdated(resourceId, "property-change", {
+ resourceUpdates: { [property]: value },
+ });
+ }
+
+ /**
+ * Event handler that is called when the state of applicable of style sheet is changed.
+ *
+ * For now, StyleSheetApplicableStateChanged event will be called at following timings.
+ * - Append <link> of stylesheet to document
+ * - Append <style> to document
+ * - Change disable attribute of stylesheet object
+ * - Change disable attribute of <link> to false
+ * When appending <link>, <style> or changing `disable` attribute to false, `applicable`
+ * is passed as true. The other hand, when changing `disable` to true, this will be
+ * false.
+ * NOTE: For now, StyleSheetApplicableStateChanged will not be called when removing the
+ * link and style element.
+ *
+ * @param {StyleSheetApplicableStateChanged}
+ * The triggering event.
+ */
+ async _onApplicableStateChanged({ applicable, stylesheet: styleSheet }) {
+ for (const existing of this._styleSheetMap.values()) {
+ if (existing.styleSheet === styleSheet) {
+ return;
+ }
+ }
+
+ if (
+ // Have interest in applicable stylesheet only.
+ applicable &&
+ // No ownerNode means that this stylesheet is *not* associated to a DOM Element.
+ styleSheet.ownerNode &&
+ this._shouldListSheet(styleSheet) &&
+ !this._haveAncestorWithSameURL(styleSheet)
+ ) {
+ await this._notifyResourcesAvailable([styleSheet]);
+ }
+ }
+
+ _shouldListSheet(styleSheet) {
+ // Special case about:PreferenceStyleSheet, as it is generated on the
+ // fly and the URI is not registered with the about: handler.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37
+ if (styleSheet.href?.toLowerCase() === "about:preferencestylesheet") {
+ return false;
+ }
+
+ return true;
+ }
+
+ async _toResource(
+ styleSheet,
+ { isCreatedByDevTools = false, fileName = null } = {}
+ ) {
+ const resourceId = `stylesheet:${this._resourceCount++}`;
+
+ const resource = {
+ resourceId,
+ resourceType: STYLESHEET,
+ disabled: styleSheet.disabled,
+ fileName,
+ href: styleSheet.href,
+ isNew: isCreatedByDevTools,
+ mediaRules: await this._getMediaRules(resourceId, styleSheet),
+ nodeHref: this._getNodeHref(styleSheet),
+ ruleCount: styleSheet.cssRules.length,
+ sourceMapBaseURL: this._getSourcemapBaseURL(styleSheet),
+ sourceMapURL: styleSheet.sourceMapURL,
+ styleSheetIndex: this._getStyleSheetIndex(styleSheet),
+ system: CssLogic.isAgentStylesheet(styleSheet),
+ title: styleSheet.title,
+ };
+
+ return resource;
+ }
+
+ async _notifyResourcesAvailable(styleSheets) {
+ const resources = await Promise.all(
+ styleSheets.map(async styleSheet => {
+ const creationData = this._stylesheetCreationData?.get(styleSheet);
+
+ const resource = await this._toResource(styleSheet, {
+ isCreatedByDevTools: creationData?.isCreatedByDevTools,
+ fileName: creationData?.fileName,
+ });
+
+ this._styleSheetMap.set(resource.resourceId, { styleSheet });
+ return resource;
+ })
+ );
+
+ await this._onAvailable(resources);
+
+ for (const styleSheet of styleSheets) {
+ const creationData = this._stylesheetCreationData?.get(styleSheet);
+ creationData?.resolve();
+ this._stylesheetCreationData?.delete(styleSheet);
+ }
+ }
+
+ _notifyResourceUpdated(
+ resourceId,
+ updateType,
+ { resourceUpdates, nestedResourceUpdates, event }
+ ) {
+ this._onUpdated([
+ {
+ resourceType: STYLESHEET,
+ resourceId,
+ updateType,
+ resourceUpdates,
+ nestedResourceUpdates,
+ event,
+ },
+ ]);
+ }
+
+ destroy() {
+ if (!this._targetActor.docShell) {
+ return;
+ }
+
+ this._targetActor.chromeEventHandler.removeEventListener(
+ "StyleSheetApplicableStateChanged",
+ this._onApplicableStateChanged,
+ true
+ );
+ }
+}
+
+module.exports = StyleSheetWatcher;
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..cf07fe3e48
--- /dev/null
+++ b/devtools/server/actors/resources/utils/content-process-storage.js
@@ -0,0 +1,218 @@
+/* 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("devtools/server/actors/storage");
+
+// ms of delay to throttle updates
+const BATCH_DELAY = 200;
+
+class ContentProcessStorage {
+ constructor(storageKey, storageType) {
+ this.storageKey = storageKey;
+ this.storageType = storageType;
+ }
+
+ async watch(targetActor, { onAvailable, onUpdated, onDestroyed }) {
+ const ActorConstructor = storageTypePool.get(this.storageKey);
+ this.actor = new ActorConstructor({
+ get conn() {
+ return targetActor.conn;
+ },
+ get windows() {
+ // about:blank pages that are included via an iframe, do not get their
+ // own process, and they will be present in targetActor.windows.
+ // We need to ignore them unless they are the top level page.
+ // Otherwise about:blank loads with the same principal as their parent document
+ // and would expose the same storage values as its parent.
+ const windows = targetActor.windows.filter(win => {
+ const isTopPage = win.parent === win;
+ return isTopPage || win.location.href !== "about:blank";
+ });
+
+ return windows;
+ },
+ get window() {
+ return targetActor.window;
+ },
+ get document() {
+ return this.window.document;
+ },
+ get originAttributes() {
+ return this.document.effectiveStoragePrincipal.originAttributes;
+ },
+
+ update(action, storeType, data) {
+ if (!this.boundUpdate) {
+ this.boundUpdate = {};
+ }
+
+ if (action === "cleared") {
+ const response = {};
+ response[this.storageKey] = data;
+
+ onDestroyed([
+ {
+ // needs this so the resource gets passed as an actor
+ // ...storages[storageKey],
+ ...storage,
+ clearedHostsOrPaths: data,
+ },
+ ]);
+ }
+
+ 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, don't send the deleted or changed update
+ this._removeNamesFromUpdateList("deleted", storeType, data);
+ this._removeNamesFromUpdateList("changed", storeType, data);
+ } else if (
+ action === "changed" &&
+ 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 deleted, or a host got deleted, there's no point
+ // in sending added or changed upate, so we remove them.
+ this._removeNamesFromUpdateList("added", storeType, data);
+ this._removeNamesFromUpdateList("changed", storeType, data);
+
+ for (const host in data) {
+ if (
+ data[host].length === 0 &&
+ this.boundUpdate?.added?.[storeType]?.[host]
+ ) {
+ delete this.boundUpdate.added[storeType][host];
+ }
+
+ if (
+ data[host].length === 0 &&
+ this.boundUpdate?.changed?.[storeType]?.[host]
+ ) {
+ delete this.boundUpdate.changed[storeType][host];
+ }
+ }
+ }
+
+ this.batchTimer = setTimeout(() => {
+ clearTimeout(this.batchTimer);
+ onUpdated([
+ {
+ // needs this so the resource gets passed as an actor
+ // ...storages[storageKey],
+ ...storage,
+ added: this.boundUpdate.added,
+ changed: this.boundUpdate.changed,
+ deleted: this.boundUpdate.deleted,
+ },
+ ]);
+ 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]?.[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;
+ },
+
+ on() {
+ targetActor.on.apply(this, arguments);
+ },
+ off() {
+ targetActor.off.apply(this, arguments);
+ },
+ once() {
+ targetActor.once.apply(this, arguments);
+ },
+ });
+
+ // We have to manage the actor manually, because ResourceWatcher 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]);
+ }
+
+ destroy() {
+ this.actor?.destroy();
+ this.actor = null;
+ }
+}
+
+module.exports = ContentProcessStorage;
diff --git a/devtools/server/actors/resources/utils/moz.build b/devtools/server/actors/resources/utils/moz.build
new file mode 100644
index 0000000000..b57360d218
--- /dev/null
+++ b/devtools/server/actors/resources/utils/moz.build
@@ -0,0 +1,13 @@
+# -*- 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",
+)
+
+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..352d622c30
--- /dev/null
+++ b/devtools/server/actors/resources/utils/nsi-console-listener-watcher.js
@@ -0,0 +1,187 @@
+/* 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 { Ci, Cu } = require("chrome");
+const Services = require("Services");
+const ChromeUtils = require("ChromeUtils");
+
+const { createStringGrip } = require("devtools/server/actors/object/utils");
+
+const {
+ getActorIdForInternalSourceId,
+} = require("devtools/server/actors/utils/dbg-source");
+
+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;
+ }
+
+ // The following code expects the ThreadActor to be instantiated (in prepareStackForRemote)
+ // The Thread Actor is instantiated via Target.attach, but we should probably review
+ // this and only instantiate the actor instead of attaching the target.
+ if (!targetActor.threadActor) {
+ targetActor.attach();
+ }
+
+ // Create the consoleListener.
+ const listener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIConsoleListener"]),
+ observe: message => {
+ if (!this.shouldHandleMessage(targetActor, message)) {
+ return;
+ }
+
+ onAvailable([this.buildResource(targetActor, message)]);
+ },
+ };
+
+ // Retrieve the cached messages just before registering the listener, so we don't get
+ // duplicated messages.
+ const cachedMessages = Services.console.getMessageArray() || [];
+ 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)) {
+ 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;