diff options
Diffstat (limited to 'devtools/server/actors/resources/console-messages.js')
-rw-r--r-- | devtools/server/actors/resources/console-messages.js | 302 |
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; +} |