summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/resources/console-messages.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/resources/console-messages.js')
-rw-r--r--devtools/server/actors/resources/console-messages.js302
1 files changed, 302 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..a643546692
--- /dev/null
+++ b/devtools/server/actors/resources/console-messages.js
@@ -0,0 +1,302 @@
+/* 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 }) {
+ this.targetActor = targetActor;
+ this.onAvailable = 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 messagesShouldMatchWindow =
+ targetActor.targetType === Targets.TYPES.FRAME &&
+ targetActor.typeName != "parentProcessTarget" &&
+ targetActor.typeName != "webExtensionTarget";
+ const window = messagesShouldMatchWindow ? targetActor.window : null;
+
+ // If we should match messages for a given window but for some reason, targetActor.window
+ // did not return a window, bail out. Otherwise we wouldn't have anything to match against
+ // and would consume all the messages, which could lead to issue (e.g. infinite loop,
+ // see Bug 1828026).
+ if (messagesShouldMatchWindow && !window) {
+ return;
+ }
+
+ 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();
+ this.listener = null;
+ }
+ this.targetActor = null;
+ this.onAvailable = null;
+ }
+
+ /**
+ * Spawn some custom console messages.
+ * This is used for example for log points and JS tracing.
+ *
+ * @param Array<Object> messages
+ * A list of fake nsIConsoleMessage, which looks like the one being generated by
+ * the platform API.
+ */
+ emitMessages(messages) {
+ if (!this.listener) {
+ throw new Error("This target actor isn't listening to console messages");
+ }
+ this.onAvailable(
+ messages.map(message => {
+ if (!message.timeStamp) {
+ throw new Error("timeStamp property is mandatory");
+ }
+
+ return {
+ resourceType: CONSOLE_MESSAGE,
+ message: prepareConsoleMessageForRemote(this.targetActor, 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 || ChromeUtils.dateNow(),
+ 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;
+}