/* 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/. */ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs", }); XPCOMUtils.defineLazyGetter(lazy, "ConsoleAPIStorage", () => { return Cc["@mozilla.org/consoleAPI-storage;1"].getService( Ci.nsIConsoleAPIStorage ); }); /** * The ConsoleAPIListener can be used to listen for messages coming from console * API usage in a given windowGlobal, eg. console.log, console.error, ... * * Example: * ``` * const listener = new ConsoleAPIListener(innerWindowId); * listener.on("message", onConsoleAPIMessage); * listener.startListening(); * * const onConsoleAPIMessage = (eventName, data = {}) => { * const { arguments: msgArguments, level, stacktrace, timeStamp } = data; * ... * }; * ``` * * @fires message * The ConsoleAPIListener emits "message" events, with the following object as * payload: * - {Array} arguments - Arguments as passed-in when the method was called. * - {String} level - Importance, one of `info`, `warn`, `error`, `debug`, `trace`. * - {Array} stacktrace - List of stack frames, starting from most recent. * - {Number} timeStamp - Timestamp when the method was called. */ export class ConsoleAPIListener { #emittedMessages; #innerWindowId; #listening; /** * Create a new ConsolerListener instance. * * @param {number} innerWindowId * The inner window id to filter the messages for. */ constructor(innerWindowId) { lazy.EventEmitter.decorate(this); this.#emittedMessages = new Set(); this.#innerWindowId = innerWindowId; this.#listening = false; } destroy() { this.stopListening(); this.#emittedMessages = null; } startListening() { if (this.#listening) { return; } lazy.ConsoleAPIStorage.addLogEventListener( this.#onConsoleAPIMessage, Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal) ); // Emit cached messages after registering the listener, to make sure we // don't miss any message. this.#emitCachedMessages(); this.#listening = true; } stopListening() { if (!this.#listening) { return; } lazy.ConsoleAPIStorage.removeLogEventListener(this.#onConsoleAPIMessage); this.#listening = false; } #emitCachedMessages() { const cachedMessages = lazy.ConsoleAPIStorage.getEvents( this.#innerWindowId ); for (const message of cachedMessages) { this.#onConsoleAPIMessage(message); } } #onConsoleAPIMessage = message => { const messageObject = message.wrappedJSObject; // Bail if this message was already emitted, useful to filter out cached // messages already received by the consumer. if (this.#emittedMessages.has(messageObject)) { return; } this.#emittedMessages.add(messageObject); if (messageObject.innerID !== this.#innerWindowId) { // If the message doesn't match the innerWindowId of the current context // ignore it. return; } this.emit("message", { arguments: messageObject.arguments, level: messageObject.level, stacktrace: messageObject.stacktrace, timeStamp: messageObject.timeStamp, }); }; }