no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / devtools / startup / DevToolsShim.sys.mjs
blob41f874a1cacaaa89eff7086a39d9ee5405ba0995
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/. */
5 const lazy = {};
6 ChromeUtils.defineLazyGetter(lazy, "DevToolsStartup", () => {
7   return Cc["@mozilla.org/devtools/startup-clh;1"].getService(
8     Ci.nsICommandLineHandler
9   ).wrappedJSObject;
10 });
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");
21   return Telemetry;
22 });
24 const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled";
26 function removeItem(array, callback) {
27   const index = array.findIndex(callback);
28   if (index >= 0) {
29     array.splice(index, 1);
30   }
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: [],
45   get telemetry() {
46     if (!this._telemetry) {
47       this._telemetry = new lazy.Telemetry();
48       this._telemetry.setEventRecordingEnabled(true);
49     }
50     return this._telemetry;
51   },
53   /**
54    * Returns true if DevTools are enabled. This now only depends on the policy.
55    * TODO: Merge isEnabled and isDisabledByPolicy.
56    */
57   isEnabled() {
58     return !this.isDisabledByPolicy();
59   },
61   /**
62    * Returns true if the devtools are completely disabled and can not be enabled. All
63    * entry points should return without throwing, initDevTools should never be called.
64    */
65   isDisabledByPolicy() {
66     return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false);
67   },
69   /**
70    * Check if DevTools have already been initialized.
71    *
72    * @return {Boolean} true if DevTools are initialized.
73    */
74   isInitialized() {
75     return !!this._gDevTools;
76   },
78   /**
79    * Returns the array of the existing toolboxes. This method is part of the compatibility
80    * layer for webextensions.
81    *
82    * @return {Array<Toolbox>}
83    *   An array of toolboxes.
84    */
85   getToolboxes() {
86     if (this.isInitialized()) {
87       return this._gDevTools.getToolboxes();
88     }
90     return [];
91   },
93   /**
94    * Register an instance of gDevTools. Should be called by DevTools during startup.
95    *
96    * @param {DevTools} a devtools instance (from client/framework/devtools)
97    */
98   register(gDevTools) {
99     this._gDevTools = gDevTools;
100     this._onDevToolsRegistered();
101     this._gDevTools.emit("devtools-registered");
102   },
104   /**
105    * Unregister the current instance of gDevTools. Should be called by DevTools during
106    * shutdown.
107    */
108   unregister() {
109     if (this.isInitialized()) {
110       this._gDevTools.emit("devtools-unregistered");
111       this._gDevTools = null;
112     }
113   },
115   /**
116    * The following methods can be called before DevTools are initialized:
117    * - on
118    * - off
119    *
120    * If DevTools are not initialized when calling the method, DevToolsShim will call the
121    * appropriate method as soon as a gDevTools instance is registered.
122    */
124   /**
125    * This method is used by browser/components/extensions/ext-devtools.js for the events:
126    * - toolbox-ready
127    * - toolbox-destroyed
128    */
129   on(event, listener) {
130     if (this.isInitialized()) {
131       this._gDevTools.on(event, listener);
132     } else {
133       this.listeners.push([event, listener]);
134     }
135   },
137   /**
138    * This method is currently only used by devtools code, but is kept here for consistency
139    * with on().
140    */
141   off(event, listener) {
142     if (this.isInitialized()) {
143       this._gDevTools.off(event, listener);
144     } else {
145       removeItem(this.listeners, ([e, l]) => e === event && l === listener);
146     }
147   },
149   /**
150    * Called from SessionStore.sys.mjs in mozilla-central when saving the current state.
151    *
152    * @param {Object} state
153    *                 A SessionStore state object that gets modified by reference
154    */
155   saveDevToolsSession(state) {
156     if (!this.isInitialized()) {
157       return;
158     }
160     this._gDevTools.saveDevToolsSession(state);
161   },
163   /**
164    * Called from SessionStore.sys.mjs in mozilla-central when restoring a previous session.
165    * Will always be called, even if the session does not contain DevTools related items.
166    */
167   restoreDevToolsSession(session) {
168     if (!this.isEnabled()) {
169       return;
170     }
172     const { browserConsole, browserToolbox } = session;
173     const hasDevToolsData = browserConsole || browserToolbox;
174     if (!hasDevToolsData) {
175       // Do not initialize DevTools unless there is DevTools specific data in the session.
176       return;
177     }
179     this.initDevTools("SessionRestore");
180     this._gDevTools.restoreDevToolsSession(session);
181   },
183   isDevToolsUser() {
184     return lazy.DevToolsStartup.isDevToolsUser();
185   },
187   /**
188    * Called from nsContextMenu.js in mozilla-central when using the Inspect Accessibility
189    * context menu item.
190    *
191    * @param {XULTab} tab
192    *        The browser tab on which inspect accessibility was used.
193    * @param {ElementIdentifier} domReference
194    *        Identifier generated by ContentDOMReference. It is a unique pair of
195    *        BrowsingContext ID and a numeric ID.
196    * @return {Promise} a promise that resolves when the accessible node is selected in the
197    *         accessibility inspector or that resolves immediately if DevTools are not
198    *         enabled.
199    */
200   inspectA11Y(tab, domReference) {
201     if (!this.isEnabled()) {
202       return Promise.resolve();
203     }
205     // Record the timing at which this event started in order to compute later in
206     // gDevTools.showToolbox, the complete time it takes to open the toolbox.
207     // i.e. especially take `DevToolsStartup.initDevTools` into account.
208     const startTime = Cu.now();
210     this.initDevTools("ContextMenu");
212     return this._gDevTools.inspectA11Y(tab, domReference, startTime);
213   },
215   /**
216    * Called from nsContextMenu.js in mozilla-central when using the Inspect Element
217    * context menu item.
218    *
219    * @param {XULTab} tab
220    *        The browser tab on which inspect node was used.
221    * @param {ElementIdentifier} domReference
222    *        Identifier generated by ContentDOMReference. It is a unique pair of
223    *        BrowsingContext ID and a numeric ID.
224    * @return {Promise} a promise that resolves when the node is selected in the inspector
225    *         markup view or that resolves immediately if DevTools are not enabled.
226    */
227   inspectNode(tab, domReference) {
228     if (!this.isEnabled()) {
229       return Promise.resolve();
230     }
232     // Record the timing at which this event started in order to compute later in
233     // gDevTools.showToolbox, the complete time it takes to open the toolbox.
234     // i.e. especially take `DevToolsStartup.initDevTools` into account.
235     const startTime = Cu.now();
237     this.initDevTools("ContextMenu");
239     return this._gDevTools.inspectNode(tab, domReference, startTime);
240   },
242   _onDevToolsRegistered() {
243     // Register all pending event listeners on the real gDevTools object.
244     for (const [event, listener] of this.listeners) {
245       this._gDevTools.on(event, listener);
246     }
248     this.listeners = [];
249   },
251   /**
252    * Initialize DevTools via DevToolsStartup if needed. This method throws if DevTools are
253    * not enabled.
254    *
255    * @param {String} reason
256    *        optional, if provided should be a valid entry point for DEVTOOLS_ENTRY_POINT
257    *        in toolkit/components/telemetry/Histograms.json
258    */
259   initDevTools(reason) {
260     if (!this.isEnabled()) {
261       throw new Error("DevTools are not enabled and can not be initialized.");
262     }
264     if (reason) {
265       const window = Services.wm.getMostRecentWindow("navigator:browser");
267       this.telemetry.addEventProperty(
268         window,
269         "open",
270         "tools",
271         null,
272         "shortcut",
273         ""
274       );
275       this.telemetry.addEventProperty(
276         window,
277         "open",
278         "tools",
279         null,
280         "entrypoint",
281         reason
282       );
283     }
285     if (!this.isInitialized()) {
286       lazy.DevToolsStartup.initDevTools(reason);
287     }
288   },
292  * Compatibility layer for webextensions.
294  * Those methods are called only after a DevTools webextension was loaded in DevTools,
295  * therefore DevTools should always be available when they are called.
296  */
297 const webExtensionsMethods = [
298   "createCommandsForTabForWebExtension",
299   "getTheme",
300   "openBrowserConsole",
304  * Compatibility layer for other third parties.
305  */
306 const otherToolMethods = [
307   // gDevTools.showToolboxForTab is used by wptrunner to start devtools
308   // https://github.com/web-platform-tests/wpt
309   // And also, Quick Actions on URL bar.
310   "showToolboxForTab",
311   // Used for Quick Actions on URL bar.
312   "hasToolboxForTab",
313   // Used for Quick Actions test.
314   "getToolboxForTab",
317 for (const method of [...webExtensionsMethods, ...otherToolMethods]) {
318   DevToolsShim[method] = function () {
319     if (!this.isEnabled()) {
320       throw new Error(
321         "Could not call a DevToolsShim webextension method ('" +
322           method +
323           "'): DevTools are not initialized."
324       );
325     }
327     this.initDevTools();
328     return this._gDevTools[method].apply(this._gDevTools, arguments);
329   };