summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/webconsole.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /devtools/server/actors/webconsole.js
parentInitial commit. (diff)
downloadfirefox-upstream/124.0.1.tar.xz
firefox-upstream/124.0.1.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/server/actors/webconsole.js')
-rw-r--r--devtools/server/actors/webconsole.js1736
1 files changed, 1736 insertions, 0 deletions
diff --git a/devtools/server/actors/webconsole.js b/devtools/server/actors/webconsole.js
new file mode 100644
index 0000000000..14401b3fbe
--- /dev/null
+++ b/devtools/server/actors/webconsole.js
@@ -0,0 +1,1736 @@
+/* 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/. */
+
+/* global clearConsoleEvents */
+
+"use strict";
+
+const { Actor } = require("resource://devtools/shared/protocol.js");
+const {
+ webconsoleSpec,
+} = require("resource://devtools/shared/specs/webconsole.js");
+
+const {
+ DevToolsServer,
+} = require("resource://devtools/server/devtools-server.js");
+const { ThreadActor } = require("resource://devtools/server/actors/thread.js");
+const { ObjectActor } = require("resource://devtools/server/actors/object.js");
+const {
+ LongStringActor,
+} = require("resource://devtools/server/actors/string.js");
+const {
+ createValueGrip,
+ isArray,
+ stringIsLong,
+} = require("resource://devtools/server/actors/object/utils.js");
+const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
+const ErrorDocs = require("resource://devtools/server/actors/errordocs.js");
+const Targets = require("resource://devtools/server/actors/targets/index.js");
+
+loader.lazyRequireGetter(
+ this,
+ "evalWithDebugger",
+ "resource://devtools/server/actors/webconsole/eval-with-debugger.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "ConsoleFileActivityListener",
+ "resource://devtools/server/actors/webconsole/listeners/console-file-activity.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "jsPropertyProvider",
+ "resource://devtools/shared/webconsole/js-property-provider.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ ["isCommand"],
+ "resource://devtools/server/actors/webconsole/commands/parser.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ ["CONSOLE_WORKER_IDS", "WebConsoleUtils"],
+ "resource://devtools/server/actors/webconsole/utils.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ ["WebConsoleCommandsManager"],
+ "resource://devtools/server/actors/webconsole/commands/manager.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "EnvironmentActor",
+ "resource://devtools/server/actors/environment.js",
+ true
+);
+loader.lazyRequireGetter(
+ this,
+ "EventEmitter",
+ "resource://devtools/shared/event-emitter.js"
+);
+loader.lazyRequireGetter(
+ this,
+ "MESSAGE_CATEGORY",
+ "resource://devtools/shared/constants.js",
+ true
+);
+
+// Generated by /devtools/shared/webconsole/GenerateReservedWordsJS.py
+loader.lazyRequireGetter(
+ this,
+ "RESERVED_JS_KEYWORDS",
+ "resource://devtools/shared/webconsole/reserved-js-words.js"
+);
+
+// Overwrite implemented listeners for workers so that we don't attempt
+// to load an unsupported module.
+if (isWorker) {
+ loader.lazyRequireGetter(
+ this,
+ ["ConsoleAPIListener", "ConsoleServiceListener"],
+ "resource://devtools/server/actors/webconsole/worker-listeners.js",
+ true
+ );
+} else {
+ loader.lazyRequireGetter(
+ this,
+ "ConsoleAPIListener",
+ "resource://devtools/server/actors/webconsole/listeners/console-api.js",
+ true
+ );
+ loader.lazyRequireGetter(
+ this,
+ "ConsoleServiceListener",
+ "resource://devtools/server/actors/webconsole/listeners/console-service.js",
+ true
+ );
+ loader.lazyRequireGetter(
+ this,
+ "ConsoleReflowListener",
+ "resource://devtools/server/actors/webconsole/listeners/console-reflow.js",
+ true
+ );
+ loader.lazyRequireGetter(
+ this,
+ "DocumentEventsListener",
+ "resource://devtools/server/actors/webconsole/listeners/document-events.js",
+ true
+ );
+}
+loader.lazyRequireGetter(
+ this,
+ "ObjectUtils",
+ "resource://devtools/server/actors/object/utils.js"
+);
+
+function isObject(value) {
+ return Object(value) === value;
+}
+
+/**
+ * The WebConsoleActor implements capabilities needed for the Web Console
+ * feature.
+ *
+ * @constructor
+ * @param object connection
+ * The connection to the client, DevToolsServerConnection.
+ * @param object [parentActor]
+ * Optional, the parent actor.
+ */
+class WebConsoleActor extends Actor {
+ constructor(connection, parentActor) {
+ super(connection, webconsoleSpec);
+
+ this.parentActor = parentActor;
+
+ this.dbg = this.parentActor.dbg;
+
+ this._gripDepth = 0;
+ this._evalCounter = 0;
+ this._listeners = new Set();
+ this._lastConsoleInputEvaluation = undefined;
+
+ this.objectGrip = this.objectGrip.bind(this);
+ this._onWillNavigate = this._onWillNavigate.bind(this);
+ this._onChangedToplevelDocument =
+ this._onChangedToplevelDocument.bind(this);
+ this.onConsoleServiceMessage = this.onConsoleServiceMessage.bind(this);
+ this.onConsoleAPICall = this.onConsoleAPICall.bind(this);
+ this.onDocumentEvent = this.onDocumentEvent.bind(this);
+
+ EventEmitter.on(
+ this.parentActor,
+ "changed-toplevel-document",
+ this._onChangedToplevelDocument
+ );
+ }
+
+ /**
+ * Debugger instance.
+ *
+ * @see jsdebugger.sys.mjs
+ */
+ dbg = null;
+
+ /**
+ * This is used by the ObjectActor to keep track of the depth of grip() calls.
+ * @private
+ * @type number
+ */
+ _gripDepth = null;
+
+ /**
+ * Holds a set of all currently registered listeners.
+ *
+ * @private
+ * @type Set
+ */
+ _listeners = null;
+
+ /**
+ * The global we work with (this can be a Window, a Worker global or even a Sandbox
+ * for processes and addons).
+ *
+ * @type nsIDOMWindow, WorkerGlobalScope or Sandbox
+ */
+ get global() {
+ if (this.parentActor.isRootActor) {
+ return this._getWindowForBrowserConsole();
+ }
+ return this.parentActor.window || this.parentActor.workerGlobal;
+ }
+
+ /**
+ * Get a window to use for the browser console.
+ *
+ * (note that is is also used for browser toolbox and webextension
+ * i.e. all targets flagged with isRootActor=true)
+ *
+ * @private
+ * @return nsIDOMWindow
+ * The window to use, or null if no window could be found.
+ */
+ _getWindowForBrowserConsole() {
+ // Check if our last used chrome window is still live.
+ let window = this._lastChromeWindow && this._lastChromeWindow.get();
+ // If not, look for a new one.
+ // In case of WebExtension reload of the background page, the last
+ // chrome window might be a dead wrapper, from which we can't check for window.closed.
+ if (!window || Cu.isDeadWrapper(window) || window.closed) {
+ window = this.parentActor.window;
+ if (!window) {
+ // Try to find the Browser Console window to use instead.
+ window = Services.wm.getMostRecentWindow("devtools:webconsole");
+ // We prefer the normal chrome window over the console window,
+ // so we'll look for those windows in order to replace our reference.
+ const onChromeWindowOpened = () => {
+ // We'll look for this window when someone next requests window()
+ Services.obs.removeObserver(onChromeWindowOpened, "domwindowopened");
+ this._lastChromeWindow = null;
+ };
+ Services.obs.addObserver(onChromeWindowOpened, "domwindowopened");
+ }
+
+ this._handleNewWindow(window);
+ }
+
+ return window;
+ }
+
+ /**
+ * Store a newly found window on the actor to be used in the future.
+ *
+ * @private
+ * @param nsIDOMWindow window
+ * The window to store on the actor (can be null).
+ */
+ _handleNewWindow(window) {
+ if (window) {
+ if (this._hadChromeWindow) {
+ Services.console.logStringMessage("Webconsole context has changed");
+ }
+ this._lastChromeWindow = Cu.getWeakReference(window);
+ this._hadChromeWindow = true;
+ } else {
+ this._lastChromeWindow = null;
+ }
+ }
+
+ /**
+ * Whether we've been using a window before.
+ *
+ * @private
+ * @type boolean
+ */
+ _hadChromeWindow = false;
+
+ /**
+ * A weak reference to the last chrome window we used to work with.
+ *
+ * @private
+ * @type nsIWeakReference
+ */
+ _lastChromeWindow = null;
+
+ // The evalGlobal is used at the scope for JS evaluation.
+ _evalGlobal = null;
+ get evalGlobal() {
+ return this._evalGlobal || this.global;
+ }
+
+ set evalGlobal(global) {
+ this._evalGlobal = global;
+
+ if (!this._progressListenerActive) {
+ EventEmitter.on(this.parentActor, "will-navigate", this._onWillNavigate);
+ this._progressListenerActive = true;
+ }
+ }
+
+ /**
+ * Flag used to track if we are listening for events from the progress
+ * listener of the target actor. We use the progress listener to clear
+ * this.evalGlobal on page navigation.
+ *
+ * @private
+ * @type boolean
+ */
+ _progressListenerActive = false;
+
+ /**
+ * The ConsoleServiceListener instance.
+ * @type object
+ */
+ consoleServiceListener = null;
+
+ /**
+ * The ConsoleAPIListener instance.
+ */
+ consoleAPIListener = null;
+
+ /**
+ * The ConsoleFileActivityListener instance.
+ */
+ consoleFileActivityListener = null;
+
+ /**
+ * The ConsoleReflowListener instance.
+ */
+ consoleReflowListener = null;
+
+ grip() {
+ return { actor: this.actorID };
+ }
+
+ _findProtoChain = ThreadActor.prototype._findProtoChain;
+ _removeFromProtoChain = ThreadActor.prototype._removeFromProtoChain;
+
+ /**
+ * Destroy the current WebConsoleActor instance.
+ */
+ destroy() {
+ this.stopListeners();
+ super.destroy();
+
+ EventEmitter.off(
+ this.parentActor,
+ "changed-toplevel-document",
+ this._onChangedToplevelDocument
+ );
+
+ this._lastConsoleInputEvaluation = null;
+ this._evalGlobal = null;
+ this.dbg = null;
+ }
+
+ /**
+ * Create and return an environment actor that corresponds to the provided
+ * Debugger.Environment. This is a straightforward clone of the ThreadActor's
+ * method except that it stores the environment actor in the web console
+ * actor's pool.
+ *
+ * @param Debugger.Environment environment
+ * The lexical environment we want to extract.
+ * @return The EnvironmentActor for |environment| or |undefined| for host
+ * functions or functions scoped to a non-debuggee global.
+ */
+ createEnvironmentActor(environment) {
+ if (!environment) {
+ return undefined;
+ }
+
+ if (environment.actor) {
+ return environment.actor;
+ }
+
+ const actor = new EnvironmentActor(environment, this);
+ this.manage(actor);
+ environment.actor = actor;
+
+ return actor;
+ }
+
+ /**
+ * Create a grip for the given value.
+ *
+ * @param mixed value
+ * @return object
+ */
+ createValueGrip(value) {
+ return createValueGrip(value, this, this.objectGrip);
+ }
+
+ /**
+ * Make a debuggee value for the given value.
+ *
+ * @param mixed value
+ * The value you want to get a debuggee value for.
+ * @param boolean useObjectGlobal
+ * If |true| the object global is determined and added as a debuggee,
+ * otherwise |this.global| is used when makeDebuggeeValue() is invoked.
+ * @return object
+ * Debuggee value for |value|.
+ */
+ makeDebuggeeValue(value, useObjectGlobal) {
+ if (useObjectGlobal && isObject(value)) {
+ try {
+ const global = Cu.getGlobalForObject(value);
+ const dbgGlobal = this.dbg.makeGlobalObjectReference(global);
+ return dbgGlobal.makeDebuggeeValue(value);
+ } catch (ex) {
+ // The above can throw an exception if value is not an actual object
+ // or 'Object in compartment marked as invisible to Debugger'
+ }
+ }
+ const dbgGlobal = this.dbg.makeGlobalObjectReference(this.global);
+ return dbgGlobal.makeDebuggeeValue(value);
+ }
+
+ /**
+ * Create a grip for the given object.
+ *
+ * @param object object
+ * The object you want.
+ * @param object pool
+ * A Pool where the new actor instance is added.
+ * @param object
+ * The object grip.
+ */
+ objectGrip(object, pool) {
+ const actor = new ObjectActor(
+ object,
+ {
+ thread: this.parentActor.threadActor,
+ getGripDepth: () => this._gripDepth,
+ incrementGripDepth: () => this._gripDepth++,
+ decrementGripDepth: () => this._gripDepth--,
+ createValueGrip: v => this.createValueGrip(v),
+ createEnvironmentActor: env => this.createEnvironmentActor(env),
+ },
+ this.conn
+ );
+ pool.manage(actor);
+ return actor.form();
+ }
+
+ /**
+ * Create a grip for the given string.
+ *
+ * @param string string
+ * The string you want to create the grip for.
+ * @param object pool
+ * A Pool where the new actor instance is added.
+ * @return object
+ * A LongStringActor object that wraps the given string.
+ */
+ longStringGrip(string, pool) {
+ const actor = new LongStringActor(this.conn, string);
+ pool.manage(actor);
+ return actor.form();
+ }
+
+ /**
+ * Create a long string grip if needed for the given string.
+ *
+ * @private
+ * @param string string
+ * The string you want to create a long string grip for.
+ * @return string|object
+ * A string is returned if |string| is not a long string.
+ * A LongStringActor grip is returned if |string| is a long string.
+ */
+ _createStringGrip(string) {
+ if (string && stringIsLong(string)) {
+ return this.longStringGrip(string, this);
+ }
+ return string;
+ }
+
+ /**
+ * Returns the latest web console input evaluation.
+ * This is undefined if no evaluations have been completed.
+ *
+ * @return object
+ */
+ getLastConsoleInputEvaluation() {
+ return this._lastConsoleInputEvaluation;
+ }
+
+ /**
+ * Preprocess a debugger object (e.g. return the `boundTargetFunction`
+ * debugger object if the given debugger object is a bound function).
+ *
+ * This method is called by both the `inspect` binding implemented
+ * for the webconsole and the one implemented for the devtools API
+ * `browser.devtools.inspectedWindow.eval`.
+ */
+ preprocessDebuggerObject(dbgObj) {
+ // Returns the bound target function on a bound function.
+ if (dbgObj?.isBoundFunction && dbgObj?.boundTargetFunction) {
+ return dbgObj.boundTargetFunction;
+ }
+
+ return dbgObj;
+ }
+
+ /**
+ * This helper is used by the WebExtensionInspectedWindowActor to
+ * inspect an object in the developer toolbox.
+ *
+ * NOTE: shared parts related to preprocess the debugger object (between
+ * this function and the `inspect` webconsole command defined in
+ * "devtools/server/actor/webconsole/utils.js") should be added to
+ * the webconsole actors' `preprocessDebuggerObject` method.
+ */
+ inspectObject(dbgObj, inspectFromAnnotation) {
+ dbgObj = this.preprocessDebuggerObject(dbgObj);
+ this.emit("inspectObject", {
+ objectActor: this.createValueGrip(dbgObj),
+ inspectFromAnnotation,
+ });
+ }
+
+ // Request handlers for known packet types.
+
+ /**
+ * Handler for the "startListeners" request.
+ *
+ * @param array listeners
+ * An array of events to start sent by the Web Console client.
+ * @return object
+ * The response object which holds the startedListeners array.
+ */
+ // eslint-disable-next-line complexity
+ async startListeners(listeners) {
+ const startedListeners = [];
+ const global = !this.parentActor.isRootActor ? this.global : null;
+ const isTargetActorContentProcess =
+ this.parentActor.targetType === Targets.TYPES.PROCESS;
+
+ for (const event of listeners) {
+ switch (event) {
+ case "PageError":
+ // Workers don't support this message type yet
+ if (isWorker) {
+ break;
+ }
+ if (!this.consoleServiceListener) {
+ this.consoleServiceListener = new ConsoleServiceListener(
+ global,
+ this.onConsoleServiceMessage,
+ {
+ matchExactWindow: this.parentActor.ignoreSubFrames,
+ }
+ );
+ this.consoleServiceListener.init();
+ }
+ startedListeners.push(event);
+ break;
+ case "ConsoleAPI":
+ if (!this.consoleAPIListener) {
+ // Create the consoleAPIListener
+ // (and apply the filtering options defined in the parent actor).
+ this.consoleAPIListener = new ConsoleAPIListener(
+ global,
+ this.onConsoleAPICall,
+ {
+ matchExactWindow: this.parentActor.ignoreSubFrames,
+ ...(this.parentActor.consoleAPIListenerOptions || {}),
+ }
+ );
+ this.consoleAPIListener.init();
+ }
+ startedListeners.push(event);
+ break;
+ case "NetworkActivity":
+ // Workers don't support this message type
+ if (isWorker) {
+ break;
+ }
+ // Bug 1807650 removed this in favor of the new Watcher/Resources APIs
+ const errorMessage =
+ "NetworkActivity is no longer supported. " +
+ "Instead use Watcher actor's watchResources and listen to NETWORK_EVENT resource";
+ dump(errorMessage + "\n");
+ throw new Error(errorMessage);
+ case "FileActivity":
+ // Workers don't support this message type
+ if (isWorker) {
+ break;
+ }
+ if (this.global instanceof Ci.nsIDOMWindow) {
+ if (!this.consoleFileActivityListener) {
+ this.consoleFileActivityListener =
+ new ConsoleFileActivityListener(this.global, this);
+ }
+ this.consoleFileActivityListener.startMonitor();
+ startedListeners.push(event);
+ }
+ break;
+ case "ReflowActivity":
+ // Workers don't support this message type
+ if (isWorker) {
+ break;
+ }
+ if (!this.consoleReflowListener) {
+ this.consoleReflowListener = new ConsoleReflowListener(
+ this.global,
+ this
+ );
+ }
+ startedListeners.push(event);
+ break;
+ case "DocumentEvents":
+ // Workers don't support this message type
+ if (isWorker || isTargetActorContentProcess) {
+ break;
+ }
+ if (!this.documentEventsListener) {
+ this.documentEventsListener = new DocumentEventsListener(
+ this.parentActor
+ );
+
+ this.documentEventsListener.on("dom-loading", data =>
+ this.onDocumentEvent("dom-loading", data)
+ );
+ this.documentEventsListener.on("dom-interactive", data =>
+ this.onDocumentEvent("dom-interactive", data)
+ );
+ this.documentEventsListener.on("dom-complete", data =>
+ this.onDocumentEvent("dom-complete", data)
+ );
+
+ this.documentEventsListener.listen();
+ }
+ startedListeners.push(event);
+ break;
+ }
+ }
+
+ // Update the live list of running listeners
+ startedListeners.forEach(this._listeners.add, this._listeners);
+
+ return {
+ startedListeners,
+ };
+ }
+
+ /**
+ * Handler for the "stopListeners" request.
+ *
+ * @param array listeners
+ * An array of events to stop sent by the Web Console client.
+ * @return object
+ * The response packet to send to the client: holds the
+ * stoppedListeners array.
+ */
+ stopListeners(listeners) {
+ const stoppedListeners = [];
+
+ // If no specific listeners are requested to be detached, we stop all
+ // listeners.
+ const eventsToDetach = listeners || [
+ "PageError",
+ "ConsoleAPI",
+ "FileActivity",
+ "ReflowActivity",
+ "DocumentEvents",
+ ];
+
+ for (const event of eventsToDetach) {
+ switch (event) {
+ case "PageError":
+ if (this.consoleServiceListener) {
+ this.consoleServiceListener.destroy();
+ this.consoleServiceListener = null;
+ }
+ stoppedListeners.push(event);
+ break;
+ case "ConsoleAPI":
+ if (this.consoleAPIListener) {
+ this.consoleAPIListener.destroy();
+ this.consoleAPIListener = null;
+ }
+ stoppedListeners.push(event);
+ break;
+ case "FileActivity":
+ if (this.consoleFileActivityListener) {
+ this.consoleFileActivityListener.stopMonitor();
+ this.consoleFileActivityListener = null;
+ }
+ stoppedListeners.push(event);
+ break;
+ case "ReflowActivity":
+ if (this.consoleReflowListener) {
+ this.consoleReflowListener.destroy();
+ this.consoleReflowListener = null;
+ }
+ stoppedListeners.push(event);
+ break;
+ case "DocumentEvents":
+ if (this.documentEventsListener) {
+ this.documentEventsListener.destroy();
+ this.documentEventsListener = null;
+ }
+ stoppedListeners.push(event);
+ break;
+ }
+ }
+
+ // Update the live list of running listeners
+ stoppedListeners.forEach(this._listeners.delete, this._listeners);
+
+ return { stoppedListeners };
+ }
+
+ /**
+ * Handler for the "getCachedMessages" request. This method sends the cached
+ * error messages and the window.console API calls to the client.
+ *
+ * @param array messageTypes
+ * An array of message types sent by the Web Console client.
+ * @return object
+ * The response packet to send to the client: it holds the cached
+ * messages array.
+ */
+ getCachedMessages(messageTypes) {
+ if (!messageTypes) {
+ return {
+ error: "missingParameter",
+ message: "The messageTypes parameter is missing.",
+ };
+ }
+
+ const messages = [];
+
+ const consoleServiceCachedMessages =
+ messageTypes.includes("PageError") || messageTypes.includes("LogMessage")
+ ? this.consoleServiceListener?.getCachedMessages(
+ !this.parentActor.isRootActor
+ )
+ : null;
+
+ for (const type of messageTypes) {
+ switch (type) {
+ case "ConsoleAPI": {
+ if (!this.consoleAPIListener) {
+ break;
+ }
+
+ // this.global might not be a window (can be a worker global or a Sandbox),
+ // and in such case performance isn't defined
+ const winStartTime =
+ this.global?.performance?.timing?.navigationStart;
+
+ const cache = this.consoleAPIListener.getCachedMessages(
+ !this.parentActor.isRootActor
+ );
+ cache.forEach(cachedMessage => {
+ // Filter out messages that came from a ServiceWorker but happened
+ // before the page was requested.
+ if (
+ cachedMessage.innerID === "ServiceWorker" &&
+ winStartTime > cachedMessage.timeStamp
+ ) {
+ return;
+ }
+
+ messages.push({
+ message: this.prepareConsoleMessageForRemote(cachedMessage),
+ type: "consoleAPICall",
+ });
+ });
+ break;
+ }
+
+ case "PageError": {
+ if (!consoleServiceCachedMessages) {
+ break;
+ }
+
+ for (const cachedMessage of consoleServiceCachedMessages) {
+ if (!(cachedMessage instanceof Ci.nsIScriptError)) {
+ continue;
+ }
+
+ messages.push({
+ pageError: this.preparePageErrorForRemote(cachedMessage),
+ type: "pageError",
+ });
+ }
+ break;
+ }
+
+ case "LogMessage": {
+ if (!consoleServiceCachedMessages) {
+ break;
+ }
+
+ for (const cachedMessage of consoleServiceCachedMessages) {
+ if (cachedMessage instanceof Ci.nsIScriptError) {
+ continue;
+ }
+
+ messages.push({
+ message: this._createStringGrip(cachedMessage.message),
+ timeStamp: cachedMessage.microSecondTimeStamp / 1000,
+ type: "logMessage",
+ });
+ }
+ break;
+ }
+ }
+ }
+
+ return {
+ messages,
+ };
+ }
+
+ /**
+ * Handler for the "evaluateJSAsync" request. This method evaluates a given
+ * JavaScript string with an associated `resultID`.
+ *
+ * The result will be returned later as an unsolicited `evaluationResult`,
+ * that can be associated back to this request via the `resultID` field.
+ *
+ * @param object request
+ * The JSON request object received from the Web Console client.
+ * @return object
+ * The response packet to send to with the unique id in the
+ * `resultID` field.
+ */
+ async evaluateJSAsync(request) {
+ const startTime = ChromeUtils.dateNow();
+ // Use a timestamp instead of a UUID as this code is used by workers, which
+ // don't have access to the UUID XPCOM component.
+ // Also use a counter in order to prevent mixing up response when calling
+ // at the exact same time.
+ const resultID = startTime + "-" + this._evalCounter++;
+
+ // Execute the evaluation in the next event loop in order to immediately
+ // reply with the resultID.
+ //
+ // The console input should be evaluated with micro task level != 0,
+ // so that microtask checkpoint isn't performed while evaluating it.
+ DevToolsUtils.executeSoonWithMicroTask(async () => {
+ try {
+ // Execute the script that may pause.
+ let response = await this.evaluateJS(request);
+ // Wait for any potential returned Promise.
+ response = await this._maybeWaitForResponseResult(response);
+
+ // Set the timestamp only now, so any messages logged in the expression (e.g. console.log)
+ // can be appended before the result message (unlike the evaluation result, other
+ // console resources are throttled before being handled by the webconsole client,
+ // which might cause some ordering issue).
+ // Use ChromeUtils.dateNow() as it gives us a higher precision than Date.now().
+ response.timestamp = ChromeUtils.dateNow();
+ // Finally, emit an unsolicited evaluationResult packet with the evaluation result.
+ this.emit("evaluationResult", {
+ type: "evaluationResult",
+ resultID,
+ startTime,
+ ...response,
+ });
+ } catch (e) {
+ const message = `Encountered error while waiting for Helper Result: ${e}\n${e.stack}`;
+ DevToolsUtils.reportException("evaluateJSAsync", Error(message));
+ }
+ });
+ return { resultID };
+ }
+
+ /**
+ * In order to support async evaluations (e.g. top-level await, …),
+ * we have to be able to handle promises. This method handles waiting for the promise,
+ * and then returns the result.
+ *
+ * @private
+ * @param object response
+ * The response packet to send to with the unique id in the
+ * `resultID` field, and potentially a promise in the `helperResult` or in the
+ * `awaitResult` field.
+ *
+ * @return object
+ * The updated response object.
+ */
+ async _maybeWaitForResponseResult(response) {
+ if (!response?.awaitResult) {
+ return response;
+ }
+
+ let result;
+ try {
+ result = await response.awaitResult;
+
+ // `createValueGrip` expect a debuggee value, while here we have the raw object.
+ // We need to call `makeDebuggeeValue` on it to make it work.
+ const dbgResult = this.makeDebuggeeValue(result);
+ response.result = this.createValueGrip(dbgResult);
+ } catch (e) {
+ // The promise was rejected. We let the engine handle this as it will report a
+ // `uncaught exception` error.
+ response.topLevelAwaitRejected = true;
+ }
+
+ // Remove the promise from the response object.
+ delete response.awaitResult;
+
+ return response;
+ }
+
+ /**
+ * Handler for the "evaluateJS" request. This method evaluates the given
+ * JavaScript string and sends back the result.
+ *
+ * @param object request
+ * The JSON request object received from the Web Console client.
+ * @return object
+ * The evaluation response packet.
+ */
+ evaluateJS(request) {
+ const input = request.text;
+
+ const evalOptions = {
+ frameActor: request.frameActor,
+ url: request.url,
+ innerWindowID: request.innerWindowID,
+ selectedNodeActor: request.selectedNodeActor,
+ selectedObjectActor: request.selectedObjectActor,
+ eager: request.eager,
+ bindings: request.bindings,
+ lineNumber: request.lineNumber,
+ // This flag is set to true in most cases as we consider most evaluations as internal and:
+ // * prevent any breakpoint from being triggerred when evaluating the JS input
+ // * prevent spawning Debugger.Source for the evaluated JS and showing it in Debugger UI
+ // This is only set to false when evaluating the console input.
+ disableBreaks: !!request.disableBreaks,
+ // Optional flag, to be set to true when Console Commands should override local symbols with
+ // the same name. Like if the page defines `$`, the evaluated string will use the `$` implemented
+ // by the console command instead of the page's function.
+ preferConsoleCommandsOverLocalSymbols:
+ !!request.preferConsoleCommandsOverLocalSymbols,
+ };
+
+ const { mapped } = request;
+
+ // Set a flag on the thread actor which indicates an evaluation is being
+ // done for the client. This is used to disable all types of breakpoints for all sources
+ // via `disabledBreaks`. When this flag is used, `reportExceptionsWhenBreaksAreDisabled`
+ // allows to still pause on exceptions.
+ this.parentActor.threadActor.insideClientEvaluation = evalOptions;
+
+ let evalInfo;
+ try {
+ evalInfo = evalWithDebugger(input, evalOptions, this);
+ } finally {
+ this.parentActor.threadActor.insideClientEvaluation = null;
+ }
+
+ return new Promise((resolve, reject) => {
+ // Queue up a task to run in the next tick so any microtask created by the evaluated
+ // expression has the time to be run.
+ // e.g. in :
+ // ```
+ // const promiseThenCb = result => "result: " + result;
+ // new Promise(res => res("hello")).then(promiseThenCb)
+ // ```
+ // we want`promiseThenCb` to have run before handling the result.
+ DevToolsUtils.executeSoon(() => {
+ try {
+ const result = this.prepareEvaluationResult(
+ evalInfo,
+ input,
+ request.eager,
+ mapped
+ );
+ resolve(result);
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+ }
+
+ // eslint-disable-next-line complexity
+ prepareEvaluationResult(evalInfo, input, eager, mapped) {
+ const evalResult = evalInfo.result;
+ const helperResult = evalInfo.helperResult;
+
+ let result,
+ errorDocURL,
+ errorMessage,
+ errorNotes = null,
+ errorGrip = null,
+ frame = null,
+ awaitResult,
+ errorMessageName,
+ exceptionStack;
+ if (evalResult) {
+ if ("return" in evalResult) {
+ result = evalResult.return;
+ if (
+ mapped?.await &&
+ result &&
+ result.class === "Promise" &&
+ typeof result.unsafeDereference === "function"
+ ) {
+ awaitResult = result.unsafeDereference();
+ }
+ } else if ("yield" in evalResult) {
+ result = evalResult.yield;
+ } else if ("throw" in evalResult) {
+ const error = evalResult.throw;
+ errorGrip = this.createValueGrip(error);
+
+ exceptionStack = this.prepareStackForRemote(evalResult.stack);
+
+ if (exceptionStack) {
+ // Set the frame based on the topmost stack frame for the exception.
+ const {
+ filename: source,
+ sourceId,
+ lineNumber: line,
+ columnNumber: column,
+ } = exceptionStack[0];
+ frame = { source, sourceId, line, column };
+
+ exceptionStack =
+ WebConsoleUtils.removeFramesAboveDebuggerEval(exceptionStack);
+ }
+
+ errorMessage = String(error);
+ if (typeof error === "object" && error !== null) {
+ try {
+ errorMessage = DevToolsUtils.callPropertyOnObject(
+ error,
+ "toString"
+ );
+ } catch (e) {
+ // If the debuggee is not allowed to access the "toString" property
+ // of the error object, calling this property from the debuggee's
+ // compartment will fail. The debugger should show the error object
+ // as it is seen by the debuggee, so this behavior is correct.
+ //
+ // Unfortunately, we have at least one test that assumes calling the
+ // "toString" property of an error object will succeed if the
+ // debugger is allowed to access it, regardless of whether the
+ // debuggee is allowed to access it or not.
+ //
+ // To accomodate these tests, if calling the "toString" property
+ // from the debuggee compartment fails, we rewrap the error object
+ // in the debugger's compartment, and then call the "toString"
+ // property from there.
+ if (typeof error.unsafeDereference === "function") {
+ const rawError = error.unsafeDereference();
+ errorMessage = rawError ? rawError.toString() : "";
+ }
+ }
+ }
+
+ // It is possible that we won't have permission to unwrap an
+ // object and retrieve its errorMessageName.
+ try {
+ errorDocURL = ErrorDocs.GetURL(error);
+ errorMessageName = error.errorMessageName;
+ } catch (ex) {
+ // ignored
+ }
+
+ try {
+ const line = error.errorLineNumber;
+ const column = error.errorColumnNumber;
+
+ if (typeof line === "number" && typeof column === "number") {
+ // Set frame only if we have line/column numbers.
+ frame = {
+ source: "debugger eval code",
+ line,
+ column,
+ };
+ }
+ } catch (ex) {
+ // ignored
+ }
+
+ try {
+ const notes = error.errorNotes;
+ if (notes?.length) {
+ errorNotes = [];
+ for (const note of notes) {
+ errorNotes.push({
+ messageBody: this._createStringGrip(note.message),
+ frame: {
+ source: note.fileName,
+ line: note.lineNumber,
+ column: note.columnNumber,
+ },
+ });
+ }
+ }
+ } catch (ex) {
+ // ignored
+ }
+ }
+ }
+
+ // If a value is encountered that the devtools server doesn't support yet,
+ // the console should remain functional.
+ let resultGrip;
+ if (!awaitResult) {
+ try {
+ const objectActor =
+ this.parentActor.threadActor.getThreadLifetimeObject(result);
+ if (objectActor) {
+ resultGrip = this.parentActor.threadActor.createValueGrip(result);
+ } else {
+ resultGrip = this.createValueGrip(result);
+ }
+ } catch (e) {
+ errorMessage = e;
+ }
+ }
+
+ // Don't update _lastConsoleInputEvaluation in eager evaluation, as it would interfere
+ // with the $_ command.
+ if (!eager) {
+ if (!awaitResult) {
+ this._lastConsoleInputEvaluation = result;
+ } else {
+ // If we evaluated a top-level await expression, we want to assign its result to the
+ // _lastConsoleInputEvaluation only when the promise resolves, and only if it
+ // resolves. If the promise rejects, we don't re-assign _lastConsoleInputEvaluation,
+ // it will keep its previous value.
+
+ const p = awaitResult.then(res => {
+ this._lastConsoleInputEvaluation = this.makeDebuggeeValue(res);
+ });
+
+ // If the top level await was already rejected (e.g. `await Promise.reject("bleh")`),
+ // catch the resulting promise of awaitResult.then.
+ // If we don't do that, the new Promise will also be rejected, and since it's
+ // unhandled, it will generate an error.
+ // We don't want to do that for pending promise (e.g. `await new Promise((res, rej) => setTimeout(rej,250))`),
+ // as the the Promise rejection will be considered as handled, and the "Uncaught (in promise)"
+ // message wouldn't be emitted.
+ const { state } = ObjectUtils.getPromiseState(evalResult.return);
+ if (state === "rejected") {
+ p.catch(() => {});
+ }
+ }
+ }
+
+ return {
+ input,
+ result: resultGrip,
+ awaitResult,
+ exception: errorGrip,
+ exceptionMessage: this._createStringGrip(errorMessage),
+ exceptionDocURL: errorDocURL,
+ exceptionStack,
+ hasException: errorGrip !== null,
+ errorMessageName,
+ frame,
+ helperResult,
+ notes: errorNotes,
+ };
+ }
+
+ /**
+ * The Autocomplete request handler.
+ *
+ * @param string text
+ * The request message - what input to autocomplete.
+ * @param number cursor
+ * The cursor position at the moment of starting autocomplete.
+ * @param string frameActor
+ * The frameactor id of the current paused frame.
+ * @param string selectedNodeActor
+ * The actor id of the currently selected node.
+ * @param array authorizedEvaluations
+ * Array of the properties access which can be executed by the engine.
+ * @return object
+ * The response message - matched properties.
+ */
+ autocomplete(
+ text,
+ cursor,
+ frameActorId,
+ selectedNodeActor,
+ authorizedEvaluations,
+ expressionVars = []
+ ) {
+ let dbgObject = null;
+ let environment = null;
+ let matches = [];
+ let matchProp;
+ let isElementAccess;
+
+ const reqText = text.substr(0, cursor);
+
+ if (isCommand(reqText)) {
+ matchProp = reqText;
+ matches = WebConsoleCommandsManager.getAllColonCommandNames()
+ .filter(c => `:${c}`.startsWith(reqText))
+ .map(c => `:${c}`);
+ } else {
+ // This is the case of the paused debugger
+ if (frameActorId) {
+ const frameActor = this.conn.getActor(frameActorId);
+ try {
+ // Need to try/catch since accessing frame.environment
+ // can throw "Debugger.Frame is not live"
+ const frame = frameActor.frame;
+ environment = frame.environment;
+ } catch (e) {
+ DevToolsUtils.reportException(
+ "autocomplete",
+ Error("The frame actor was not found: " + frameActorId)
+ );
+ }
+ } else {
+ dbgObject = this.dbg.addDebuggee(this.evalGlobal);
+ }
+
+ const result = jsPropertyProvider({
+ dbgObject,
+ environment,
+ frameActorId,
+ inputValue: text,
+ cursor,
+ webconsoleActor: this,
+ selectedNodeActor,
+ authorizedEvaluations,
+ expressionVars,
+ });
+
+ if (result === null) {
+ return {
+ matches: null,
+ };
+ }
+
+ if (result && result.isUnsafeGetter === true) {
+ return {
+ isUnsafeGetter: true,
+ getterPath: result.getterPath,
+ };
+ }
+
+ matches = result.matches || new Set();
+ matchProp = result.matchProp || "";
+ isElementAccess = result.isElementAccess;
+
+ // We consider '$' as alphanumeric because it is used in the names of some
+ // helper functions; we also consider whitespace as alphanum since it should not
+ // be seen as break in the evaled string.
+ const lastNonAlphaIsDot = /[.][a-zA-Z0-9$\s]*$/.test(reqText);
+
+ // We only return commands and keywords when we are not dealing with a property or
+ // element access.
+ if (matchProp && !lastNonAlphaIsDot && !isElementAccess) {
+ const colonOnlyCommands =
+ WebConsoleCommandsManager.getColonOnlyCommandNames();
+ for (const name of WebConsoleCommandsManager.getAllCommandNames()) {
+ // Filter out commands like `screenshot` as it is inaccessible without the `:` prefix
+ if (
+ !colonOnlyCommands.includes(name) &&
+ name.startsWith(result.matchProp)
+ ) {
+ matches.add(name);
+ }
+ }
+
+ for (const keyword of RESERVED_JS_KEYWORDS) {
+ if (keyword.startsWith(result.matchProp)) {
+ matches.add(keyword);
+ }
+ }
+ }
+
+ // Sort the results in order to display lowercased item first (e.g. we want to
+ // display `document` then `Document` as we loosely match the user input if the
+ // first letter was lowercase).
+ const firstMeaningfulCharIndex = isElementAccess ? 1 : 0;
+ matches = Array.from(matches).sort((a, b) => {
+ const aFirstMeaningfulChar = a[firstMeaningfulCharIndex];
+ const bFirstMeaningfulChar = b[firstMeaningfulCharIndex];
+ const lA =
+ aFirstMeaningfulChar.toLocaleLowerCase() === aFirstMeaningfulChar;
+ const lB =
+ bFirstMeaningfulChar.toLocaleLowerCase() === bFirstMeaningfulChar;
+ if (lA === lB) {
+ if (a === matchProp) {
+ return -1;
+ }
+ if (b === matchProp) {
+ return 1;
+ }
+ return a.localeCompare(b);
+ }
+ return lA ? -1 : 1;
+ });
+ }
+
+ return {
+ matches,
+ matchProp,
+ isElementAccess: isElementAccess === true,
+ };
+ }
+
+ /**
+ * The "clearMessagesCacheAsync" request handler.
+ */
+ clearMessagesCacheAsync() {
+ if (isWorker) {
+ // Defined on WorkerScope
+ clearConsoleEvents();
+ return;
+ }
+
+ const windowId = !this.parentActor.isRootActor
+ ? WebConsoleUtils.getInnerWindowId(this.global)
+ : null;
+
+ const ConsoleAPIStorage = Cc[
+ "@mozilla.org/consoleAPI-storage;1"
+ ].getService(Ci.nsIConsoleAPIStorage);
+ ConsoleAPIStorage.clearEvents(windowId);
+
+ CONSOLE_WORKER_IDS.forEach(id => {
+ ConsoleAPIStorage.clearEvents(id);
+ });
+
+ if (this.parentActor.isRootActor || !this.global) {
+ // If were dealing with the root actor (e.g. the browser console), we want
+ // to remove all cached messages, not only the ones specific to a window.
+ Services.console.reset();
+ } else if (this.parentActor.ignoreSubFrames) {
+ Services.console.resetWindow(windowId);
+ } else {
+ WebConsoleUtils.getInnerWindowIDsForFrames(this.global).forEach(id =>
+ Services.console.resetWindow(id)
+ );
+ }
+ }
+
+ // End of request handlers.
+
+ // Event handlers for various listeners.
+
+ /**
+ * Handler for messages received from the ConsoleServiceListener. This method
+ * sends the nsIConsoleMessage to the remote Web Console client.
+ *
+ * @param nsIConsoleMessage message
+ * The message we need to send to the client.
+ */
+ onConsoleServiceMessage(message) {
+ if (message instanceof Ci.nsIScriptError) {
+ this.emit("pageError", {
+ pageError: this.preparePageErrorForRemote(message),
+ });
+ } else {
+ this.emit("logMessage", {
+ message: this._createStringGrip(message.message),
+ timeStamp: message.microSecondTimeStamp / 1000,
+ });
+ }
+ }
+
+ getActorIdForInternalSourceId(id) {
+ const actor =
+ this.parentActor.sourcesManager.getSourceActorByInternalSourceId(id);
+ return actor ? actor.actorID : null;
+ }
+
+ /**
+ * Prepare a SavedFrame stack to be sent to the client.
+ *
+ * @param SavedFrame errorStack
+ * Stack for an error we need to send to the client.
+ * @return object
+ * The object you can send to the remote client.
+ */
+ prepareStackForRemote(errorStack) {
+ // Convert stack objects to the JSON attributes expected by client code
+ // Bug 1348885: If the global from which this error came from has been
+ // nuked, stack is going to be a dead wrapper.
+ if (!errorStack || (Cu && Cu.isDeadWrapper(errorStack))) {
+ return null;
+ }
+ const stack = [];
+ let s = errorStack;
+ while (s) {
+ stack.push({
+ filename: s.source,
+ sourceId: this.getActorIdForInternalSourceId(s.sourceId),
+ lineNumber: s.line,
+ columnNumber: s.column,
+ functionName: s.functionDisplayName,
+ asyncCause: s.asyncCause ? s.asyncCause : undefined,
+ });
+ s = s.parent || s.asyncParent;
+ }
+ return stack;
+ }
+
+ /**
+ * Prepare an nsIScriptError to be sent to the client.
+ *
+ * @param nsIScriptError pageError
+ * The page error we need to send to the client.
+ * @return object
+ * The object you can send to the remote client.
+ */
+ preparePageErrorForRemote(pageError) {
+ const stack = this.prepareStackForRemote(pageError.stack);
+ let lineText = pageError.sourceLine;
+ if (
+ lineText &&
+ lineText.length > DevToolsServer.LONG_STRING_INITIAL_LENGTH
+ ) {
+ lineText = lineText.substr(0, DevToolsServer.LONG_STRING_INITIAL_LENGTH);
+ }
+
+ let notesArray = null;
+ const notes = pageError.notes;
+ if (notes?.length) {
+ notesArray = [];
+ for (let i = 0, len = notes.length; i < len; i++) {
+ const note = notes.queryElementAt(i, Ci.nsIScriptErrorNote);
+ notesArray.push({
+ messageBody: this._createStringGrip(note.errorMessage),
+ frame: {
+ source: note.sourceName,
+ sourceId: this.getActorIdForInternalSourceId(note.sourceId),
+ line: note.lineNumber,
+ column: note.columnNumber,
+ },
+ });
+ }
+ }
+
+ // If there is no location information in the error but we have a stack,
+ // fill in the location with the first frame on the stack.
+ let { sourceName, sourceId, lineNumber, columnNumber } = pageError;
+ if (!sourceName && !sourceId && !lineNumber && !columnNumber && stack) {
+ sourceName = stack[0].filename;
+ sourceId = stack[0].sourceId;
+ lineNumber = stack[0].lineNumber;
+ columnNumber = stack[0].columnNumber;
+ }
+
+ const isCSSMessage = pageError.category === MESSAGE_CATEGORY.CSS_PARSER;
+
+ const result = {
+ errorMessage: this._createStringGrip(pageError.errorMessage),
+ errorMessageName: isCSSMessage ? undefined : pageError.errorMessageName,
+ exceptionDocURL: ErrorDocs.GetURL(pageError),
+ sourceName,
+ sourceId: this.getActorIdForInternalSourceId(sourceId),
+ lineText,
+ lineNumber,
+ columnNumber,
+ category: pageError.category,
+ innerWindowID: pageError.innerWindowID,
+ timeStamp: pageError.microSecondTimeStamp / 1000,
+ warning: !!(pageError.flags & pageError.warningFlag),
+ error: !(pageError.flags & (pageError.warningFlag | pageError.infoFlag)),
+ info: !!(pageError.flags & pageError.infoFlag),
+ private: pageError.isFromPrivateWindow,
+ stacktrace: stack,
+ notes: notesArray,
+ chromeContext: pageError.isFromChromeContext,
+ isPromiseRejection: isCSSMessage
+ ? undefined
+ : pageError.isPromiseRejection,
+ isForwardedFromContentProcess: pageError.isForwardedFromContentProcess,
+ cssSelectors: isCSSMessage ? pageError.cssSelectors : undefined,
+ };
+
+ // If the pageError does have an exception object, we want to return the grip for it,
+ // but only if we do manage to get the grip, as we're checking the property on the
+ // client to render things differently.
+ if (pageError.hasException) {
+ try {
+ const obj = this.makeDebuggeeValue(pageError.exception, true);
+ if (obj?.class !== "DeadObject") {
+ result.exception = this.createValueGrip(obj);
+ result.hasException = true;
+ }
+ } catch (e) {}
+ }
+
+ return result;
+ }
+
+ /**
+ * Handler for window.console API calls received from the ConsoleAPIListener.
+ * This method sends the object to the remote Web Console client.
+ *
+ * @see ConsoleAPIListener
+ * @param object message
+ * The console API call we need to send to the remote client.
+ * @param object extraProperties
+ * an object whose properties will be folded in the packet that is emitted.
+ */
+ onConsoleAPICall(message, extraProperties = {}) {
+ this.emit("consoleAPICall", {
+ message: this.prepareConsoleMessageForRemote(message),
+ ...extraProperties,
+ });
+ }
+
+ /**
+ * Handler for the DocumentEventsListener.
+ *
+ * @see DocumentEventsListener
+ * @param {String} name
+ * The document event name that either of followings.
+ * - dom-loading
+ * - dom-interactive
+ * - dom-complete
+ * @param {Number} time
+ * The time that the event is fired.
+ * @param {Boolean} hasNativeConsoleAPI
+ * Tells if the window.console object is native or overwritten by script in the page.
+ * Only passed when `name` is "dom-complete" (see devtools/server/actors/webconsole/listeners/document-events.js).
+ */
+ onDocumentEvent(name, { time, hasNativeConsoleAPI }) {
+ this.emit("documentEvent", {
+ name,
+ time,
+ hasNativeConsoleAPI,
+ });
+ }
+
+ /**
+ * Handler for file activity. This method sends the file request information
+ * to the remote Web Console client.
+ *
+ * @see ConsoleFileActivityListener
+ * @param string fileURI
+ * The requested file URI.
+ */
+ onFileActivity(fileURI) {
+ this.emit("fileActivity", {
+ uri: fileURI,
+ });
+ }
+
+ // End of event handlers for various listeners.
+
+ /**
+ * Prepare a message from the console API to be sent to the remote Web Console
+ * instance.
+ *
+ * @param object message
+ * The original message received from the console storage listener.
+ * @param boolean aUseObjectGlobal
+ * If |true| the object global is determined and added as a debuggee,
+ * otherwise |this.global| is used when makeDebuggeeValue() is invoked.
+ * @return object
+ * The object that can be sent to the remote client.
+ */
+ prepareConsoleMessageForRemote(message, useObjectGlobal = true) {
+ const result = {
+ arguments: message.arguments
+ ? message.arguments.map(obj => {
+ const dbgObj = this.makeDebuggeeValue(obj, useObjectGlobal);
+ return this.createValueGrip(dbgObj);
+ })
+ : [],
+ chromeContext: message.chromeContext,
+ 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,
+ sourceId: this.getActorIdForInternalSourceId(message.sourceId),
+ category: message.category || "webdev",
+ innerWindowID: message.innerID,
+ };
+
+ // 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.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: this.getActorIdForInternalSourceId(frame.sourceId),
+ };
+ });
+ }
+
+ if (message.styles && message.styles.length) {
+ result.styles = message.styles.map(string => {
+ return this.createValueGrip(string);
+ });
+ }
+
+ if (message.timer) {
+ result.timer = message.timer;
+ }
+
+ if (message.level === "table") {
+ const tableItems = this._getConsoleTableMessageItems(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);
+ }
+
+ return result;
+ }
+
+ /**
+ * 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 {Object} result: The console.table message.
+ * @returns {Object} An object containing the properties of the first argument of the
+ * console.table call.
+ */
+ _getConsoleTableMessageItems(result) {
+ if (
+ !result ||
+ !Array.isArray(result.arguments) ||
+ !result.arguments.length
+ ) {
+ return null;
+ }
+
+ const [tableItemGrip] = result.arguments;
+ const dataType = tableItemGrip.class;
+ const needEntries = ["Map", "WeakMap", "Set", "WeakSet"].includes(dataType);
+ const ignoreNonIndexedProperties = isArray(tableItemGrip);
+
+ const tableItemActor = this.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 && this.getActorByID(grip.actor);
+ if (actor) {
+ const res = actor
+ .enumProperties({
+ ignoreNonIndexedProperties: isArray(grip),
+ })
+ .all();
+ if (res?.ownProperties) {
+ desc[key].ownProperties = res.ownProperties;
+ }
+ }
+ }
+ });
+ }
+ });
+
+ return ownProperties;
+ }
+
+ /**
+ * The "will-navigate" progress listener. This is used to clear the current
+ * eval scope.
+ */
+ _onWillNavigate({ window, isTopLevel }) {
+ if (isTopLevel) {
+ this._evalGlobal = null;
+ EventEmitter.off(this.parentActor, "will-navigate", this._onWillNavigate);
+ this._progressListenerActive = false;
+ }
+ }
+
+ /**
+ * This listener is called when we switch to another frame,
+ * mostly to unregister previous listeners and start listening on the new document.
+ */
+ _onChangedToplevelDocument() {
+ // Convert the Set to an Array
+ const listeners = [...this._listeners];
+
+ // Unregister existing listener on the previous document
+ // (pass a copy of the array as it will shift from it)
+ this.stopListeners(listeners.slice());
+
+ // This method is called after this.global is changed,
+ // so we register new listener on this new global
+ this.startListeners(listeners);
+
+ // Also reset the cached top level chrome window being targeted
+ this._lastChromeWindow = null;
+ }
+}
+
+exports.WebConsoleActor = WebConsoleActor;