console-messages.js (10728B)
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 "use strict"; 6 7 const Targets = require("devtools/server/actors/targets/index"); 8 9 const consoleAPIListenerModule = isWorker 10 ? "devtools/server/actors/webconsole/worker-listeners" 11 : "devtools/server/actors/webconsole/listeners/console-api"; 12 const { ConsoleAPIListener } = require(consoleAPIListenerModule); 13 14 const { 15 makeDebuggeeValue, 16 createValueGripForTarget, 17 } = require("devtools/server/actors/object/utils"); 18 19 const { 20 getActorIdForInternalSourceId, 21 } = require("devtools/server/actors/utils/dbg-source"); 22 23 loader.lazyRequireGetter( 24 this, 25 "isArray", 26 "resource://devtools/server/actors/object/utils.js", 27 true 28 ); 29 30 loader.lazyRequireGetter( 31 this, 32 "isSupportedByConsoleTable", 33 "resource://devtools/shared/webconsole/messages.js", 34 true 35 ); 36 37 /** 38 * Start watching for all console messages related to a given Target Actor. 39 * This will notify about existing console messages, but also the one created in future. 40 * 41 * @param TargetActor targetActor 42 * The target actor from which we should observe console messages 43 * @param Object options 44 * Dictionary object with following attributes: 45 * - onAvailable: mandatory function 46 * This will be called for each resource. 47 */ 48 class ConsoleMessageWatcher { 49 async watch(targetActor, { onAvailable }) { 50 this.targetActor = targetActor; 51 this.onAvailable = onAvailable; 52 53 // Bug 1642297: Maybe we could merge ConsoleAPI Listener into this module? 54 const onConsoleAPICall = message => { 55 onAvailable([prepareConsoleMessageForRemote(targetActor, message)]); 56 }; 57 58 const isTargetActorContentProcess = 59 targetActor.targetType === Targets.TYPES.PROCESS; 60 61 // Only consider messages from a given window for all FRAME targets (this includes 62 // WebExt and ParentProcess which inherits from WindowGlobalTargetActor) 63 // But ParentProcess should be ignored as we want all messages emitted directly from 64 // that process (window and window-less). 65 // To do that we pass a null window and ConsoleAPIListener will catch everything. 66 const messagesShouldMatchWindow = 67 targetActor.targetType === Targets.TYPES.FRAME && 68 targetActor.typeName != "parentProcessTarget"; 69 const window = messagesShouldMatchWindow ? targetActor.window : null; 70 71 // If we should match messages for a given window but for some reason, targetActor.window 72 // did not return a window, bail out. Otherwise we wouldn't have anything to match against 73 // and would consume all the messages, which could lead to issue (e.g. infinite loop, 74 // see Bug 1828026). 75 if (messagesShouldMatchWindow && !window) { 76 return; 77 } 78 79 const listener = new ConsoleAPIListener(window, onConsoleAPICall, { 80 excludeMessagesBoundToWindow: isTargetActorContentProcess, 81 matchExactWindow: targetActor.ignoreSubFrames, 82 addonId: 83 targetActor.targetType === Targets.TYPES.CONTENT_SCRIPT 84 ? targetActor.addonId 85 : null, 86 }); 87 this.listener = listener; 88 listener.init(); 89 90 // It can happen that the targetActor does not have a window reference (e.g. in worker 91 // thread, targetActor exposes a targetGlobal property which isn't a Window object) 92 const winStartTime = 93 targetActor.window?.performance?.timing?.navigationStart || 0; 94 95 const cachedMessages = listener.getCachedMessages(!targetActor.isRootActor); 96 const messages = []; 97 // Filter out messages that came from a ServiceWorker but happened 98 // before the page was requested. 99 for (const message of cachedMessages) { 100 if ( 101 message.innerID === "ServiceWorker" && 102 winStartTime > message.timeStamp 103 ) { 104 continue; 105 } 106 messages.push(prepareConsoleMessageForRemote(targetActor, message)); 107 } 108 onAvailable(messages); 109 } 110 111 /** 112 * Stop watching for console messages. 113 */ 114 destroy() { 115 if (this.listener) { 116 this.listener.destroy(); 117 this.listener = null; 118 } 119 this.targetActor = null; 120 this.onAvailable = null; 121 } 122 123 /** 124 * Spawn some custom console messages. 125 * This is used for example for log points and JS tracing. 126 * 127 * @param Array<Object> messages 128 * A list of fake nsIConsoleMessage, which looks like the one being generated by 129 * the platform API. 130 * @param boolean argumentsAreRawObjects 131 * Tells whether the messages' arguments properties contain raw or debugger objects. 132 */ 133 emitMessages(messages, argumentsAreRawObjects = true) { 134 if (!this.listener) { 135 throw new Error("This target actor isn't listening to console messages"); 136 } 137 this.onAvailable( 138 messages.map(message => { 139 if (!message.timeStamp) { 140 throw new Error("timeStamp property is mandatory"); 141 } 142 143 return prepareConsoleMessageForRemote( 144 this.targetActor, 145 message, 146 argumentsAreRawObjects 147 ); 148 }) 149 ); 150 } 151 } 152 153 module.exports = ConsoleMessageWatcher; 154 155 /** 156 * Return the properties needed to display the appropriate table for a given 157 * console.table call. 158 * This function does a little more than creating an ObjectActor for the first 159 * parameter of the message. When layout out the console table in the output, we want 160 * to be able to look into sub-properties so the table can have a different layout ( 161 * for arrays of arrays, objects with objects properties, arrays of objects, …). 162 * So here we need to retrieve the properties of the first parameter, and also all the 163 * sub-properties we might need. 164 * 165 * @param {TargetActor} targetActor: The Target Actor from which this object originates. 166 * @param {object} result: The console.table message. 167 * @returns {object} An object containing the properties of the first argument of the 168 * console.table call. 169 */ 170 function getConsoleTableMessageItems(targetActor, result) { 171 const [tableItemGrip] = result.arguments; 172 const dataType = tableItemGrip.class; 173 const needEntries = ["Map", "WeakMap", "Set", "WeakSet"].includes(dataType); 174 const ignoreNonIndexedProperties = isArray(tableItemGrip); 175 176 const tableItemActor = targetActor.objectsPool.getActorByID( 177 tableItemGrip.actor 178 ); 179 if (!tableItemActor) { 180 return null; 181 } 182 183 // Retrieve the properties (or entries for Set/Map) of the console table first arg. 184 const iterator = needEntries 185 ? tableItemActor.enumEntries() 186 : tableItemActor.enumProperties({ 187 ignoreNonIndexedProperties, 188 }); 189 const { ownProperties } = iterator.all(); 190 191 // The iterator returns a descriptor for each property, wherein the value could be 192 // in one of those sub-property. 193 const descriptorKeys = ["safeGetterValues", "getterValue", "value"]; 194 195 Object.values(ownProperties).forEach(desc => { 196 if (typeof desc !== "undefined") { 197 descriptorKeys.forEach(key => { 198 if (desc && desc.hasOwnProperty(key)) { 199 const grip = desc[key]; 200 201 // We need to load sub-properties as well to render the table in a nice way. 202 const actor = 203 grip && targetActor.objectsPool.getActorByID(grip.actor); 204 if (actor && typeof actor.enumProperties === "function") { 205 const res = actor 206 .enumProperties({ 207 ignoreNonIndexedProperties: isArray(grip), 208 }) 209 .all(); 210 if (res?.ownProperties) { 211 desc[key].ownProperties = res.ownProperties; 212 } 213 } 214 } 215 }); 216 } 217 }); 218 219 return ownProperties; 220 } 221 222 /** 223 * Prepare a message from the console API to be sent to the remote Web Console 224 * instance. 225 * 226 * @param TargetActor targetActor 227 * The related target actor 228 * @param object message 229 * The original message received from the console storage listener or emitMessages(). 230 * @param boolean argumentsAreRawObjects 231 * Tells whether the message's arguments property contains raw or debugger objects. 232 * @return object 233 * The object that can be sent to the remote client. 234 */ 235 function prepareConsoleMessageForRemote( 236 targetActor, 237 message, 238 argumentsAreRawObjects = true 239 ) { 240 const result = { 241 arguments: message.arguments 242 ? message.arguments.map(obj => { 243 const dbgObj = argumentsAreRawObjects 244 ? makeDebuggeeValue(targetActor, obj) 245 : obj; 246 return createValueGripForTarget(targetActor, dbgObj); 247 }) 248 : [], 249 250 // The line is 1-based. 251 lineNumber: message.lineNumber, 252 // The column is also 1-based as it ultimately derivates from SavedFrame's 1-based column 253 columnNumber: message.columnNumber, 254 255 filename: message.filename, 256 level: message.level, 257 258 // messages emitted from Console.sys.mjs don't have a microSecondTimeStamp property 259 timeStamp: message.microSecondTimeStamp 260 ? message.microSecondTimeStamp / 1000 261 : message.timeStamp || ChromeUtils.dateNow(), 262 sourceId: getActorIdForInternalSourceId(targetActor, message.sourceId), 263 innerWindowID: message.innerID, 264 }; 265 266 // This can be a hot path when loading lots of messages, and it only make sense to 267 // include the following properties in the message when they have a meaningful value. 268 // Otherwise we simply don't include them so we save cycles in JSActor communication. 269 if (message.chromeContext) { 270 result.chromeContext = message.chromeContext; 271 } 272 273 if (message.counter) { 274 result.counter = message.counter; 275 } 276 if (message.private) { 277 result.private = message.private; 278 } 279 if (message.prefix) { 280 result.prefix = message.prefix; 281 } 282 283 if (message.stacktrace) { 284 result.stacktrace = message.stacktrace.map(frame => { 285 return { 286 ...frame, 287 sourceId: getActorIdForInternalSourceId(targetActor, frame.sourceId), 288 }; 289 }); 290 } 291 292 if (message.styles && message.styles.length) { 293 result.styles = message.styles.map(string => { 294 return createValueGripForTarget(targetActor, string); 295 }); 296 } 297 298 if (message.timer) { 299 result.timer = message.timer; 300 } 301 302 if (message.level === "table") { 303 if (result && isSupportedByConsoleTable(result.arguments)) { 304 const tableItems = getConsoleTableMessageItems(targetActor, result); 305 if (tableItems) { 306 result.arguments[0].ownProperties = tableItems; 307 result.arguments[0].preview = null; 308 309 // Only return the 2 first params. 310 result.arguments = result.arguments.slice(0, 2); 311 } 312 } 313 // NOTE: See transformConsoleAPICallResource for not-supported case. 314 } 315 316 return result; 317 }