ConsoleAPIStorage.sys.mjs (6360B)
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 const STORAGE_MAX_EVENTS = 1000; 6 7 var _consoleStorage = new Map(); 8 9 // NOTE: these listeners used to just be added as observers and notified via 10 // Services.obs.notifyObservers. However, that has enough overhead to be a 11 // problem for this. Using an explicit global array is much cheaper, and 12 // should be equivalent. 13 var _logEventListeners = []; 14 15 const CONSOLEAPISTORAGE_CID = Components.ID( 16 "{96cf7855-dfa9-4c6d-8276-f9705b4890f2}" 17 ); 18 19 /** 20 * The ConsoleAPIStorage is meant to cache window.console API calls for later 21 * reuse by other components when needed. For example, the Web Console code can 22 * display the cached messages when it opens for the active tab. 23 * 24 * ConsoleAPI messages are stored as they come from the ConsoleAPI code, with 25 * all their properties. They are kept around until the inner window object that 26 * created the messages is destroyed. Messages are indexed by the inner window 27 * ID. 28 * 29 * Usage: 30 * let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"] 31 * .getService(Ci.nsIConsoleAPIStorage); 32 * 33 * // Get the cached events array for the window you want (use the inner 34 * // window ID). 35 * let events = ConsoleAPIStorage.getEvents(innerWindowID); 36 * events.forEach(function(event) { ... }); 37 * 38 * // Clear the events for the given inner window ID. 39 * ConsoleAPIStorage.clearEvents(innerWindowID); 40 */ 41 export function ConsoleAPIStorageService() { 42 this.init(); 43 } 44 45 ConsoleAPIStorageService.prototype = { 46 classID: CONSOLEAPISTORAGE_CID, 47 QueryInterface: ChromeUtils.generateQI([ 48 "nsIConsoleAPIStorage", 49 "nsIObserver", 50 ]), 51 52 observe: function CS_observe(aSubject, aTopic) { 53 if (aTopic == "xpcom-shutdown") { 54 Services.obs.removeObserver(this, "xpcom-shutdown"); 55 Services.obs.removeObserver(this, "inner-window-destroyed"); 56 Services.obs.removeObserver(this, "memory-pressure"); 57 } else if (aTopic == "inner-window-destroyed") { 58 let innerWindowID = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data; 59 this.clearEvents(innerWindowID + ""); 60 } else if (aTopic == "memory-pressure") { 61 this.clearEvents(); 62 } 63 }, 64 65 /** @private */ 66 init: function CS_init() { 67 Services.obs.addObserver(this, "xpcom-shutdown"); 68 Services.obs.addObserver(this, "inner-window-destroyed"); 69 Services.obs.addObserver(this, "memory-pressure"); 70 }, 71 72 /** 73 * Get the events array by inner window ID or all events from all windows. 74 * 75 * @param string [aId] 76 * Optional, the inner window ID for which you want to get the array of 77 * cached events. 78 * @returns array 79 * The array of cached events for the given window. If no |aId| is 80 * given this function returns all of the cached events, from any 81 * window. 82 */ 83 getEvents: function CS_getEvents(aId) { 84 if (aId != null) { 85 return (_consoleStorage.get(aId) || []).slice(0); 86 } 87 88 let result = []; 89 90 for (let [, events] of _consoleStorage) { 91 result.push.apply(result, events); 92 } 93 94 return result.sort(function (a, b) { 95 return a.timeStamp - b.timeStamp; 96 }); 97 }, 98 99 /** 100 * Adds a listener to be notified of log events. 101 * 102 * @param jsval [aListener] 103 * A JS listener which will be notified with the message object when 104 * a log event occurs. 105 * @param nsIPrincipal [aPrincipal] 106 * The principal of the listener - used to determine if we need to 107 * clone the message before forwarding it. 108 */ 109 addLogEventListener: function CS_addLogEventListener(aListener, aPrincipal) { 110 // If our listener has a less-privileged principal than us, then they won't 111 // be able to access the log event object which was populated for our 112 // scope. Accordingly we need to clone it for these listeners. 113 // 114 // XXX: AFAICT these listeners which we need to clone messages for are all 115 // tests. Alternative solutions are welcome. 116 const clone = !aPrincipal.subsumes( 117 Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal) 118 ); 119 _logEventListeners.push({ 120 callback: aListener, 121 clone, 122 }); 123 }, 124 125 /** 126 * Removes a listener added with `addLogEventListener`. 127 * 128 * @param jsval [aListener] 129 * A JS listener which was added with `addLogEventListener`. 130 */ 131 removeLogEventListener: function CS_removeLogEventListener(aListener) { 132 const index = _logEventListeners.findIndex(l => l.callback === aListener); 133 if (index != -1) { 134 _logEventListeners.splice(index, 1); 135 } else { 136 console.error( 137 "Attempted to remove a log event listener that does not exist." 138 ); 139 } 140 }, 141 142 /** 143 * Record an event associated with the given window ID. 144 * 145 * @param string aId 146 * The ID of the inner window for which the event occurred or "jsm" for 147 * messages logged from JavaScript modules.. 148 * @param object aEvent 149 * A JavaScript object you want to store. 150 */ 151 recordEvent: function CS_recordEvent(aId, aEvent) { 152 if (!_consoleStorage.has(aId)) { 153 _consoleStorage.set(aId, []); 154 } 155 156 let storage = _consoleStorage.get(aId); 157 158 storage.push(aEvent); 159 160 // truncate 161 if (storage.length > STORAGE_MAX_EVENTS) { 162 storage.shift(); 163 } 164 165 for (let { callback, clone } of _logEventListeners) { 166 try { 167 if (clone) { 168 callback(Cu.cloneInto(aEvent, callback)); 169 } else { 170 callback(aEvent); 171 } 172 } catch (e) { 173 // A failing listener should not prevent from calling other listeners. 174 console.error(e); 175 } 176 } 177 }, 178 179 /** 180 * Clear storage data for the given window. 181 * 182 * @param string [aId] 183 * Optional, the inner window ID for which you want to clear the 184 * messages. If this is not specified all of the cached messages are 185 * cleared, from all window objects. 186 */ 187 clearEvents: function CS_clearEvents(aId) { 188 if (aId != null) { 189 _consoleStorage.delete(aId); 190 } else { 191 _consoleStorage.clear(); 192 Services.obs.notifyObservers(null, "console-storage-reset"); 193 } 194 }, 195 };