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.js247
1 files changed, 247 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;
+}