diff options
Diffstat (limited to '')
-rw-r--r-- | devtools/startup/DevToolsShim.jsm | 332 |
1 files changed, 332 insertions, 0 deletions
diff --git a/devtools/startup/DevToolsShim.jsm b/devtools/startup/DevToolsShim.jsm new file mode 100644 index 0000000000..4d14002b3e --- /dev/null +++ b/devtools/startup/DevToolsShim.jsm @@ -0,0 +1,332 @@ +/* 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +XPCOMUtils.defineLazyGetter(this, "DevtoolsStartup", () => { + return Cc["@mozilla.org/devtools/startup-clh;1"].getService( + Ci.nsICommandLineHandler + ).wrappedJSObject; +}); + +// We don't want to spend time initializing the full loader here so we create +// our own lazy require. +XPCOMUtils.defineLazyGetter(this, "Telemetry", function() { + const { require } = ChromeUtils.import( + "resource://devtools/shared/Loader.jsm" + ); + // eslint-disable-next-line no-shadow + const Telemetry = require("devtools/client/shared/telemetry"); + + return Telemetry; +}); + +const DEVTOOLS_ENABLED_PREF = "devtools.enabled"; +const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled"; + +const EXPORTED_SYMBOLS = ["DevToolsShim"]; + +function removeItem(array, callback) { + const index = array.findIndex(callback); + if (index >= 0) { + array.splice(index, 1); + } +} + +/** + * DevToolsShim is a singleton that provides a set of helpers to interact with DevTools, + * that work whether Devtools are enabled or not. + * + * It can be used to start listening to devtools events before DevTools are ready. As soon + * as DevTools are enabled, the DevToolsShim will forward all the requests received until + * then to the real DevTools instance. + */ +const DevToolsShim = { + _gDevTools: null, + listeners: [], + + get telemetry() { + if (!this._telemetry) { + this._telemetry = new Telemetry(); + this._telemetry.setEventRecordingEnabled(true); + } + return this._telemetry; + }, + + /** + * Returns true if DevTools are enabled for the current profile. If devtools are not + * enabled, initializing DevTools will open the onboarding page. Some entry points + * should no-op in this case. + */ + isEnabled: function() { + const enabled = Services.prefs.getBoolPref(DEVTOOLS_ENABLED_PREF); + return enabled && !this.isDisabledByPolicy(); + }, + + /** + * Returns true if the devtools are completely disabled and can not be enabled. All + * entry points should return without throwing, initDevTools should never be called. + */ + isDisabledByPolicy: function() { + return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false); + }, + + /** + * Check if DevTools have already been initialized. + * + * @return {Boolean} true if DevTools are initialized. + */ + isInitialized: function() { + return !!this._gDevTools; + }, + + /** + * Returns the array of the existing toolboxes. This method is part of the compatibility + * layer for webextensions. + * + * @return {Array<Toolbox>} + * An array of toolboxes. + */ + getToolboxes: function() { + if (this.isInitialized()) { + return this._gDevTools.getToolboxes(); + } + + return []; + }, + + /** + * Register an instance of gDevTools. Should be called by DevTools during startup. + * + * @param {DevTools} a devtools instance (from client/framework/devtools) + */ + register: function(gDevTools) { + this._gDevTools = gDevTools; + this._onDevToolsRegistered(); + this._gDevTools.emit("devtools-registered"); + }, + + /** + * Unregister the current instance of gDevTools. Should be called by DevTools during + * shutdown. + */ + unregister: function() { + if (this.isInitialized()) { + this._gDevTools.emit("devtools-unregistered"); + this._gDevTools = null; + } + }, + + /** + * The following methods can be called before DevTools are initialized: + * - on + * - off + * + * If DevTools are not initialized when calling the method, DevToolsShim will call the + * appropriate method as soon as a gDevTools instance is registered. + */ + + /** + * This method is used by browser/components/extensions/ext-devtools.js for the events: + * - toolbox-created + * - toolbox-destroyed + */ + on: function(event, listener) { + if (this.isInitialized()) { + this._gDevTools.on(event, listener); + } else { + this.listeners.push([event, listener]); + } + }, + + /** + * This method is currently only used by devtools code, but is kept here for consistency + * with on(). + */ + off: function(event, listener) { + if (this.isInitialized()) { + this._gDevTools.off(event, listener); + } else { + removeItem(this.listeners, ([e, l]) => e === event && l === listener); + } + }, + + /** + * Called from SessionStore.jsm in mozilla-central when saving the current state. + * + * @param {Object} state + * A SessionStore state object that gets modified by reference + */ + saveDevToolsSession: function(state) { + if (!this.isInitialized()) { + return; + } + + this._gDevTools.saveDevToolsSession(state); + }, + + /** + * Called from SessionStore.jsm in mozilla-central when restoring a previous session. + * Will always be called, even if the session does not contain DevTools related items. + */ + restoreDevToolsSession: function(session) { + if (!this.isEnabled()) { + return; + } + + const { browserConsole, browserToolbox } = session; + const hasDevToolsData = browserConsole || browserToolbox; + if (!hasDevToolsData) { + // Do not initialize DevTools unless there is DevTools specific data in the session. + return; + } + + this.initDevTools("SessionRestore"); + this._gDevTools.restoreDevToolsSession(session); + }, + + /** + * Called from nsContextMenu.js in mozilla-central when using the Inspect Accessibility + * context menu item. + * + * @param {XULTab} tab + * The browser tab on which inspect accessibility was used. + * @param {ElementIdentifier} domReference + * Identifier generated by ContentDOMReference. It is a unique pair of + * BrowsingContext ID and a numeric ID. + * @return {Promise} a promise that resolves when the accessible node is selected in the + * accessibility inspector or that resolves immediately if DevTools are not + * enabled. + */ + inspectA11Y: function(tab, domReference) { + if (!this.isEnabled()) { + if (!this.isDisabledByPolicy()) { + DevtoolsStartup.openInstallPage("ContextMenu"); + } + return Promise.resolve(); + } + + // Record the timing at which this event started in order to compute later in + // gDevTools.showToolbox, the complete time it takes to open the toolbox. + // i.e. especially take `DevtoolsStartup.initDevTools` into account. + const startTime = Cu.now(); + + this.initDevTools("ContextMenu"); + + return this._gDevTools.inspectA11Y(tab, domReference, startTime); + }, + + /** + * Called from nsContextMenu.js in mozilla-central when using the Inspect Element + * context menu item. + * + * @param {XULTab} tab + * The browser tab on which inspect node was used. + * @param {ElementIdentifier} domReference + * Identifier generated by ContentDOMReference. It is a unique pair of + * BrowsingContext ID and a numeric ID. + * @return {Promise} a promise that resolves when the node is selected in the inspector + * markup view or that resolves immediately if DevTools are not enabled. + */ + inspectNode: function(tab, domReference) { + if (!this.isEnabled()) { + if (!this.isDisabledByPolicy()) { + DevtoolsStartup.openInstallPage("ContextMenu"); + } + return Promise.resolve(); + } + + // Record the timing at which this event started in order to compute later in + // gDevTools.showToolbox, the complete time it takes to open the toolbox. + // i.e. especially take `DevtoolsStartup.initDevTools` into account. + const startTime = Cu.now(); + + this.initDevTools("ContextMenu"); + + return this._gDevTools.inspectNode(tab, domReference, startTime); + }, + + _onDevToolsRegistered: function() { + // Register all pending event listeners on the real gDevTools object. + for (const [event, listener] of this.listeners) { + this._gDevTools.on(event, listener); + } + + this.listeners = []; + }, + + /** + * Initialize DevTools via DevToolsStartup if needed. This method throws if DevTools are + * not enabled.. If the entry point is supposed to trigger the onboarding, call it + * explicitly via DevtoolsStartup.openInstallPage(). + * + * @param {String} reason + * optional, if provided should be a valid entry point for DEVTOOLS_ENTRY_POINT + * in toolkit/components/telemetry/Histograms.json + */ + initDevTools: function(reason) { + if (!this.isEnabled()) { + throw new Error("DevTools are not enabled and can not be initialized."); + } + + if (reason) { + const window = Services.wm.getMostRecentWindow("navigator:browser"); + + this.telemetry.addEventProperty( + window, + "open", + "tools", + null, + "shortcut", + "" + ); + this.telemetry.addEventProperty( + window, + "open", + "tools", + null, + "entrypoint", + reason + ); + } + + if (!this.isInitialized()) { + DevtoolsStartup.initDevTools(reason); + } + }, +}; + +/** + * Compatibility layer for webextensions. + * + * Those methods are called only after a DevTools webextension was loaded in DevTools, + * therefore DevTools should always be available when they are called. + */ +const webExtensionsMethods = [ + "createDescriptorForTab", + "createWebExtensionInspectedWindowFront", + "getTargetForTab", + "getTheme", + "openBrowserConsole", +]; + +for (const method of webExtensionsMethods) { + DevToolsShim[method] = function() { + if (!this.isEnabled()) { + throw new Error( + "Could not call a DevToolsShim webextension method ('" + + method + + "'): DevTools are not initialized." + ); + } + + this.initDevTools(); + return this._gDevTools[method].apply(this._gDevTools, arguments); + }; +} |