302 lines
10 KiB
JavaScript
302 lines
10 KiB
JavaScript
/* 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;
|
|
}
|