summaryrefslogtreecommitdiffstats
path: root/devtools/startup/DevToolsShim.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/startup/DevToolsShim.sys.mjs')
-rw-r--r--devtools/startup/DevToolsShim.sys.mjs330
1 files changed, 330 insertions, 0 deletions
diff --git a/devtools/startup/DevToolsShim.sys.mjs b/devtools/startup/DevToolsShim.sys.mjs
new file mode 100644
index 0000000000..41f874a1ca
--- /dev/null
+++ b/devtools/startup/DevToolsShim.sys.mjs
@@ -0,0 +1,330 @@
+/* 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/. */
+
+const lazy = {};
+ChromeUtils.defineLazyGetter(lazy, "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.
+ChromeUtils.defineLazyGetter(lazy, "Telemetry", function () {
+ const { require } = ChromeUtils.importESModule(
+ "resource://devtools/shared/loader/Loader.sys.mjs"
+ );
+ // eslint-disable-next-line no-shadow
+ const Telemetry = require("devtools/client/shared/telemetry");
+
+ return Telemetry;
+});
+
+const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled";
+
+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 ready, the DevToolsShim will forward all the requests received until
+ * then to the real DevTools instance.
+ */
+export const DevToolsShim = {
+ _gDevTools: null,
+ listeners: [],
+
+ get telemetry() {
+ if (!this._telemetry) {
+ this._telemetry = new lazy.Telemetry();
+ this._telemetry.setEventRecordingEnabled(true);
+ }
+ return this._telemetry;
+ },
+
+ /**
+ * Returns true if DevTools are enabled. This now only depends on the policy.
+ * TODO: Merge isEnabled and isDisabledByPolicy.
+ */
+ isEnabled() {
+ return !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() {
+ return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false);
+ },
+
+ /**
+ * Check if DevTools have already been initialized.
+ *
+ * @return {Boolean} true if DevTools are initialized.
+ */
+ isInitialized() {
+ 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() {
+ 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(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() {
+ 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-ready
+ * - toolbox-destroyed
+ */
+ on(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(event, listener) {
+ if (this.isInitialized()) {
+ this._gDevTools.off(event, listener);
+ } else {
+ removeItem(this.listeners, ([e, l]) => e === event && l === listener);
+ }
+ },
+
+ /**
+ * Called from SessionStore.sys.mjs in mozilla-central when saving the current state.
+ *
+ * @param {Object} state
+ * A SessionStore state object that gets modified by reference
+ */
+ saveDevToolsSession(state) {
+ if (!this.isInitialized()) {
+ return;
+ }
+
+ this._gDevTools.saveDevToolsSession(state);
+ },
+
+ /**
+ * Called from SessionStore.sys.mjs in mozilla-central when restoring a previous session.
+ * Will always be called, even if the session does not contain DevTools related items.
+ */
+ restoreDevToolsSession(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);
+ },
+
+ isDevToolsUser() {
+ return lazy.DevToolsStartup.isDevToolsUser();
+ },
+
+ /**
+ * 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(tab, domReference) {
+ if (!this.isEnabled()) {
+ 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(tab, domReference) {
+ if (!this.isEnabled()) {
+ 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() {
+ // 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.
+ *
+ * @param {String} reason
+ * optional, if provided should be a valid entry point for DEVTOOLS_ENTRY_POINT
+ * in toolkit/components/telemetry/Histograms.json
+ */
+ initDevTools(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()) {
+ lazy.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 = [
+ "createCommandsForTabForWebExtension",
+ "getTheme",
+ "openBrowserConsole",
+];
+
+/**
+ * Compatibility layer for other third parties.
+ */
+const otherToolMethods = [
+ // gDevTools.showToolboxForTab is used by wptrunner to start devtools
+ // https://github.com/web-platform-tests/wpt
+ // And also, Quick Actions on URL bar.
+ "showToolboxForTab",
+ // Used for Quick Actions on URL bar.
+ "hasToolboxForTab",
+ // Used for Quick Actions test.
+ "getToolboxForTab",
+];
+
+for (const method of [...webExtensionsMethods, ...otherToolMethods]) {
+ 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);
+ };
+}