Source code
Revision control
Copy as Markdown
Other Tools
/* 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
import { WindowGlobalBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/WindowGlobalBiDiModule.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ConsoleAPIListener:
"chrome://remote/content/shared/listeners/ConsoleAPIListener.sys.mjs",
ConsoleListener:
"chrome://remote/content/shared/listeners/ConsoleListener.sys.mjs",
isChromeFrame: "chrome://remote/content/shared/Stack.sys.mjs",
OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
setDefaultSerializationOptions:
"chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs",
});
class LogModule extends WindowGlobalBiDiModule {
#consoleAPIListener;
#consoleMessageListener;
#subscribedEvents;
constructor(messageHandler) {
super(messageHandler);
// Create the console-api listener and listen on "message" events.
this.#consoleAPIListener = new lazy.ConsoleAPIListener(
this.messageHandler.innerWindowId
);
this.#consoleAPIListener.on("message", this.#onConsoleAPIMessage);
// Create the console listener and listen on error messages.
this.#consoleMessageListener = new lazy.ConsoleListener(
this.messageHandler.innerWindowId
);
this.#consoleMessageListener.on("error", this.#onJavaScriptError);
// Set of event names which have active subscriptions.
this.#subscribedEvents = new Set();
}
destroy() {
this.#consoleAPIListener.off("message", this.#onConsoleAPIMessage);
this.#consoleAPIListener.destroy();
this.#consoleMessageListener.off("error", this.#onJavaScriptError);
this.#consoleMessageListener.destroy();
this.#subscribedEvents = null;
}
#buildSource(realm) {
return {
realm: realm.id,
context: this.messageHandler.context,
};
}
/**
* Map the internal stacktrace representation to a WebDriver BiDi
* compatible one.
*
* Currently chrome frames will be filtered out until chrome scope
*
* @param {Array<StackFrame>=} stackTrace
* Stack frames to process.
*
* @returns {object=} Object, containing the list of frames as `callFrames`.
*/
#buildStackTrace(stackTrace) {
if (stackTrace == undefined) {
return undefined;
}
const callFrames = stackTrace
.filter(frame => !lazy.isChromeFrame(frame))
.map(frame => {
return {
columnNumber: frame.columnNumber - 1,
functionName: frame.functionName,
lineNumber: frame.lineNumber - 1,
url: frame.filename,
};
});
return { callFrames };
}
#getLogEntryLevelFromConsoleMethod(method) {
switch (method) {
case "assert":
case "error":
return "error";
case "debug":
case "trace":
return "debug";
case "warn":
return "warn";
default:
return "info";
}
}
#onConsoleAPIMessage = (eventName, data = {}) => {
const {
// `arguments` cannot be used as variable name in functions
arguments: messageArguments,
// `level` corresponds to the console method used
level: method,
stacktrace,
timeStamp,
} = data;
// Step numbers below refer to the specifications at
// Translate the console message method to a log.LogEntry level
const logEntrylevel = this.#getLogEntryLevelFromConsoleMethod(method);
// Use the message's timeStamp or fallback on the current time value.
const timestamp = timeStamp || Date.now();
// Start assembling the text representation of the message.
let text = "";
// Formatters have already been applied at this points.
// message.arguments corresponds to the "formatted args" from the
// specifications.
// Concatenate all formatted arguments in text
// TODO: For m1 we only support string arguments, so we rely on the builtin
// toString for each argument which will be available in message.arguments.
const args = messageArguments || [];
text += args.map(String).join(" ");
const defaultRealm = this.messageHandler.getRealm();
const serializedArgs = [];
const seenNodeIds = new Map();
// Serialize each arg as remote value.
for (const arg of args) {
// Note that we can pass a default realm for now since realms are only
// involved when creating object references, which will not happen with
serializedArgs.push(
this.serialize(
Cu.waiveXrays(arg),
lazy.setDefaultSerializationOptions(),
lazy.OwnershipModel.None,
defaultRealm,
{ seenNodeIds }
)
);
}
// Set source to an object which contains realm and browsing context.
const source = this.#buildSource(defaultRealm);
// Set stack trace only for certain methods.
let stackTrace;
if (["assert", "error", "trace", "warn"].includes(method)) {
stackTrace = this.#buildStackTrace(stacktrace);
}
// Build the ConsoleLogEntry
const entry = {
type: "console",
method,
source,
args: serializedArgs,
level: logEntrylevel,
text,
timestamp,
stackTrace,
_extraData: { seenNodeIds },
};
// TODO: Those steps relate to:
// - emitting associated BrowsingContext. See log.entryAdded full support
// - handling cases where session doesn't exist or the event is not
// monitored. The implementation differs from the spec here because we
// only react to events if there is a session & if the session subscribed
// to those events.
this.emitEvent("log.entryAdded", entry);
};
#onJavaScriptError = (eventName, data = {}) => {
const { level, message, stacktrace, timeStamp } = data;
const defaultRealm = this.messageHandler.getRealm();
// Build the JavascriptLogEntry
const entry = {
type: "javascript",
level,
source: this.#buildSource(defaultRealm),
text: message,
timestamp: timeStamp || Date.now(),
stackTrace: this.#buildStackTrace(stacktrace),
};
this.emitEvent("log.entryAdded", entry);
};
#subscribeEvent(event) {
if (event === "log.entryAdded") {
this.#consoleAPIListener.startListening();
this.#consoleMessageListener.startListening();
this.#subscribedEvents.add(event);
}
}
#unsubscribeEvent(event) {
if (event === "log.entryAdded") {
this.#consoleAPIListener.stopListening();
this.#consoleMessageListener.stopListening();
this.#subscribedEvents.delete(event);
}
}
/**
* Internal commands
*/
_applySessionData(params) {
// class.
const { category } = params;
if (category === "event") {
const filteredSessionData = params.sessionData.filter(item =>
this.messageHandler.matchesContext(item.contextDescriptor)
);
for (const event of this.#subscribedEvents.values()) {
const hasSessionItem = filteredSessionData.some(
item => item.value === event
);
// If there are no session items for this context, we should unsubscribe from the event.
if (!hasSessionItem) {
this.#unsubscribeEvent(event);
}
}
// Subscribe to all events, which have an item in SessionData.
for (const { value } of filteredSessionData) {
this.#subscribeEvent(value);
}
}
}
}
export const log = LogModule;