DevToolsShim.sys.mjs (9862B)
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 lazy = {}; 6 ChromeUtils.defineLazyGetter(lazy, "DevToolsStartup", () => { 7 return Cc["@mozilla.org/devtools/startup-clh;1"].getService( 8 Ci.nsICommandLineHandler 9 ).wrappedJSObject; 10 }); 11 12 // We don't want to spend time initializing the full loader here so we create 13 // our own lazy require. 14 ChromeUtils.defineLazyGetter(lazy, "Telemetry", function () { 15 const { require } = ChromeUtils.importESModule( 16 "resource://devtools/shared/loader/Loader.sys.mjs" 17 ); 18 // eslint-disable-next-line no-shadow 19 const Telemetry = require("devtools/client/shared/telemetry"); 20 21 return Telemetry; 22 }); 23 24 const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled"; 25 26 function removeItem(array, callback) { 27 const index = array.findIndex(callback); 28 if (index >= 0) { 29 array.splice(index, 1); 30 } 31 } 32 33 /** 34 * DevToolsShim is a singleton that provides a set of helpers to interact with DevTools, 35 * that work whether Devtools are enabled or not. 36 * 37 * It can be used to start listening to devtools events before DevTools are ready. As soon 38 * as DevTools are ready, the DevToolsShim will forward all the requests received until 39 * then to the real DevTools instance. 40 */ 41 export const DevToolsShim = { 42 _gDevTools: null, 43 listeners: [], 44 45 get telemetry() { 46 if (!this._telemetry) { 47 this._telemetry = new lazy.Telemetry(); 48 } 49 return this._telemetry; 50 }, 51 52 /** 53 * Returns true if DevTools are enabled. This now only depends on the policy. 54 * TODO: Merge isEnabled and isDisabledByPolicy. 55 * 56 * @returns {boolean} 57 */ 58 isEnabled() { 59 return !this.isDisabledByPolicy(); 60 }, 61 62 /** 63 * Returns true if the devtools are completely disabled and can not be enabled. All 64 * entry points should return without throwing, initDevTools should never be called. 65 * 66 * @returns {boolean} 67 */ 68 isDisabledByPolicy() { 69 return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false); 70 }, 71 72 /** 73 * Check if DevTools have already been initialized. 74 * 75 * @returns {boolean} true if DevTools are initialized. 76 */ 77 isInitialized() { 78 return !!this._gDevTools; 79 }, 80 81 /** 82 * Returns the array of the existing toolboxes. This method is part of the compatibility 83 * layer for webextensions. 84 * 85 * @returns {Array<Toolbox>} 86 * An array of toolboxes. 87 */ 88 getToolboxes() { 89 if (this.isInitialized()) { 90 return this._gDevTools.getToolboxes(); 91 } 92 93 return []; 94 }, 95 96 /** 97 * Register an instance of gDevTools. Should be called by DevTools during startup. 98 * 99 * @param {DevTools} gDevTools - A DevTools instance (from client/framework/devtools) 100 */ 101 register(gDevTools) { 102 this._gDevTools = gDevTools; 103 this._onDevToolsRegistered(); 104 this._gDevTools.emit("devtools-registered"); 105 }, 106 107 /** 108 * Unregister the current instance of gDevTools. Should be called by DevTools during 109 * shutdown. 110 */ 111 unregister() { 112 if (this.isInitialized()) { 113 this._gDevTools.emit("devtools-unregistered"); 114 this._gDevTools = null; 115 } 116 }, 117 118 /** 119 * The following methods can be called before DevTools are initialized: 120 * - on 121 * - off 122 * 123 * If DevTools are not initialized when calling the method, DevToolsShim will call the 124 * appropriate method as soon as a gDevTools instance is registered. 125 */ 126 127 /** 128 * This method is used by browser/components/extensions/ext-devtools.js for the events: 129 * - toolbox-ready 130 * - toolbox-destroyed 131 * 132 * @param {string} event 133 * @param {Function} listener 134 */ 135 on(event, listener) { 136 if (this.isInitialized()) { 137 this._gDevTools.on(event, listener); 138 } else { 139 this.listeners.push([event, listener]); 140 } 141 }, 142 143 /** 144 * This method is currently only used by devtools code, but is kept here for consistency 145 * with on(). 146 * 147 * @param {string} event 148 * @param {Function} listener 149 */ 150 off(event, listener) { 151 if (this.isInitialized()) { 152 this._gDevTools.off(event, listener); 153 } else { 154 removeItem(this.listeners, ([e, l]) => e === event && l === listener); 155 } 156 }, 157 158 /** 159 * Called from SessionStore.sys.mjs in mozilla-central when saving the current state. 160 * 161 * @param {object} state - A SessionStore state object that gets modified by reference 162 */ 163 saveDevToolsSession(state) { 164 if (!this.isInitialized()) { 165 return; 166 } 167 168 this._gDevTools.saveDevToolsSession(state); 169 }, 170 171 /** 172 * Called from SessionStore.sys.mjs in mozilla-central when restoring a previous session. 173 * Will always be called, even if the session does not contain DevTools related items. 174 * 175 * @param {object} session 176 */ 177 restoreDevToolsSession(session) { 178 if (!this.isEnabled()) { 179 return; 180 } 181 182 const { browserConsole, browserToolbox } = session; 183 const hasDevToolsData = browserConsole || browserToolbox; 184 if (!hasDevToolsData) { 185 // Do not initialize DevTools unless there is DevTools specific data in the session. 186 return; 187 } 188 189 this.initDevTools("SessionRestore"); 190 this._gDevTools.restoreDevToolsSession(session); 191 }, 192 193 isDevToolsUser() { 194 return lazy.DevToolsStartup.isDevToolsUser(); 195 }, 196 197 /** 198 * Called from nsContextMenu.js in mozilla-central when using the Inspect Accessibility 199 * context menu item. 200 * 201 * @param {XULTab} tab 202 * The browser tab on which inspect accessibility was used. 203 * @param {ElementIdentifier} domReference 204 * Identifier generated by ContentDOMReference. It is a unique pair of 205 * BrowsingContext ID and a numeric ID. 206 * @returns {Promise} a promise that resolves when the accessible node is selected in the 207 * accessibility inspector or that resolves immediately if DevTools are not 208 * enabled. 209 */ 210 inspectA11Y(tab, domReference) { 211 if (!this.isEnabled()) { 212 return Promise.resolve(); 213 } 214 215 // Record the timing at which this event started in order to compute later in 216 // gDevTools.showToolbox, the complete time it takes to open the toolbox. 217 // i.e. especially take `DevToolsStartup.initDevTools` into account. 218 const startTime = ChromeUtils.now(); 219 220 this.initDevTools("ContextMenu"); 221 222 return this._gDevTools.inspectA11Y(tab, domReference, startTime); 223 }, 224 225 /** 226 * Called from nsContextMenu.js in mozilla-central when using the Inspect Element 227 * context menu item. 228 * 229 * @param {XULTab} tab 230 * The browser tab on which inspect node was used. 231 * @param {ElementIdentifier} domReference 232 * Identifier generated by ContentDOMReference. It is a unique pair of 233 * BrowsingContext ID and a numeric ID. 234 * @returns {Promise} a promise that resolves when the node is selected in the inspector 235 * markup view or that resolves immediately if DevTools are not enabled. 236 */ 237 inspectNode(tab, domReference) { 238 if (!this.isEnabled()) { 239 return Promise.resolve(); 240 } 241 242 // Record the timing at which this event started in order to compute later in 243 // gDevTools.showToolbox, the complete time it takes to open the toolbox. 244 // i.e. especially take `DevToolsStartup.initDevTools` into account. 245 const startTime = ChromeUtils.now(); 246 247 this.initDevTools("ContextMenu"); 248 249 return this._gDevTools.inspectNode(tab, domReference, startTime); 250 }, 251 252 _onDevToolsRegistered() { 253 // Register all pending event listeners on the real gDevTools object. 254 for (const [event, listener] of this.listeners) { 255 this._gDevTools.on(event, listener); 256 } 257 258 this.listeners = []; 259 }, 260 261 /** 262 * Initialize DevTools via DevToolsStartup if needed. This method throws if DevTools are 263 * not enabled. 264 * 265 * @param {string} [reason] 266 * optional, if provided should be a valid entry point for DEVTOOLS_ENTRY_POINT 267 * in toolkit/components/telemetry/Histograms.json 268 * and devtools:entry_point in devtools/client/shared/metrics.yaml 269 */ 270 initDevTools(reason) { 271 if (!this.isEnabled()) { 272 throw new Error("DevTools are not enabled and can not be initialized."); 273 } 274 275 if (reason) { 276 const window = Services.wm.getMostRecentBrowserWindow(); 277 278 this.telemetry.addEventProperty( 279 window, 280 "open", 281 "tools", 282 null, 283 "shortcut", 284 "" 285 ); 286 this.telemetry.addEventProperty( 287 window, 288 "open", 289 "tools", 290 null, 291 "entrypoint", 292 reason 293 ); 294 } 295 296 if (!this.isInitialized()) { 297 lazy.DevToolsStartup.initDevTools(reason); 298 } 299 }, 300 }; 301 302 /** 303 * Compatibility layer for webextensions. 304 * 305 * Those methods are called only after a DevTools webextension was loaded in DevTools, 306 * therefore DevTools should always be available when they are called. 307 */ 308 const webExtensionsMethods = [ 309 "createCommandsForTabForWebExtension", 310 "getTheme", 311 "openBrowserConsole", 312 ]; 313 314 /** 315 * Compatibility layer for other third parties. 316 */ 317 const otherToolMethods = [ 318 // gDevTools.showToolboxForTab is used by wptrunner to start devtools 319 // https://github.com/web-platform-tests/wpt 320 // And also, Quick Actions on URL bar. 321 "showToolboxForTab", 322 // Used for Quick Actions on URL bar. 323 "hasToolboxForTab", 324 // Used for Quick Actions test. 325 "getToolboxForTab", 326 ]; 327 328 for (const method of [...webExtensionsMethods, ...otherToolMethods]) { 329 DevToolsShim[method] = function () { 330 if (!this.isEnabled()) { 331 throw new Error( 332 "Could not call a DevToolsShim webextension method ('" + 333 method + 334 "'): DevTools are not initialized." 335 ); 336 } 337 338 this.initDevTools(); 339 return this._gDevTools[method].apply(this._gDevTools, arguments); 340 }; 341 }