Bug 1890689 accumulate input in LargerReceiverBlockSizeThanDesiredBuffering GTest...
[gecko.git] / devtools / client / framework / devtools.js
blob05e340a454ccdc9050717cb6ccb8f88e19bce111
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 "use strict";
7 const { DevToolsShim } = ChromeUtils.importESModule(
8   "chrome://devtools-startup/content/DevToolsShim.sys.mjs"
9 );
11 const { DEFAULT_SANDBOX_NAME } = ChromeUtils.importESModule(
12   "resource://devtools/shared/loader/Loader.sys.mjs"
15 const lazy = {};
16 ChromeUtils.defineESModuleGetters(lazy, {
17   BrowserToolboxLauncher:
18     "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs",
19 });
21 loader.lazyRequireGetter(
22   this,
23   "LocalTabCommandsFactory",
24   "resource://devtools/client/framework/local-tab-commands-factory.js",
25   true
27 loader.lazyRequireGetter(
28   this,
29   "CommandsFactory",
30   "resource://devtools/shared/commands/commands-factory.js",
31   true
33 loader.lazyRequireGetter(
34   this,
35   "ToolboxHostManager",
36   "resource://devtools/client/framework/toolbox-host-manager.js",
37   true
39 loader.lazyRequireGetter(
40   this,
41   "BrowserConsoleManager",
42   "resource://devtools/client/webconsole/browser-console-manager.js",
43   true
45 loader.lazyRequireGetter(
46   this,
47   "Toolbox",
48   "resource://devtools/client/framework/toolbox.js",
49   true
52 loader.lazyRequireGetter(
53   this,
54   "Telemetry",
55   "resource://devtools/client/shared/telemetry.js"
58 const {
59   defaultTools: DefaultTools,
60   defaultThemes: DefaultThemes,
61 } = require("resource://devtools/client/definitions.js");
62 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
63 const {
64   getTheme,
65   setTheme,
66   getAutoTheme,
67   addThemeObserver,
68   removeThemeObserver,
69 } = require("resource://devtools/client/shared/theme.js");
71 const FORBIDDEN_IDS = new Set(["toolbox", ""]);
72 const MAX_ORDINAL = 99;
73 const POPUP_DEBUG_PREF = "devtools.popups.debug";
74 const DEVTOOLS_ALWAYS_ON_TOP = "devtools.toolbox.alwaysOnTop";
76 /**
77  * DevTools is a class that represents a set of developer tools, it holds a
78  * set of tools and keeps track of open toolboxes in the browser.
79  */
80 function DevTools() {
81   // We should be careful to always load a unique instance of this module:
82   // - only in the parent process
83   // - only in the "shared JSM global" spawn by mozJSModuleLoader
84   //   The server codebase typically use another global named "DevTools global",
85   //   which will load duplicated instances of all the modules -or- another
86   //   DevTools module loader named "DevTools (Server Module loader)".
87   //   Also the realm location is appended the loading callsite, so only check
88   //   the beginning of the string.
89   if (
90     Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT ||
91     !Cu.getRealmLocation(globalThis).startsWith(DEFAULT_SANDBOX_NAME)
92   ) {
93     throw new Error(
94       "This module should be loaded in the parent process only, in the shared global."
95     );
96   }
98   this._tools = new Map(); // Map<toolId, tool>
99   this._themes = new Map(); // Map<themeId, theme>
100   this._toolboxesPerCommands = new Map(); // Map<commands, toolbox>
101   // List of toolboxes that are still in process of creation
102   this._creatingToolboxes = new Map(); // Map<commands, toolbox Promise>
104   EventEmitter.decorate(this);
105   this._telemetry = new Telemetry();
106   this._telemetry.setEventRecordingEnabled(true);
108   // List of all commands of debugged local Web Extension.
109   this._commandsPromiseByWebExtId = new Map(); // Map<extensionId, commands>
111   // Listen for changes to the theme pref.
112   this._onThemeChanged = this._onThemeChanged.bind(this);
113   addThemeObserver(this._onThemeChanged);
115   // This is important step in initialization codepath where we are going to
116   // start registering all default tools and themes: create menuitems, keys, emit
117   // related events.
118   this.registerDefaults();
120   // Register this DevTools instance on the DevToolsShim, which is used by non-devtools
121   // code to interact with DevTools.
122   DevToolsShim.register(this);
125 DevTools.prototype = {
126   // The windowtype of the main window, used in various tools. This may be set
127   // to something different by other gecko apps.
128   chromeWindowType: "navigator:browser",
130   registerDefaults() {
131     // Ensure registering items in the sorted order (getDefault* functions
132     // return sorted lists)
133     this.getDefaultTools().forEach(definition => this.registerTool(definition));
134     this.getDefaultThemes().forEach(definition =>
135       this.registerTheme(definition)
136     );
137   },
139   unregisterDefaults() {
140     for (const definition of this.getToolDefinitionArray()) {
141       this.unregisterTool(definition.id);
142     }
143     for (const definition of this.getThemeDefinitionArray()) {
144       this.unregisterTheme(definition.id);
145     }
146   },
148   /**
149    * Register a new developer tool.
150    *
151    * A definition is a light object that holds different information about a
152    * developer tool. This object is not supposed to have any operational code.
153    * See it as a "manifest".
154    * The only actual code lives in the build() function, which will be used to
155    * start an instance of this tool.
156    *
157    * Each toolDefinition has the following properties:
158    * - id: Unique identifier for this tool (string|required)
159    * - visibilityswitch: Property name to allow us to hide this tool from the
160    *                     DevTools Toolbox.
161    *                     A falsy value indicates that it cannot be hidden.
162    * - icon: URL pointing to a graphic which will be used as the src for an
163    *         16x16 img tag (string|required)
164    * - url: URL pointing to a XUL/XHTML document containing the user interface
165    *        (string|required)
166    * - label: Localized name for the tool to be displayed to the user
167    *          (string|required)
168    * - hideInOptions: Boolean indicating whether or not this tool should be
169                       shown in toolbox options or not. Defaults to false.
170    *                  (boolean)
171    * - build: Function that takes an iframe, which has been populated with the
172    *          markup from |url|, and also the toolbox containing the panel.
173    *          And returns an instance of ToolPanel (function|required)
174    */
175   registerTool(toolDefinition) {
176     const toolId = toolDefinition.id;
178     if (!toolId || FORBIDDEN_IDS.has(toolId)) {
179       throw new Error("Invalid definition.id");
180     }
182     // Make sure that additional tools will always be able to be hidden.
183     // When being called from main.js, defaultTools has not yet been exported.
184     // But, we can assume that in this case, it is a default tool.
185     if (!DefaultTools.includes(toolDefinition)) {
186       toolDefinition.visibilityswitch = "devtools." + toolId + ".enabled";
187     }
189     this._tools.set(toolId, toolDefinition);
191     this.emit("tool-registered", toolId);
192   },
194   /**
195    * Removes all tools that match the given |toolId|
196    * Needed so that add-ons can remove themselves when they are deactivated
197    *
198    * @param {string} toolId
199    *        The id of the tool to unregister.
200    * @param {boolean} isQuitApplication
201    *        true to indicate that the call is due to app quit, so we should not
202    *        cause a cascade of costly events
203    */
204   unregisterTool(toolId, isQuitApplication) {
205     this._tools.delete(toolId);
207     if (!isQuitApplication) {
208       this.emit("tool-unregistered", toolId);
209     }
210   },
212   /**
213    * Sorting function used for sorting tools based on their ordinals.
214    */
215   ordinalSort(d1, d2) {
216     const o1 = typeof d1.ordinal == "number" ? d1.ordinal : MAX_ORDINAL;
217     const o2 = typeof d2.ordinal == "number" ? d2.ordinal : MAX_ORDINAL;
218     return o1 - o2;
219   },
221   getDefaultTools() {
222     return DefaultTools.sort(this.ordinalSort);
223   },
225   getAdditionalTools() {
226     const tools = [];
227     for (const [, value] of this._tools) {
228       if (!DefaultTools.includes(value)) {
229         tools.push(value);
230       }
231     }
232     return tools.sort(this.ordinalSort);
233   },
235   getDefaultThemes() {
236     return DefaultThemes.sort(this.ordinalSort);
237   },
239   /**
240    * Get a tool definition if it exists and is enabled.
241    *
242    * @param {string} toolId
243    *        The id of the tool to show
244    *
245    * @return {ToolDefinition|null} tool
246    *         The ToolDefinition for the id or null.
247    */
248   getToolDefinition(toolId) {
249     const tool = this._tools.get(toolId);
250     if (!tool) {
251       return null;
252     } else if (!tool.visibilityswitch) {
253       return tool;
254     }
256     const enabled = Services.prefs.getBoolPref(tool.visibilityswitch, true);
258     return enabled ? tool : null;
259   },
261   /**
262    * Allow ToolBoxes to get at the list of tools that they should populate
263    * themselves with.
264    *
265    * @return {Map} tools
266    *         A map of the the tool definitions registered in this instance
267    */
268   getToolDefinitionMap() {
269     const tools = new Map();
271     for (const [id, definition] of this._tools) {
272       if (this.getToolDefinition(id)) {
273         tools.set(id, definition);
274       }
275     }
277     return tools;
278   },
280   /**
281    * Tools have an inherent ordering that can't be represented in a Map so
282    * getToolDefinitionArray provides an alternative representation of the
283    * definitions sorted by ordinal value.
284    *
285    * @return {Array} tools
286    *         A sorted array of the tool definitions registered in this instance
287    */
288   getToolDefinitionArray() {
289     const definitions = [];
291     for (const [id, definition] of this._tools) {
292       if (this.getToolDefinition(id)) {
293         definitions.push(definition);
294       }
295     }
297     return definitions.sort(this.ordinalSort);
298   },
300   /**
301    * Returns the name of the current theme for devtools.
302    *
303    * @return {string} theme
304    *         The name of the current devtools theme.
305    */
306   getTheme() {
307     return getTheme();
308   },
310   /**
311    * Returns the name of the default (auto) theme for devtools.
312    *
313    * @return {string} theme
314    */
315   getAutoTheme() {
316     return getAutoTheme();
317   },
319   /**
320    * Called when the developer tools theme changes.
321    */
322   _onThemeChanged() {
323     this.emit("theme-changed", getTheme());
324   },
326   /**
327    * Register a new theme for developer tools toolbox.
328    *
329    * A definition is a light object that holds various information about a
330    * theme.
331    *
332    * Each themeDefinition has the following properties:
333    * - id: Unique identifier for this theme (string|required)
334    * - label: Localized name for the theme to be displayed to the user
335    *          (string|required)
336    * - stylesheets: Array of URLs pointing to a CSS document(s) containing
337    *                the theme style rules (array|required)
338    * - classList: Array of class names identifying the theme within a document.
339    *              These names are set to document element when applying
340    *              the theme (array|required)
341    * - onApply: Function that is executed by the framework when the theme
342    *            is applied. The function takes the current iframe window
343    *            and the previous theme id as arguments (function)
344    * - onUnapply: Function that is executed by the framework when the theme
345    *            is unapplied. The function takes the current iframe window
346    *            and the new theme id as arguments (function)
347    */
348   registerTheme(themeDefinition) {
349     const themeId = themeDefinition.id;
351     if (!themeId) {
352       throw new Error("Invalid theme id");
353     }
355     if (this._themes.get(themeId)) {
356       throw new Error("Theme with the same id is already registered");
357     }
359     this._themes.set(themeId, themeDefinition);
361     this.emit("theme-registered", themeId);
362   },
364   /**
365    * Removes an existing theme from the list of registered themes.
366    * Needed so that add-ons can remove themselves when they are deactivated
367    *
368    * @param {string|object} theme
369    *        Definition or the id of the theme to unregister.
370    */
371   unregisterTheme(theme) {
372     let themeId = null;
373     if (typeof theme == "string") {
374       themeId = theme;
375       theme = this._themes.get(theme);
376     } else {
377       themeId = theme.id;
378     }
380     const currTheme = getTheme();
382     // Note that we can't check if `theme` is an item
383     // of `DefaultThemes` as we end up reloading definitions
384     // module and end up with different theme objects
385     const isCoreTheme = DefaultThemes.some(t => t.id === themeId);
387     // Reset the theme if an extension theme that's currently applied
388     // is being removed.
389     // Ignore shutdown since addons get disabled during that time.
390     if (
391       !Services.startup.shuttingDown &&
392       !isCoreTheme &&
393       theme.id == currTheme
394     ) {
395       setTheme("auto");
397       this.emit("theme-unregistered", theme);
398     }
400     this._themes.delete(themeId);
401   },
403   /**
404    * Get a theme definition if it exists.
405    *
406    * @param {string} themeId
407    *        The id of the theme
408    *
409    * @return {ThemeDefinition|null} theme
410    *         The ThemeDefinition for the id or null.
411    */
412   getThemeDefinition(themeId) {
413     const theme = this._themes.get(themeId);
414     if (!theme) {
415       return null;
416     }
417     return theme;
418   },
420   /**
421    * Get map of registered themes.
422    *
423    * @return {Map} themes
424    *         A map of the the theme definitions registered in this instance
425    */
426   getThemeDefinitionMap() {
427     const themes = new Map();
429     for (const [id, definition] of this._themes) {
430       if (this.getThemeDefinition(id)) {
431         themes.set(id, definition);
432       }
433     }
435     return themes;
436   },
438   /**
439    * Get registered themes definitions sorted by ordinal value.
440    *
441    * @return {Array} themes
442    *         A sorted array of the theme definitions registered in this instance
443    */
444   getThemeDefinitionArray() {
445     const definitions = [];
447     for (const [id, definition] of this._themes) {
448       if (this.getThemeDefinition(id)) {
449         definitions.push(definition);
450       }
451     }
453     return definitions.sort(this.ordinalSort);
454   },
456   /**
457    * Called from SessionStore.sys.mjs in mozilla-central when saving the current state.
458    *
459    * @param {Object} state
460    *                 A SessionStore state object that gets modified by reference
461    */
462   saveDevToolsSession(state) {
463     state.browserConsole =
464       BrowserConsoleManager.getBrowserConsoleSessionState();
465     state.browserToolbox =
466       lazy.BrowserToolboxLauncher.getBrowserToolboxSessionState();
467   },
469   /**
470    * Restore the devtools session state as provided by SessionStore.
471    */
472   async restoreDevToolsSession({ browserConsole, browserToolbox }) {
473     if (browserToolbox) {
474       lazy.BrowserToolboxLauncher.init();
475     }
477     if (browserConsole && !BrowserConsoleManager.getBrowserConsole()) {
478       await BrowserConsoleManager.toggleBrowserConsole();
479     }
480   },
482   /**
483    * Boolean, true, if we never opened a toolbox.
484    * Used to implement the telemetry tracking toolbox opening.
485    */
486   _firstShowToolbox: true,
488   /**
489    * Show a Toolbox for a given "commands" (either by creating a new one, or if a
490    * toolbox already exists for the commands, by bringing to the front the
491    * existing one).
492    *
493    * If a Toolbox already exists, we will still update it based on some of the
494    * provided parameters:
495    *   - if |toolId| is provided then the toolbox will switch to the specified
496    *     tool.
497    *   - if |hostType| is provided then the toolbox will be switched to the
498    *     specified HostType.
499    *
500    * @param {Commands Object} commands
501    *         The commands object which designates which context the toolbox will debug
502    * @param {Object}
503    *        - {String} toolId
504    *          The id of the tool to show
505    *        - {Toolbox.HostType} hostType
506    *          The type of host (bottom, window, left, right)
507    *        - {object} hostOptions
508    *          Options for host specifically
509    *        - {Number} startTime
510    *          Indicates the time at which the user event related to
511    *          this toolbox opening started. This is a `Cu.now()` timing.
512    *        - {string} reason
513    *          Reason the tool was opened
514    *        - {boolean} raise
515    *          Whether we need to raise the toolbox or not.
516    *
517    * @return {Toolbox} toolbox
518    *        The toolbox that was opened
519    */
520   async showToolbox(
521     commands,
522     {
523       toolId,
524       hostType,
525       startTime,
526       raise = true,
527       reason = "toolbox_show",
528       hostOptions,
529     } = {}
530   ) {
531     let toolbox = this._toolboxesPerCommands.get(commands);
533     if (toolbox) {
534       if (hostType != null && toolbox.hostType != hostType) {
535         await toolbox.switchHost(hostType);
536       }
538       if (toolId != null) {
539         // selectTool will either select the tool if not currently selected, or wait for
540         // the tool to be loaded if needed.
541         await toolbox.selectTool(toolId, reason);
542       }
544       if (raise) {
545         await toolbox.raise();
546       }
547     } else {
548       // Toolbox creation is async, we have to be careful about races.
549       // Check if we are already waiting for a Toolbox for the provided
550       // commands before creating a new one.
551       const promise = this._creatingToolboxes.get(commands);
552       if (promise) {
553         return promise;
554       }
555       const toolboxPromise = this._createToolbox(
556         commands,
557         toolId,
558         hostType,
559         hostOptions
560       );
561       this._creatingToolboxes.set(commands, toolboxPromise);
562       toolbox = await toolboxPromise;
563       this._creatingToolboxes.delete(commands);
565       if (startTime) {
566         this.logToolboxOpenTime(toolbox, startTime);
567       }
568       this._firstShowToolbox = false;
569     }
571     // We send the "enter" width here to ensure it is always sent *after*
572     // the "open" event.
573     const width = Math.ceil(toolbox.win.outerWidth / 50) * 50;
574     const panelName = this.makeToolIdHumanReadable(
575       toolId || toolbox.defaultToolId
576     );
577     this._telemetry.addEventProperty(
578       toolbox,
579       "enter",
580       panelName,
581       null,
582       "width",
583       width
584     );
586     return toolbox;
587   },
589   /**
590    * Show the toolbox for a given tab. If a toolbox already exists for this tab
591    * the existing toolbox will be raised. Otherwise a new toolbox is created.
592    *
593    * Relies on `showToolbox`, see its jsDoc for additional information and
594    * arguments description.
595    *
596    * Also used by 3rd party tools (eg wptrunner) and exposed by
597    * DevToolsShim.sys.mjs.
598    *
599    * @param {XULTab} tab
600    *        The tab the toolbox will debug
601    * @param {Object} options
602    *        Various options that will be forwarded to `showToolbox`. See the
603    *        JSDoc on this method.
604    */
605   async showToolboxForTab(
606     tab,
607     { toolId, hostType, startTime, raise, reason, hostOptions } = {}
608   ) {
609     // Popups are debugged via the toolbox of their opener document/tab.
610     // So avoid opening dedicated toolbox for them.
611     if (
612       tab.linkedBrowser.browsingContext.opener &&
613       Services.prefs.getBoolPref(POPUP_DEBUG_PREF)
614     ) {
615       const openerTab = tab.ownerGlobal.gBrowser.getTabForBrowser(
616         tab.linkedBrowser.browsingContext.opener.embedderElement
617       );
618       const openerCommands = await LocalTabCommandsFactory.getCommandsForTab(
619         openerTab
620       );
621       if (this.getToolboxForCommands(openerCommands)) {
622         console.log(
623           "Can't open a toolbox for this document as this is debugged from its opener tab"
624         );
625         return null;
626       }
627     }
628     const commands = await LocalTabCommandsFactory.createCommandsForTab(tab);
629     return this.showToolbox(commands, {
630       toolId,
631       hostType,
632       startTime,
633       raise,
634       reason,
635       hostOptions,
636     });
637   },
639   /**
640    * Open a Toolbox in a dedicated top-level window for debugging a local WebExtension.
641    * This will re-open a previously opened toolbox if we try to re-debug the same extension.
642    *
643    * Note that this will spawn a new DevToolsClient.
644    *
645    * @param {String} extensionId
646    *        ID of the extension to debug.
647    * @param {Object} (optional)
648    *        - {String} toolId
649    *          The id of the tool to show
650    */
651   async showToolboxForWebExtension(extensionId, { toolId } = {}) {
652     // Ensure spawning only one commands instance per extension at a time by caching its commands.
653     // showToolbox will later reopen the previously opened toolbox if called with the same
654     // commands.
655     let commandsPromise = this._commandsPromiseByWebExtId.get(extensionId);
656     if (!commandsPromise) {
657       commandsPromise = CommandsFactory.forAddon(extensionId);
658       this._commandsPromiseByWebExtId.set(extensionId, commandsPromise);
659     }
660     const commands = await commandsPromise;
661     commands.client.once("closed").then(() => {
662       this._commandsPromiseByWebExtId.delete(extensionId);
663     });
665     return this.showToolbox(commands, {
666       hostType: Toolbox.HostType.WINDOW,
667       hostOptions: {
668         // The toolbox is always displayed on top so that we can keep
669         // the DevTools visible while interacting with the Firefox window.
670         alwaysOnTop: Services.prefs.getBoolPref(DEVTOOLS_ALWAYS_ON_TOP, false),
671       },
672       toolId,
673     });
674   },
676   /**
677    * Log telemetry related to toolbox opening.
678    * Two distinct probes are logged. One for cold startup, when we open the very first
679    * toolbox. This one includes devtools framework loading. And a second one for all
680    * subsequent toolbox opening, which should all be faster.
681    * These two probes are indexed by Tool ID.
682    *
683    * @param {String} toolbox
684    *        Toolbox instance.
685    * @param {Number} startTime
686    *        Indicates the time at which the user event related to the toolbox
687    *        opening started. This is a `Cu.now()` timing.
688    */
689   logToolboxOpenTime(toolbox, startTime) {
690     const toolId = toolbox.currentToolId || toolbox.defaultToolId;
691     const delay = Cu.now() - startTime;
692     const panelName = this.makeToolIdHumanReadable(toolId);
694     const telemetryKey = this._firstShowToolbox
695       ? "DEVTOOLS_COLD_TOOLBOX_OPEN_DELAY_MS"
696       : "DEVTOOLS_WARM_TOOLBOX_OPEN_DELAY_MS";
697     this._telemetry.getKeyedHistogramById(telemetryKey).add(toolId, delay);
699     const browserWin = toolbox.topWindow;
700     this._telemetry.addEventProperty(
701       browserWin,
702       "open",
703       "tools",
704       null,
705       "first_panel",
706       panelName
707     );
708   },
710   makeToolIdHumanReadable(toolId) {
711     if (/^[0-9a-fA-F]{40}_temporary-addon/.test(toolId)) {
712       return "temporary-addon";
713     }
715     let matches = toolId.match(
716       /^_([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})_/
717     );
718     if (matches && matches.length === 2) {
719       return matches[1];
720     }
722     matches = toolId.match(/^_?(.*)-\d+-\d+-devtools-panel$/);
723     if (matches && matches.length === 2) {
724       return matches[1];
725     }
727     return toolId;
728   },
730   /**
731    * Unconditionally create a new Toolbox instance for the provided commands.
732    * See `showToolbox` for the arguments' jsdoc.
733    */
734   async _createToolbox(commands, toolId, hostType, hostOptions) {
735     const manager = new ToolboxHostManager(commands, hostType, hostOptions);
737     const toolbox = await manager.create(toolId);
739     this._toolboxesPerCommands.set(commands, toolbox);
741     toolbox.once("destroy", () => {
742       this.emit("toolbox-destroy", toolbox);
743     });
745     toolbox.once("destroyed", () => {
746       this._toolboxesPerCommands.delete(commands);
747       this.emit("toolbox-destroyed", toolbox);
748     });
750     await toolbox.open();
751     this.emit("toolbox-ready", toolbox);
753     return toolbox;
754   },
756   /**
757    * Return the toolbox for a given commands object.
758    *
759    * @param  {Commands Object} commands
760    *         Debugging context commands that owns this toolbox
761    *
762    * @return {Toolbox} toolbox
763    *         The toolbox that is debugging the given context designated by the commands
764    */
765   getToolboxForCommands(commands) {
766     return this._toolboxesPerCommands.get(commands);
767   },
769   /**
770    * TabDescriptorFront requires a synchronous method and don't have a reference to its
771    * related commands object. So expose something handcrafted just for this.
772    */
773   getToolboxForDescriptorFront(descriptorFront) {
774     for (const [commands, toolbox] of this._toolboxesPerCommands) {
775       if (commands.descriptorFront == descriptorFront) {
776         return toolbox;
777       }
778     }
779     return null;
780   },
782   /**
783    * Retrieve an existing toolbox for the provided tab if it was created before.
784    * Returns null otherwise.
785    *
786    * @param {XULTab} tab
787    *        The browser tab.
788    * @return {Toolbox}
789    *        Returns tab's toolbox object.
790    */
791   getToolboxForTab(tab) {
792     return this.getToolboxes().find(
793       t => t.commands.descriptorFront.localTab === tab
794     );
795   },
797   /**
798    * Close the toolbox for a given tab.
799    *
800    * @return {Promise} Returns a promise that resolves either:
801    *         - immediately if no Toolbox was found
802    *         - or after toolbox.destroy() resolved if a Toolbox was found
803    */
804   async closeToolboxForTab(tab) {
805     const commands = await LocalTabCommandsFactory.getCommandsForTab(tab);
807     let toolbox = await this._creatingToolboxes.get(commands);
808     if (!toolbox) {
809       toolbox = this._toolboxesPerCommands.get(commands);
810     }
811     if (!toolbox) {
812       return;
813     }
814     await toolbox.destroy();
815   },
817   /**
818    * Compatibility layer for web-extensions. Used by DevToolsShim for
819    * browser/components/extensions/ext-devtools.js
820    *
821    * web-extensions need to use dedicated instances of Commands and cannot reuse the
822    * cached instances managed by DevTools.
823    * Note that is will end up being cached in WebExtension codebase, via
824    * DevToolsExtensionPageContextParent.getDevToolsCommands.
825    */
826   createCommandsForTabForWebExtension(tab) {
827     return CommandsFactory.forTab(tab, { isWebExtension: true });
828   },
830   /**
831    * Compatibility layer for web-extensions. Used by DevToolsShim for
832    * toolkit/components/extensions/ext-c-toolkit.js
833    */
834   openBrowserConsole() {
835     const {
836       BrowserConsoleManager,
837     } = require("resource://devtools/client/webconsole/browser-console-manager.js");
838     BrowserConsoleManager.openBrowserConsoleOrFocus();
839   },
841   /**
842    * Called from the DevToolsShim, used by nsContextMenu.js.
843    *
844    * @param {XULTab} tab
845    *        The browser tab on which inspect node was used.
846    * @param {ElementIdentifier} domReference
847    *        Identifier generated by ContentDOMReference. It is a unique pair of
848    *        BrowsingContext ID and a numeric ID.
849    * @param {Number} startTime
850    *        Optional, indicates the time at which the user event related to this node
851    *        inspection started. This is a `Cu.now()` timing.
852    * @return {Promise} a promise that resolves when the node is selected in the inspector
853    *         markup view.
854    */
855   async inspectNode(tab, domReference, startTime) {
856     const toolbox = await gDevTools.showToolboxForTab(tab, {
857       toolId: "inspector",
858       startTime,
859       reason: "inspect_dom",
860     });
861     const inspector = toolbox.getCurrentPanel();
863     const nodeFront =
864       await inspector.inspectorFront.getNodeActorFromContentDomReference(
865         domReference
866       );
867     if (!nodeFront) {
868       return;
869     }
871     // "new-node-front" tells us when the node has been selected, whether the
872     // browser is remote or not.
873     const onNewNode = inspector.selection.once("new-node-front");
874     // Select the final node
875     inspector.selection.setNodeFront(nodeFront, {
876       reason: "browser-context-menu",
877     });
879     await onNewNode;
880     // Now that the node has been selected, wait until the inspector is
881     // fully updated.
882     await inspector.once("inspector-updated");
883   },
885   /**
886    * Called from the DevToolsShim, used by nsContextMenu.js.
887    *
888    * @param {XULTab} tab
889    *        The browser tab on which inspect accessibility was used.
890    * @param {ElementIdentifier} domReference
891    *        Identifier generated by ContentDOMReference. It is a unique pair of
892    *        BrowsingContext ID and a numeric ID.
893    * @param {Number} startTime
894    *        Optional, indicates the time at which the user event related to this
895    *        node inspection started. This is a `Cu.now()` timing.
896    * @return {Promise} a promise that resolves when the accessible object is
897    *         selected in the accessibility inspector.
898    */
899   async inspectA11Y(tab, domReference, startTime) {
900     const toolbox = await gDevTools.showToolboxForTab(tab, {
901       toolId: "accessibility",
902       startTime,
903     });
904     const inspectorFront = await toolbox.target.getFront("inspector");
905     const nodeFront = await inspectorFront.getNodeActorFromContentDomReference(
906       domReference
907     );
908     if (!nodeFront) {
909       return;
910     }
912     // Select the accessible object in the panel and wait for the event that
913     // tells us it has been done.
914     const a11yPanel = toolbox.getCurrentPanel();
915     const onSelected = a11yPanel.once("new-accessible-front-selected");
916     a11yPanel.selectAccessibleForNode(nodeFront, "browser-context-menu");
917     await onSelected;
918   },
920   /**
921    * Either the DevTools Loader has been destroyed or firefox is shutting down.
922    * @param {boolean} shuttingDown
923    *        True if firefox is currently shutting down. We may prevent doing
924    *        some cleanups to speed it up. Otherwise everything need to be
925    *        cleaned up in order to be able to load devtools again.
926    */
927   destroy({ shuttingDown }) {
928     // Do not cleanup everything during firefox shutdown.
929     if (!shuttingDown) {
930       for (const [, toolbox] of this._toolboxesPerCommands) {
931         toolbox.destroy();
932       }
933     }
935     for (const [key] of this.getToolDefinitionMap()) {
936       this.unregisterTool(key, true);
937     }
939     gDevTools.unregisterDefaults();
941     removeThemeObserver(this._onThemeChanged);
943     // Do not unregister devtools from the DevToolsShim if the destroy is caused by an
944     // application shutdown. For instance SessionStore needs to save the Browser Toolbox
945     // state on shutdown.
946     if (!shuttingDown) {
947       // Notify the DevToolsShim that DevTools are no longer available, particularly if
948       // the destroy was caused by disabling/removing DevTools.
949       DevToolsShim.unregister();
950     }
952     // Cleaning down the toolboxes: i.e.
953     //   for (let [, toolbox] of this._toolboxesPerCommands) toolbox.destroy();
954     // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow
955   },
957   /**
958    * Returns the array of the existing toolboxes.
959    *
960    * @return {Array<Toolbox>}
961    *   An array of toolboxes.
962    */
963   getToolboxes() {
964     return Array.from(this._toolboxesPerCommands.values());
965   },
967   /**
968    * Returns whether the given tab has toolbox.
969    *
970    * @param {XULTab} tab
971    *        The browser tab.
972    * @return {boolean}
973    *        Returns true if the tab has toolbox.
974    */
975   hasToolboxForTab(tab) {
976     return this.getToolboxes().some(
977       t => t.commands.descriptorFront.localTab === tab
978     );
979   },
982 const gDevTools = (exports.gDevTools = new DevTools());