Source code

Revision control

Copy as Markdown

Other Tools

/* 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;
}