tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

devtools.js (31629B)


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