log.sys.mjs (8068B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { WindowGlobalBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/WindowGlobalBiDiModule.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 ConsoleAPIListener: 11 "chrome://remote/content/shared/listeners/ConsoleAPIListener.sys.mjs", 12 ConsoleListener: 13 "chrome://remote/content/shared/listeners/ConsoleListener.sys.mjs", 14 isChromeFrame: "chrome://remote/content/shared/Stack.sys.mjs", 15 OwnershipModel: "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs", 16 setDefaultSerializationOptions: 17 "chrome://remote/content/webdriver-bidi/RemoteValue.sys.mjs", 18 }); 19 20 class LogModule extends WindowGlobalBiDiModule { 21 #consoleAPIListener; 22 #consoleMessageListener; 23 #subscribedEvents; 24 25 constructor(messageHandler) { 26 super(messageHandler); 27 28 // Create the console-api listener and listen on "message" events. 29 this.#consoleAPIListener = new lazy.ConsoleAPIListener( 30 this.messageHandler.innerWindowId 31 ); 32 this.#consoleAPIListener.on("message", this.#onConsoleAPIMessage); 33 34 // Create the console listener and listen on error messages. 35 this.#consoleMessageListener = new lazy.ConsoleListener( 36 this.messageHandler.innerWindowId 37 ); 38 this.#consoleMessageListener.on("error", this.#onJavaScriptError); 39 40 // Set of event names which have active subscriptions. 41 this.#subscribedEvents = new Set(); 42 } 43 44 destroy() { 45 this.#consoleAPIListener.off("message", this.#onConsoleAPIMessage); 46 this.#consoleAPIListener.destroy(); 47 this.#consoleMessageListener.off("error", this.#onJavaScriptError); 48 this.#consoleMessageListener.destroy(); 49 50 this.#subscribedEvents = null; 51 } 52 53 #buildSource(realm) { 54 return { 55 realm: realm.id, 56 context: this.messageHandler.context, 57 }; 58 } 59 60 /** 61 * Map the internal stacktrace representation to a WebDriver BiDi 62 * compatible one. 63 * 64 * Currently chrome frames will be filtered out until chrome scope 65 * is supported (bug 1722679). 66 * 67 * @param {Array<StackFrame>=} stackTrace 68 * Stack frames to process. 69 * 70 * @returns {object=} Object, containing the list of frames as `callFrames`. 71 */ 72 #buildStackTrace(stackTrace) { 73 if (stackTrace == undefined) { 74 return undefined; 75 } 76 77 const callFrames = stackTrace 78 .filter(frame => !lazy.isChromeFrame(frame)) 79 .map(frame => { 80 return { 81 columnNumber: frame.columnNumber - 1, 82 functionName: frame.functionName, 83 lineNumber: frame.lineNumber - 1, 84 url: frame.filename, 85 }; 86 }); 87 88 return { callFrames }; 89 } 90 91 #getLogEntryLevelFromConsoleMethod(method) { 92 switch (method) { 93 case "assert": 94 case "error": 95 return "error"; 96 case "debug": 97 case "trace": 98 return "debug"; 99 case "warn": 100 return "warn"; 101 default: 102 return "info"; 103 } 104 } 105 106 #onConsoleAPIMessage = (eventName, data = {}) => { 107 const { 108 // `arguments` cannot be used as variable name in functions 109 arguments: messageArguments, 110 // `level` corresponds to the console method used 111 level: method, 112 stacktrace, 113 timeStamp, 114 } = data; 115 116 // Step numbers below refer to the specifications at 117 // https://w3c.github.io/webdriver-bidi/#event-log-entryAdded 118 119 // Translate the console message method to a log.LogEntry level 120 const logEntrylevel = this.#getLogEntryLevelFromConsoleMethod(method); 121 122 // Use the message's timeStamp or fallback on the current time value. 123 const timestamp = timeStamp || Date.now(); 124 125 // Start assembling the text representation of the message. 126 let text = ""; 127 128 // Formatters have already been applied at this points. 129 // message.arguments corresponds to the "formatted args" from the 130 // specifications. 131 132 // Concatenate all formatted arguments in text 133 // TODO: For m1 we only support string arguments, so we rely on the builtin 134 // toString for each argument which will be available in message.arguments. 135 const args = messageArguments || []; 136 text += args.map(String).join(" "); 137 138 const defaultRealm = this.messageHandler.getRealm(); 139 const serializedArgs = []; 140 const seenNodeIds = new Map(); 141 142 // Serialize each arg as remote value. 143 for (const arg of args) { 144 // Note that we can pass a default realm for now since realms are only 145 // involved when creating object references, which will not happen with 146 // OwnershipModel.None. This will be revisited in Bug 1742589. 147 serializedArgs.push( 148 this.serialize( 149 Cu.waiveXrays(arg), 150 lazy.setDefaultSerializationOptions(), 151 lazy.OwnershipModel.None, 152 defaultRealm, 153 { seenNodeIds } 154 ) 155 ); 156 } 157 158 // Set source to an object which contains realm and browsing context. 159 // TODO: Bug 1742589. Use an actual realm from which the event came from. 160 const source = this.#buildSource(defaultRealm); 161 162 // Set stack trace only for certain methods. 163 let stackTrace; 164 if (["assert", "error", "trace", "warn"].includes(method)) { 165 stackTrace = this.#buildStackTrace(stacktrace); 166 } 167 168 // Build the ConsoleLogEntry 169 const entry = { 170 type: "console", 171 method, 172 source, 173 args: serializedArgs, 174 level: logEntrylevel, 175 text, 176 timestamp, 177 stackTrace, 178 _extraData: { seenNodeIds }, 179 }; 180 181 // TODO: Those steps relate to: 182 // - emitting associated BrowsingContext. See log.entryAdded full support 183 // in https://bugzilla.mozilla.org/show_bug.cgi?id=1724669#c0 184 // - handling cases where session doesn't exist or the event is not 185 // monitored. The implementation differs from the spec here because we 186 // only react to events if there is a session & if the session subscribed 187 // to those events. 188 189 this.emitEvent("log.entryAdded", entry); 190 }; 191 192 #onJavaScriptError = (eventName, data = {}) => { 193 const { level, message, stacktrace, timeStamp } = data; 194 const defaultRealm = this.messageHandler.getRealm(); 195 196 // Build the JavascriptLogEntry 197 const entry = { 198 type: "javascript", 199 level, 200 // TODO: Bug 1742589. Use an actual realm from which the event came from. 201 source: this.#buildSource(defaultRealm), 202 text: message, 203 timestamp: timeStamp || Date.now(), 204 stackTrace: this.#buildStackTrace(stacktrace), 205 }; 206 207 this.emitEvent("log.entryAdded", entry); 208 }; 209 210 #subscribeEvent(event) { 211 if (event === "log.entryAdded") { 212 this.#consoleAPIListener.startListening(); 213 this.#consoleMessageListener.startListening(); 214 this.#subscribedEvents.add(event); 215 } 216 } 217 218 #unsubscribeEvent(event) { 219 if (event === "log.entryAdded") { 220 this.#consoleAPIListener.stopListening(); 221 this.#consoleMessageListener.stopListening(); 222 this.#subscribedEvents.delete(event); 223 } 224 } 225 226 /** 227 * Internal commands 228 */ 229 230 _applySessionData(params) { 231 // TODO: Bug 1775231. Move this logic to a shared module or an abstract 232 // class. 233 const { category } = params; 234 if (category === "event") { 235 const filteredSessionData = params.sessionData.filter(item => 236 this.messageHandler.matchesContext(item.contextDescriptor) 237 ); 238 for (const event of this.#subscribedEvents.values()) { 239 const hasSessionItem = filteredSessionData.some( 240 item => item.value === event 241 ); 242 // If there are no session items for this context, we should unsubscribe from the event. 243 if (!hasSessionItem) { 244 this.#unsubscribeEvent(event); 245 } 246 } 247 248 // Subscribe to all events, which have an item in SessionData. 249 for (const { value } of filteredSessionData) { 250 this.#subscribeEvent(value); 251 } 252 } 253 } 254 } 255 256 export const log = LogModule;