tor-browser

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

DevToolsStartup.sys.mjs (49277B)


      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 /**
      6 * This XPCOM component is loaded very early.
      7 * Be careful to lazy load dependencies as much as possible.
      8 *
      9 * It manages all the possible entry points for DevTools:
     10 * - Handles command line arguments like -jsconsole,
     11 * - Register all key shortcuts,
     12 * - Listen for "Browser Tools" system menu opening, under "Tools",
     13 * - Inject the wrench icon in toolbar customization, which is used
     14 *   by the "Browser Tools" list displayed in the hamburger menu,
     15 * - Register the JSON Viewer protocol handler.
     16 * - Inject the profiler recording button in toolbar customization.
     17 *
     18 * Only once any of these entry point is fired, this module ensures starting
     19 * core modules like 'devtools-browser.js' that hooks the browser windows
     20 * and ensure setting up tools.
     21 */
     22 
     23 const kDebuggerPrefs = [
     24  "devtools.debugger.remote-enabled",
     25  "devtools.chrome.enabled",
     26 ];
     27 
     28 const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled";
     29 
     30 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
     31 
     32 const lazy = {};
     33 ChromeUtils.defineESModuleGetters(lazy, {
     34  CustomizableUI:
     35    "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
     36  CustomizableWidgets:
     37    "moz-src:///browser/components/customizableui/CustomizableWidgets.sys.mjs",
     38  PanelMultiView:
     39    "moz-src:///browser/components/customizableui/PanelMultiView.sys.mjs",
     40  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     41  ProfilerMenuButton:
     42    "resource://devtools/client/performance-new/popup/menu-button.sys.mjs",
     43  WebChannel: "resource://gre/modules/WebChannel.sys.mjs",
     44 });
     45 
     46 // We don't want to spend time initializing the full loader here so we create
     47 // our own lazy require.
     48 ChromeUtils.defineLazyGetter(lazy, "Telemetry", function () {
     49  const { require } = ChromeUtils.importESModule(
     50    "resource://devtools/shared/loader/Loader.sys.mjs"
     51  );
     52  // eslint-disable-next-line no-shadow
     53  const Telemetry = require("devtools/client/shared/telemetry");
     54 
     55  return Telemetry;
     56 });
     57 
     58 ChromeUtils.defineLazyGetter(lazy, "KeyShortcutsBundle", function () {
     59  return new Localization(["devtools/startup/key-shortcuts.ftl"], true);
     60 });
     61 
     62 /**
     63 * Safely retrieve a localized DevTools key shortcut from KeyShortcutsBundle.
     64 * If the shortcut is not available, this will return null. Consumer code
     65 * should rely on this to skip unavailable shortcuts.
     66 *
     67 * Note that all shortcuts should always be available, but there is a notable
     68 * exception, which is why we have to do this. When a localization change is
     69 * uplifted to beta, language packs will not be updated immediately when the
     70 * updated beta is available.
     71 *
     72 * This means that language pack users might get a new Beta version but will not
     73 * have a language pack with the new strings yet.
     74 *
     75 * @param {string} id
     76 * @returns {string|null}
     77 */
     78 function getLocalizedKeyShortcut(id) {
     79  try {
     80    return lazy.KeyShortcutsBundle.formatValueSync(id);
     81  } catch (e) {
     82    console.error("Failed to retrieve DevTools localized shortcut for id", id);
     83    return null;
     84  }
     85 }
     86 
     87 ChromeUtils.defineLazyGetter(lazy, "KeyShortcuts", function () {
     88  const isMac = AppConstants.platform == "macosx";
     89 
     90  // Common modifier shared by most key shortcuts
     91  const modifiers = isMac ? "accel,alt" : "accel,shift";
     92 
     93  // List of all key shortcuts triggering installation UI
     94  // `id` should match tool's id from client/definitions.js
     95  const shortcuts = [
     96    // The following keys are also registered in /client/menus.js
     97    // And should be synced.
     98 
     99    // Both are toggling the toolbox on the last selected panel
    100    // or the default one.
    101    {
    102      id: "toggleToolbox",
    103      shortcut: getLocalizedKeyShortcut("devtools-commandkey-toggle-toolbox"),
    104      modifiers,
    105    },
    106    // All locales are using F12
    107    {
    108      id: "toggleToolboxF12",
    109      shortcut: getLocalizedKeyShortcut(
    110        "devtools-commandkey-toggle-toolbox-f12"
    111      ),
    112      modifiers: "", // F12 is the only one without modifiers
    113    },
    114    // Open the Browser Toolbox
    115    {
    116      id: "browserToolbox",
    117      shortcut: getLocalizedKeyShortcut("devtools-commandkey-browser-toolbox"),
    118      modifiers: "accel,alt,shift",
    119    },
    120    // Open the Browser Console
    121    {
    122      id: "browserConsole",
    123      shortcut: getLocalizedKeyShortcut("devtools-commandkey-browser-console"),
    124      modifiers: "accel,shift",
    125    },
    126    // Toggle the Responsive Design Mode
    127    {
    128      id: "responsiveDesignMode",
    129      shortcut: getLocalizedKeyShortcut(
    130        "devtools-commandkey-responsive-design-mode"
    131      ),
    132      modifiers,
    133    },
    134    // The following keys are also registered in /client/definitions.js
    135    // and should be synced.
    136 
    137    // Key for opening the Inspector
    138    {
    139      toolId: "inspector",
    140      shortcut: getLocalizedKeyShortcut("devtools-commandkey-inspector"),
    141      modifiers,
    142    },
    143    // Key for opening the Web Console
    144    {
    145      toolId: "webconsole",
    146      shortcut: getLocalizedKeyShortcut("devtools-commandkey-webconsole"),
    147      modifiers,
    148    },
    149    // Key for opening the Debugger
    150    {
    151      toolId: "jsdebugger",
    152      shortcut: getLocalizedKeyShortcut("devtools-commandkey-jsdebugger"),
    153      modifiers,
    154    },
    155    // Key for opening the Network Monitor
    156    {
    157      toolId: "netmonitor",
    158      shortcut: getLocalizedKeyShortcut("devtools-commandkey-netmonitor"),
    159      modifiers,
    160    },
    161    // Key for opening the Style Editor
    162    {
    163      toolId: "styleeditor",
    164      shortcut: getLocalizedKeyShortcut("devtools-commandkey-styleeditor"),
    165      modifiers: "shift",
    166    },
    167    // Key for opening the Performance Panel
    168    {
    169      toolId: "performance",
    170      shortcut: getLocalizedKeyShortcut("devtools-commandkey-performance"),
    171      modifiers: "shift",
    172    },
    173    // Key for opening the Storage Panel
    174    {
    175      toolId: "storage",
    176      shortcut: getLocalizedKeyShortcut("devtools-commandkey-storage"),
    177      modifiers: "shift",
    178    },
    179    // Key for opening the DOM Panel
    180    {
    181      toolId: "dom",
    182      shortcut: getLocalizedKeyShortcut("devtools-commandkey-dom"),
    183      modifiers,
    184    },
    185    // Key for opening the Accessibility Panel
    186    {
    187      toolId: "accessibility",
    188      shortcut: getLocalizedKeyShortcut(
    189        "devtools-commandkey-accessibility-f12"
    190      ),
    191      modifiers: "shift",
    192    },
    193  ];
    194 
    195  if (isMac) {
    196    // Add the extra key command for macOS, so you can open the inspector with cmd+shift+C
    197    // like on Chrome DevTools.
    198    shortcuts.push({
    199      id: "inspectorMac",
    200      toolId: "inspector",
    201      shortcut: getLocalizedKeyShortcut("devtools-commandkey-inspector"),
    202      modifiers: "accel,shift",
    203    });
    204  }
    205 
    206  if (lazy.ProfilerMenuButton.isInNavbar()) {
    207    shortcuts.push(...getProfilerKeyShortcuts());
    208  }
    209 
    210  // Allow toggling the JavaScript tracing not only from DevTools UI,
    211  // but also from the web page when it is focused.
    212  if (
    213    Services.prefs.getBoolPref(
    214      "devtools.debugger.features.javascript-tracing",
    215      false
    216    )
    217  ) {
    218    shortcuts.push({
    219      id: "javascriptTracingToggle",
    220      shortcut: getLocalizedKeyShortcut(
    221        "devtools-commandkey-javascript-tracing-toggle"
    222      ),
    223      modifiers: "control,shift",
    224    });
    225  }
    226 
    227  return shortcuts;
    228 });
    229 
    230 function getProfilerKeyShortcuts() {
    231  return [
    232    // Start/stop the profiler
    233    {
    234      id: "profilerStartStop",
    235      shortcut: getLocalizedKeyShortcut(
    236        "devtools-commandkey-profiler-start-stop"
    237      ),
    238      modifiers: "control,shift",
    239    },
    240    // Capture a profile
    241    {
    242      id: "profilerCapture",
    243      shortcut: getLocalizedKeyShortcut("devtools-commandkey-profiler-capture"),
    244      modifiers: "control,shift",
    245    },
    246    // Because it's not uncommon for content or extension to bind this
    247    // shortcut, allow using alt as well for starting and stopping the profiler
    248    {
    249      id: "profilerStartStopAlternate",
    250      shortcut: getLocalizedKeyShortcut(
    251        "devtools-commandkey-profiler-start-stop"
    252      ),
    253      modifiers: "control,shift,alt",
    254    },
    255    {
    256      id: "profilerCaptureAlternate",
    257      shortcut: getLocalizedKeyShortcut("devtools-commandkey-profiler-capture"),
    258      modifiers: "control,shift,alt",
    259    },
    260  ];
    261 }
    262 
    263 /**
    264 * Validate the URL that will be used for the WebChannel for the profiler.
    265 *
    266 * @param {string} targetUrl
    267 * @returns {string}
    268 */
    269 export function validateProfilerWebChannelUrl(targetUrl) {
    270  const frontEndUrl = "https://profiler.firefox.com";
    271 
    272  if (targetUrl !== frontEndUrl) {
    273    // The user can specify either localhost or deploy previews as well as
    274    // the official frontend URL for testing.
    275    if (
    276      // Allow a test URL.
    277      /^https?:\/\/example\.com$/.test(targetUrl) ||
    278      // Allows the following:
    279      //   "http://localhost:4242"
    280      //   "http://localhost:4242/"
    281      //   "http://localhost:3"
    282      //   "http://localhost:334798455"
    283      /^http:\/\/localhost:\d+\/?$/.test(targetUrl) ||
    284      // Allows the following:
    285      //   "https://deploy-preview-1234--perf-html.netlify.com"
    286      //   "https://deploy-preview-1234--perf-html.netlify.com/"
    287      //   "https://deploy-preview-1234567--perf-html.netlify.app"
    288      //   "https://main--perf-html.netlify.app"
    289      /^https:\/\/(?:deploy-preview-\d+|main)--perf-html\.netlify\.(?:com|app)\/?$/.test(
    290        targetUrl
    291      )
    292    ) {
    293      // This URL is one of the allowed ones to be used for configuration.
    294      return targetUrl;
    295    }
    296 
    297    console.error(
    298      `The preference "devtools.performance.recording.ui-base-url" was set to a ` +
    299        "URL that is not allowed. No WebChannel messages will be sent between the " +
    300        `browser and that URL. Falling back to ${frontEndUrl}. Only localhost ` +
    301        "and deploy previews URLs are allowed.",
    302      targetUrl
    303    );
    304  }
    305 
    306  return frontEndUrl;
    307 }
    308 
    309 ChromeUtils.defineLazyGetter(lazy, "ProfilerPopupBackground", function () {
    310  return ChromeUtils.importESModule(
    311    "resource://devtools/client/performance-new/shared/background.sys.mjs"
    312  );
    313 });
    314 
    315 // eslint-disable-next-line jsdoc/require-jsdoc
    316 export class DevToolsStartup {
    317  constructor() {
    318    this.onWindowReady = this.onWindowReady.bind(this);
    319    this.addDevToolsItemsToSubview = this.addDevToolsItemsToSubview.bind(this);
    320    this.onMoreToolsViewShowing = this.onMoreToolsViewShowing.bind(this);
    321    this.toggleProfilerKeyShortcuts =
    322      this.toggleProfilerKeyShortcuts.bind(this);
    323  }
    324  /**
    325   * Boolean flag to check if DevTools have been already initialized or not.
    326   * By initialized, we mean that its main modules are loaded.
    327   */
    328  initialized = false;
    329 
    330  /**
    331   * Boolean flag to check if the devtools initialization was already sent to telemetry.
    332   * We only want to record one devtools entry point per Firefox run, but we are not
    333   * interested in all the entry points.
    334   */
    335  recorded = false;
    336 
    337  get telemetry() {
    338    if (!this._telemetry) {
    339      this._telemetry = new lazy.Telemetry();
    340    }
    341    return this._telemetry;
    342  }
    343 
    344  /**
    345   * Flag that indicates if the developer toggle was already added to customizableUI.
    346   */
    347  developerToggleCreated = false;
    348 
    349  /**
    350   * Flag that indicates if the profiler recording popup was already added to
    351   * customizableUI.
    352   */
    353  profilerRecordingButtonCreated = false;
    354 
    355  isDisabledByPolicy() {
    356    return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false);
    357  }
    358 
    359  handle(cmdLine) {
    360    const flags = this.readCommandLineFlags(cmdLine);
    361 
    362    // handle() can be called after browser startup (e.g. opening links from other apps).
    363    const isInitialLaunch =
    364      cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH;
    365    if (isInitialLaunch) {
    366      // Store devtoolsFlag to check it later in onWindowReady.
    367      this.devtoolsFlag = flags.devtools;
    368 
    369      /* eslint-disable mozilla/balanced-observers */
    370      // We are not expecting to remove those listeners until Firefox closes.
    371 
    372      // Only top level Firefox Windows fire a browser-delayed-startup-finished event
    373      Services.obs.addObserver(
    374        this.onWindowReady,
    375        "browser-delayed-startup-finished"
    376      );
    377 
    378      // Add DevTools menu items to the "More Tools" view.
    379      Services.obs.addObserver(
    380        this.onMoreToolsViewShowing,
    381        "web-developer-tools-view-showing"
    382      );
    383      // Add DevTools menu items so they can be picked up by the customize
    384      // keyboard shortcuts UI.
    385      Services.obs.addObserver(() => {
    386        // Initialize DevTools to create all menuitems in the system menu.
    387        this.initDevTools("CustomKeysUI");
    388      }, "customkeys-ui-showing");
    389      /* eslint-enable mozilla/balanced-observers */
    390 
    391      if (!this.isDisabledByPolicy()) {
    392        if (AppConstants.MOZ_DEV_EDITION) {
    393          // On DevEdition, the developer toggle is displayed by default in the navbar
    394          // area and should be created before the first paint.
    395          this.hookDeveloperToggle();
    396        }
    397 
    398        this.hookProfilerRecordingButton();
    399      }
    400    }
    401 
    402    if (flags.console) {
    403      this.commandLine = true;
    404      this.handleConsoleFlag(cmdLine);
    405    }
    406    if (flags.debugger) {
    407      this.commandLine = true;
    408      const binaryPath =
    409        typeof flags.debugger == "string" ? flags.debugger : null;
    410      this.handleDebuggerFlag(cmdLine, binaryPath);
    411    }
    412 
    413    if (flags.devToolsServer) {
    414      this.handleDevToolsServerFlag(cmdLine, flags.devToolsServer);
    415    }
    416 
    417    // If Firefox is already opened, and DevTools are also already opened,
    418    // try to open links passed via command line arguments.
    419    if (!isInitialLaunch && this.initialized && cmdLine.length) {
    420      this.checkForDebuggerLink(cmdLine);
    421    }
    422  }
    423 
    424  /**
    425   * Lookup in all arguments passed to firefox binary to find
    426   * URLs including a precise location, like this:
    427   *   https://domain.com/file.js:1:10 (URL ending with `:${line}:${number}`)
    428   * When such argument exists, try to open this source and precise location
    429   * in the debugger.
    430   *
    431   * @param {nsICommandLine} cmdLine
    432   */
    433  checkForDebuggerLink(cmdLine) {
    434    const urlFlagIdx = cmdLine.findFlag("url", false);
    435    // Bail out when there is no -url argument, or if that's last and so there is no URL after it.
    436    if (urlFlagIdx == -1 && urlFlagIdx + 1 < cmdLine.length) {
    437      return;
    438    }
    439 
    440    // The following code would only work if we have a top level browser window opened
    441    const window = Services.wm.getMostRecentBrowserWindow();
    442    if (!window) {
    443      return;
    444    }
    445 
    446    const urlParam = cmdLine.getArgument(urlFlagIdx + 1);
    447 
    448    // Avoid processing valid url like:
    449    //   http://foo@user:123
    450    // Note that when loading `http://foo.com` the URL of the default html page will be `http://foo.com/`.
    451    // So that there will always be another `/` after `https://`
    452    if (
    453      (urlParam.startsWith("http://") || urlParam.startsWith("https://")) &&
    454      urlParam.lastIndexOf("/") <= 7
    455    ) {
    456      return;
    457    }
    458 
    459    let match = urlParam.match(/^(?<url>\w+:.+):(?<line>\d+):(?<column>\d+)$/);
    460    if (!match) {
    461      // fallback on only having the line when there is no column
    462      match = urlParam.match(/^(?<url>\w+:.+):(?<line>\d+)?$/);
    463      if (!match) {
    464        return;
    465      }
    466    }
    467 
    468    // line and column are supposed to be 1-based.
    469    const { url, line, column } = match.groups;
    470 
    471    // Debugger internal uses 0-based column number.
    472    // NOTE: Non-debugger view-source doesn't use column number.
    473    const columnOneBased = parseInt(column || 0, 10);
    474    const columnZeroBased = columnOneBased > 0 ? columnOneBased - 1 : 0;
    475 
    476    // If for any reason the final url is invalid, ignore it
    477    try {
    478      Services.io.newURI(url);
    479    } catch (e) {
    480      return;
    481    }
    482 
    483    const require = this.initDevTools("CommandLine");
    484    const { gDevTools } = require("devtools/client/framework/devtools");
    485    const toolbox = gDevTools.getToolboxForTab(window.gBrowser.selectedTab);
    486    // Ignore the url if there is no devtools currently opened for the current tab
    487    if (!toolbox) {
    488      return;
    489    }
    490 
    491    // Avoid regular Firefox code from processing this argument,
    492    // otherwise we would open the source in DevTools and in a new tab.
    493    //
    494    // /!\ This has to be called synchronously from the call to `DevToolsStartup.handle(cmdLine)`
    495    //     Otherwise the next command lines listener will interpret the argument redundantly.
    496    cmdLine.removeArguments(urlFlagIdx, urlFlagIdx + 1);
    497 
    498    // Avoid opening a new empty top level window if there is no more arguments
    499    if (!cmdLine.length) {
    500      cmdLine.preventDefault = true;
    501    }
    502 
    503    // Immediately focus the browser window in order, to focus devtools, or the view-source tab.
    504    // Otherwise, without this, the terminal would still be the topmost window.
    505    toolbox.win.focus();
    506 
    507    // Note that the following method is async and returns a promise.
    508    // But the current method has to be synchronous because of cmdLine.removeArguments.
    509    // Also note that it will fallback to view-source when the source url isn't found in the debugger
    510    toolbox.viewSourceInDebugger(
    511      url,
    512      parseInt(line, 10),
    513      columnZeroBased,
    514      null,
    515      "CommandLine"
    516    );
    517  }
    518 
    519  readCommandLineFlags(cmdLine) {
    520    // All command line flags are disabled if DevTools are disabled by policy.
    521    if (this.isDisabledByPolicy()) {
    522      return {
    523        console: false,
    524        debugger: false,
    525        devtools: false,
    526        devToolsServer: false,
    527      };
    528    }
    529 
    530    const jsConsole = cmdLine.handleFlag("jsconsole", false);
    531    const devtools = cmdLine.handleFlag("devtools", false);
    532 
    533    let devToolsServer;
    534    try {
    535      devToolsServer = cmdLine.handleFlagWithParam(
    536        "start-debugger-server",
    537        false
    538      );
    539    } catch (e) {
    540      // We get an error if the option is given but not followed by a value.
    541      // By catching and trying again, the value is effectively optional.
    542      devToolsServer = cmdLine.handleFlag("start-debugger-server", false);
    543    }
    544 
    545    let debuggerFlag;
    546    try {
    547      debuggerFlag = cmdLine.handleFlagWithParam("jsdebugger", false);
    548    } catch (e) {
    549      // We get an error if the option is given but not followed by a value.
    550      // By catching and trying again, the value is effectively optional.
    551      debuggerFlag = cmdLine.handleFlag("jsdebugger", false);
    552    }
    553 
    554    return {
    555      console: jsConsole,
    556      debugger: debuggerFlag,
    557      devtools,
    558      devToolsServer,
    559    };
    560  }
    561 
    562  /**
    563   * Called when receiving the "browser-delayed-startup-finished" event for a new
    564   * top-level window.
    565   *
    566   * @param {Window} window
    567   */
    568  onWindowReady(window) {
    569    if (
    570      this.isDisabledByPolicy() ||
    571      AppConstants.MOZ_APP_NAME == "thunderbird"
    572    ) {
    573      return;
    574    }
    575 
    576    this.hookWindow(window);
    577 
    578    // This listener is called for all Firefox windows, but we want to execute some code
    579    // only once.
    580    if (!this._firstWindowReadyReceived) {
    581      this.onFirstWindowReady(window);
    582      this._firstWindowReadyReceived = true;
    583    }
    584 
    585    JsonView.initialize();
    586  }
    587 
    588  /**
    589   * Called when receiving the "browser-delayed-startup-finished" event for a top-level
    590   * window for the first time.
    591   *
    592   * @param {Window} window
    593   */
    594  onFirstWindowReady(window) {
    595    if (this.devtoolsFlag) {
    596      this.handleDevToolsFlag(window);
    597 
    598      // In the case of the --jsconsole and --jsdebugger command line parameters
    599      // there was no browser window when they were processed so we act on the
    600      // this.commandline flag instead.
    601      if (this.commandLine) {
    602        this.sendEntryPointTelemetry("CommandLine");
    603      }
    604    }
    605    this.setSlowScriptDebugHandler();
    606  }
    607 
    608  /**
    609   * Register listeners to all possible entry points for Developer Tools.
    610   * But instead of implementing the actual actions, defer to DevTools codebase.
    611   * In most cases, it only needs to call this.initDevTools which handles the rest.
    612   * We do that to prevent loading any DevTools module until the user intent to use them.
    613   *
    614   * @param {Window} window
    615   */
    616  hookWindow(window) {
    617    // Key Shortcuts need to be added on all the created windows.
    618    this.hookKeyShortcuts(window);
    619 
    620    // In some situations (e.g. starting Firefox with --jsconsole) DevTools will be
    621    // initialized before the first browser-delayed-startup-finished event is received.
    622    // We use a dedicated flag because we still need to hook the developer toggle.
    623    this.hookDeveloperToggle();
    624    this.hookProfilerRecordingButton();
    625 
    626    // The developer menu hook only needs to be added if devtools have not been
    627    // initialized yet.
    628    if (!this.initialized) {
    629      this.hookBrowserToolsMenu(window);
    630    }
    631  }
    632 
    633  /**
    634   * Dynamically register a wrench icon in the customization menu.
    635   * You can use this button by right clicking on Firefox toolbar
    636   * and dragging it from the customization panel to the toolbar.
    637   * (i.e. this isn't displayed by default to users!)
    638   *
    639   * _But_, the "Browser Tools" entry in the hamburger menu (the menu with
    640   * 3 horizontal lines), is using this "developer-button" view to populate
    641   * its menu. So we have to register this button for the menu to work.
    642   *
    643   * Also, this menu duplicates its own entries from the "Browser Tools"
    644   * menu in the system menu, under "Tools" main menu item. The system
    645   * menu is being hooked by "hookBrowserToolsMenu" which ends up calling
    646   * devtools/client/framework/browser-menus to create the items for real,
    647   * initDevTools, from onViewShowing is also calling browser-menu.
    648   */
    649  hookDeveloperToggle() {
    650    if (this.developerToggleCreated) {
    651      return;
    652    }
    653 
    654    const id = "developer-button";
    655    const widget = lazy.CustomizableUI.getWidget(id);
    656    if (widget && widget.provider == lazy.CustomizableUI.PROVIDER_API) {
    657      return;
    658    }
    659 
    660    const panelviewId = "PanelUI-developer-tools";
    661    const subviewId = "PanelUI-developer-tools-view";
    662 
    663    const item = {
    664      id,
    665      type: "view",
    666      viewId: panelviewId,
    667      shortcutId: "key_toggleToolbox",
    668      tooltiptext: "developer-button.tooltiptext2",
    669      onViewShowing: event => {
    670        const doc = event.target.ownerDocument;
    671        const developerItems = lazy.PanelMultiView.getViewNode(doc, subviewId);
    672        this.addDevToolsItemsToSubview(developerItems);
    673      },
    674      onInit(anchor) {
    675        // Since onBeforeCreated already bails out when initialized, we can call
    676        // it right away.
    677        this.onBeforeCreated(anchor.ownerDocument);
    678      },
    679      onBeforeCreated: doc => {
    680        // The developer toggle needs the "key_toggleToolbox" <key> element.
    681        // In DEV EDITION, the toggle is added before 1st paint and hookKeyShortcuts() is
    682        // not called yet when CustomizableUI creates the widget.
    683        this.hookKeyShortcuts(doc.defaultView);
    684      },
    685    };
    686    lazy.CustomizableUI.createWidget(item);
    687    lazy.CustomizableWidgets.push(item);
    688 
    689    this.developerToggleCreated = true;
    690  }
    691 
    692  addDevToolsItemsToSubview(subview) {
    693    // Initialize DevTools to create all menuitems in the system menu before
    694    // trying to copy them.
    695    this.initDevTools("HamburgerMenu");
    696 
    697    // Populate the subview with whatever menuitems are in the developer
    698    // menu. We skip menu elements, because the menu panel has no way
    699    // of dealing with those right now.
    700    const doc = subview.ownerDocument;
    701    const menu = doc.getElementById("menuWebDeveloperPopup");
    702    const itemsToDisplay = [...menu.children];
    703 
    704    lazy.CustomizableUI.clearSubview(subview);
    705    lazy.CustomizableUI.fillSubviewFromMenuItems(itemsToDisplay, subview);
    706  }
    707 
    708  onMoreToolsViewShowing(moreToolsView) {
    709    this.addDevToolsItemsToSubview(moreToolsView);
    710  }
    711 
    712  /**
    713   * Register the profiler recording button. This button will be available
    714   * in the customization palette for the Firefox toolbar. In addition, it can be
    715   * enabled from profiler.firefox.com.
    716   */
    717  hookProfilerRecordingButton() {
    718    if (this.profilerRecordingButtonCreated) {
    719      return;
    720    }
    721    const featureFlagPref = "devtools.performance.popup.feature-flag";
    722    const isPopupFeatureFlagEnabled =
    723      Services.prefs.getBoolPref(featureFlagPref);
    724    this.profilerRecordingButtonCreated = true;
    725 
    726    // Listen for messages from the front-end. This needs to happen even if the
    727    // button isn't enabled yet. This will allow the front-end to turn on the
    728    // popup for our users, regardless of if the feature is enabled by default.
    729    this.initializeProfilerWebChannel();
    730 
    731    if (isPopupFeatureFlagEnabled) {
    732      // Initialize the CustomizableUI widget.
    733      lazy.ProfilerMenuButton.initialize(this.toggleProfilerKeyShortcuts);
    734    } else {
    735      // The feature flag is not enabled, but watch for it to be enabled. If it is,
    736      // initialize everything.
    737      const enable = () => {
    738        lazy.ProfilerMenuButton.initialize(this.toggleProfilerKeyShortcuts);
    739        Services.prefs.removeObserver(featureFlagPref, enable);
    740      };
    741      Services.prefs.addObserver(featureFlagPref, enable);
    742    }
    743 
    744    if (!Cu.isInAutomation && Services.env.exists("MOZ_PROFILER_STARTUP")) {
    745      // If the profiler is active due to startup profiling, show the profiler
    746      // button in the nav bar. But do not do it in automation to avoid
    747      // side-effects with existing tests.
    748      lazy.ProfilerMenuButton.ensureButtonInNavbar();
    749    }
    750  }
    751 
    752  /**
    753   * Initialize the WebChannel for profiler.firefox.com. This function happens at
    754   * startup, so care should be taken to minimize its performance impact. The WebChannel
    755   * is a mechanism that is used to communicate between the browser, and front-end code.
    756   */
    757  initializeProfilerWebChannel() {
    758    let channel;
    759 
    760    // Register a channel for the URL in preferences. Also update the WebChannel if
    761    // the URL changes.
    762    const urlPref = "devtools.performance.recording.ui-base-url";
    763 
    764    // This method is only run once per Firefox instance, so it should not be
    765    // strictly necessary to remove observers here.
    766    // eslint-disable-next-line mozilla/balanced-observers
    767    Services.prefs.addObserver(urlPref, registerWebChannel);
    768 
    769    registerWebChannel();
    770 
    771    function registerWebChannel() {
    772      if (channel) {
    773        channel.stopListening();
    774      }
    775 
    776      const urlForWebChannel = Services.io.newURI(
    777        validateProfilerWebChannelUrl(Services.prefs.getStringPref(urlPref))
    778      );
    779 
    780      channel = new lazy.WebChannel("profiler.firefox.com", urlForWebChannel);
    781 
    782      channel.listen((id, message, target) => {
    783        // Defer loading the ProfilerPopupBackground script until it's absolutely needed,
    784        // as this code path gets loaded at startup.
    785        lazy.ProfilerPopupBackground.handleWebChannelMessage(
    786          channel,
    787          id,
    788          message,
    789          target
    790        );
    791      });
    792    }
    793  }
    794 
    795  /*
    796   * We listen to the "Browser Tools" system menu, which is under "Tools" main item.
    797   * This menu item is hardcoded empty in Firefox UI. We listen for its opening to
    798   * populate it lazily. Loading main DevTools module is going to populate it.
    799   */
    800  hookBrowserToolsMenu(window) {
    801    const menu = window.document.getElementById("browserToolsMenu");
    802    const onPopupShowing = () => {
    803      menu.removeEventListener("popupshowing", onPopupShowing);
    804      this.initDevTools("SystemMenu");
    805    };
    806    menu.addEventListener("popupshowing", onPopupShowing);
    807  }
    808 
    809  /**
    810   * Check if the user is a DevTools user by looking at our selfxss pref.
    811   * This preference is incremented everytime the console is used (up to 5).
    812   *
    813   * @returns {boolean} true if the user can be considered as a devtools user.
    814   */
    815  isDevToolsUser() {
    816    const selfXssCount = Services.prefs.getIntPref("devtools.selfxss.count", 0);
    817    return selfXssCount > 0;
    818  }
    819 
    820  hookKeyShortcuts(window) {
    821    const doc = window.document;
    822 
    823    // hookKeyShortcuts can be called both from hookWindow and from the developer toggle
    824    // onBeforeCreated. Make sure shortcuts are only added once per window.
    825    if (doc.getElementById("devtoolsKeyset")) {
    826      return;
    827    }
    828 
    829    const keyset = doc.createXULElement("keyset");
    830    keyset.setAttribute("id", "devtoolsKeyset");
    831 
    832    this.attachKeys(doc, lazy.KeyShortcuts, keyset);
    833 
    834    // Appending a <key> element is not always enough. The <keyset> needs
    835    // to be detached and reattached to make sure the <key> is taken into
    836    // account (see bug 832984).
    837    const mainKeyset = doc.getElementById("mainKeyset");
    838    mainKeyset.parentNode.insertBefore(keyset, mainKeyset);
    839  }
    840 
    841  /**
    842   * This method attaches on the key elements to the devtools keyset.
    843   *
    844   * @param {Document} doc
    845   * @param {Array<object>} keyShortcuts
    846   * @param {XULElement} [keyset]
    847   */
    848  attachKeys(doc, keyShortcuts, keyset = doc.getElementById("devtoolsKeyset")) {
    849    const window = doc.defaultView;
    850    for (const key of keyShortcuts) {
    851      if (!key.shortcut) {
    852        // Shortcuts might be missing when a user relies on a language packs
    853        // which is missing a recently uplifted shortcut. Language packs are
    854        // typically updated a few days after a code uplift.
    855        continue;
    856      }
    857      const xulKey = this.createKey(doc, key, () => this.onKey(window, key));
    858      keyset.appendChild(xulKey);
    859    }
    860  }
    861 
    862  /**
    863   * This method removes keys from the devtools keyset.
    864   *
    865   * @param {Document} doc
    866   * @param {Array<object>} keyShortcuts
    867   */
    868  removeKeys(doc, keyShortcuts) {
    869    for (const key of keyShortcuts) {
    870      const keyElement = doc.getElementById(this.getKeyElementId(key));
    871      if (keyElement) {
    872        keyElement.remove();
    873      }
    874    }
    875  }
    876 
    877  /**
    878   * We only want to have the keyboard shortcuts active when the menu button is on.
    879   * This function either adds or removes the elements.
    880   *
    881   * @param {boolean} isEnabled
    882   */
    883  toggleProfilerKeyShortcuts(isEnabled) {
    884    const profilerKeyShortcuts = getProfilerKeyShortcuts();
    885    for (const { document } of Services.wm.getEnumerator(null)) {
    886      const devtoolsKeyset = document.getElementById("devtoolsKeyset");
    887      const mainKeyset = document.getElementById("mainKeyset");
    888 
    889      if (!devtoolsKeyset || !mainKeyset) {
    890        // There may not be devtools keyset on this window.
    891        continue;
    892      }
    893 
    894      const areProfilerKeysPresent = !!document.getElementById(
    895        "key_profilerStartStop"
    896      );
    897      if (isEnabled === areProfilerKeysPresent) {
    898        // Don't double add or double remove the shortcuts.
    899        continue;
    900      }
    901      if (isEnabled) {
    902        this.attachKeys(document, profilerKeyShortcuts);
    903      } else {
    904        this.removeKeys(document, profilerKeyShortcuts);
    905      }
    906      // Appending a <key> element is not always enough. The <keyset> needs
    907      // to be detached and reattached to make sure the <key> is taken into
    908      // account (see bug 832984).
    909      mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset);
    910    }
    911  }
    912 
    913  async onKey(window, key) {
    914    try {
    915      // The profiler doesn't care if DevTools is loaded, so provide a quick check
    916      // first to bail out of checking if DevTools is available.
    917      switch (key.id) {
    918        case "profilerStartStop":
    919        case "profilerStartStopAlternate": {
    920          lazy.ProfilerPopupBackground.toggleProfiler("aboutprofiling");
    921          return;
    922        }
    923        case "profilerCapture":
    924        case "profilerCaptureAlternate": {
    925          lazy.ProfilerPopupBackground.captureProfile("aboutprofiling");
    926          return;
    927        }
    928      }
    929 
    930      // Ignore the following key shortcut if DevTools aren't yet opened.
    931      // The key shortcut is registered in this core component in order to
    932      // work even when the web page is focused.
    933      if (key.id == "javascriptTracingToggle" && !this.initialized) {
    934        return;
    935      }
    936 
    937      // Record the timing at which this event started in order to compute later in
    938      // gDevTools.showToolbox, the complete time it takes to open the toolbox.
    939      // i.e. especially take `initDevTools` into account.
    940      const startTime = ChromeUtils.now();
    941      const require = this.initDevTools("KeyShortcut", key);
    942      const {
    943        gDevToolsBrowser,
    944      } = require("devtools/client/framework/devtools-browser");
    945      await gDevToolsBrowser.onKeyShortcut(window, key, startTime);
    946    } catch (e) {
    947      console.error(`Exception while trigerring key ${key}: ${e}\n${e.stack}`);
    948    }
    949  }
    950 
    951  getKeyElementId({ id, toolId }) {
    952    return "key_" + (id || toolId);
    953  }
    954 
    955  // Create a <xul:key> DOM Element
    956  createKey(doc, key, oncommand) {
    957    const { shortcut, modifiers: mod } = key;
    958    const k = doc.createXULElement("key");
    959    k.id = this.getKeyElementId(key);
    960 
    961    if (shortcut.startsWith("VK_")) {
    962      k.setAttribute("keycode", shortcut);
    963      if (shortcut.match(/^VK_\d$/)) {
    964        // Add the event keydown attribute to ensure that shortcuts work for combinations
    965        // such as ctrl shift 1.
    966        k.setAttribute("event", "keydown");
    967      }
    968    } else {
    969      k.setAttribute("key", shortcut);
    970    }
    971 
    972    if (mod) {
    973      k.setAttribute("modifiers", mod);
    974    }
    975 
    976    k.addEventListener("command", oncommand);
    977 
    978    return k;
    979  }
    980 
    981  initDevTools(reason, key = "") {
    982    // In the case of the --jsconsole and --jsdebugger command line parameters
    983    // there is no browser window yet so we don't send any telemetry yet.
    984    if (reason !== "CommandLine") {
    985      this.sendEntryPointTelemetry(reason, key);
    986    }
    987 
    988    this.initialized = true;
    989    const { require } = ChromeUtils.importESModule(
    990      "resource://devtools/shared/loader/Loader.sys.mjs"
    991    );
    992    // Ensure loading main devtools module that hooks up into browser UI
    993    // and initialize all devtools machinery.
    994    // eslint-disable-next-line import/no-unassigned-import
    995    require("devtools/client/framework/devtools-browser");
    996    return require;
    997  }
    998 
    999  handleConsoleFlag(cmdLine) {
   1000    const window = Services.wm.getMostRecentWindow("devtools:webconsole");
   1001    if (!window) {
   1002      const require = this.initDevTools("CommandLine");
   1003      const {
   1004        BrowserConsoleManager,
   1005      } = require("devtools/client/webconsole/browser-console-manager");
   1006      BrowserConsoleManager.toggleBrowserConsole().catch(console.error);
   1007    } else {
   1008      // the Browser Console was already open
   1009      window.focus();
   1010    }
   1011 
   1012    if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
   1013      cmdLine.preventDefault = true;
   1014    }
   1015  }
   1016 
   1017  // Open the toolbox on the selected tab once the browser starts up.
   1018  async handleDevToolsFlag(window) {
   1019    const require = this.initDevTools("CommandLine");
   1020    const { gDevTools } = require("devtools/client/framework/devtools");
   1021    await gDevTools.showToolboxForTab(window.gBrowser.selectedTab);
   1022  }
   1023 
   1024  _isRemoteDebuggingEnabled() {
   1025    let remoteDebuggingEnabled = false;
   1026    try {
   1027      remoteDebuggingEnabled = kDebuggerPrefs.every(pref => {
   1028        return Services.prefs.getBoolPref(pref);
   1029      });
   1030    } catch (ex) {
   1031      console.error(ex);
   1032      return false;
   1033    }
   1034    if (!remoteDebuggingEnabled) {
   1035      const errorMsg =
   1036        "Could not run chrome debugger! You need the following " +
   1037        "prefs to be set to true: " +
   1038        kDebuggerPrefs.join(", ");
   1039      console.error(new Error(errorMsg));
   1040      // Dump as well, as we're doing this from a commandline, make sure people
   1041      // don't miss it:
   1042      dump(errorMsg + "\n");
   1043    }
   1044    return remoteDebuggingEnabled;
   1045  }
   1046 
   1047  handleDebuggerFlag(cmdLine, binaryPath) {
   1048    if (!this._isRemoteDebuggingEnabled()) {
   1049      return;
   1050    }
   1051 
   1052    let devtoolsThreadResumed = false;
   1053    const pauseOnStartup = cmdLine.handleFlag("wait-for-jsdebugger", false);
   1054    if (pauseOnStartup) {
   1055      const observe = function () {
   1056        devtoolsThreadResumed = true;
   1057        Services.obs.removeObserver(observe, "devtools-thread-ready");
   1058      };
   1059      Services.obs.addObserver(observe, "devtools-thread-ready");
   1060    }
   1061 
   1062    const { BrowserToolboxLauncher } = ChromeUtils.importESModule(
   1063      "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs"
   1064    );
   1065    // --jsdebugger $binaryPath is an helper alias to set MOZ_BROWSER_TOOLBOX_BINARY=$binaryPath
   1066    // See comment within BrowserToolboxLauncher.
   1067    // Setting it as an environment variable helps it being reused if we restart the browser via CmdOrCtrl+R
   1068    Services.env.set("MOZ_BROWSER_TOOLBOX_BINARY", binaryPath);
   1069 
   1070    const browserToolboxLauncherConfig = {};
   1071 
   1072    // If user passed the --jsdebugger in mochitests, we want to enable the
   1073    // multiprocess Browser Toolbox (by default it's parent process only)
   1074    if (Services.prefs.getBoolPref("devtools.testing", false)) {
   1075      browserToolboxLauncherConfig.forceMultiprocess = true;
   1076    }
   1077    BrowserToolboxLauncher.init(browserToolboxLauncherConfig);
   1078 
   1079    if (pauseOnStartup) {
   1080      // Spin the event loop until the debugger connects.
   1081      const tm = Cc["@mozilla.org/thread-manager;1"].getService();
   1082      tm.spinEventLoopUntil(
   1083        "DevToolsStartup.sys.mjs:handleDebuggerFlag",
   1084        () => {
   1085          return devtoolsThreadResumed;
   1086        }
   1087      );
   1088    }
   1089 
   1090    if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
   1091      cmdLine.preventDefault = true;
   1092    }
   1093  }
   1094 
   1095  /**
   1096   * Handle the --start-debugger-server command line flag. The options are:
   1097   * --start-debugger-server
   1098   *   The portOrPath parameter is boolean true in this case. Reads and uses the defaults
   1099   *   from devtools.debugger.remote-port and devtools.debugger.remote-websocket prefs.
   1100   *   The default values of these prefs are port 6000, WebSocket disabled.
   1101   *
   1102   * --start-debugger-server 6789
   1103   *   Start the non-WebSocket server on port 6789.
   1104   *
   1105   * --start-debugger-server /path/to/filename
   1106   *   Start the server on a Unix domain socket.
   1107   *
   1108   * --start-debugger-server ws:6789
   1109   *   Start the WebSocket server on port 6789.
   1110   *
   1111   * --start-debugger-server ws:
   1112   *   Start the WebSocket server on the default port (taken from d.d.remote-port)
   1113   *
   1114   * @param {nsICommandLine} cmdLine
   1115   * @param {boolean|string} portOrPath
   1116   */
   1117  handleDevToolsServerFlag(cmdLine, portOrPath) {
   1118    if (!this._isRemoteDebuggingEnabled()) {
   1119      return;
   1120    }
   1121 
   1122    let webSocket = false;
   1123    const defaultPort = Services.prefs.getIntPref(
   1124      "devtools.debugger.remote-port"
   1125    );
   1126    if (portOrPath === true) {
   1127      // Default to pref values if no values given on command line
   1128      webSocket = Services.prefs.getBoolPref(
   1129        "devtools.debugger.remote-websocket"
   1130      );
   1131      portOrPath = defaultPort;
   1132    } else if (portOrPath.startsWith("ws:")) {
   1133      webSocket = true;
   1134      const port = portOrPath.slice(3);
   1135      portOrPath = Number(port) ? port : defaultPort;
   1136    }
   1137 
   1138    const {
   1139      useDistinctSystemPrincipalLoader,
   1140      releaseDistinctSystemPrincipalLoader,
   1141    } = ChromeUtils.importESModule(
   1142      "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs",
   1143      { global: "shared" }
   1144    );
   1145 
   1146    try {
   1147      // Create a separate loader instance, so that we can be sure to receive
   1148      // a separate instance of the DebuggingServer from the rest of the
   1149      // devtools.  This allows us to safely use the tools against even the
   1150      // actors and DebuggingServer itself, especially since we can mark
   1151      // serverLoader as invisible to the debugger (unlike the usual loader
   1152      // settings).
   1153      const serverLoader = useDistinctSystemPrincipalLoader(this);
   1154      const { DevToolsServer: devToolsServer } = serverLoader.require(
   1155        "resource://devtools/server/devtools-server.js"
   1156      );
   1157      const { SocketListener } = serverLoader.require(
   1158        "resource://devtools/shared/security/socket.js"
   1159      );
   1160      devToolsServer.init();
   1161 
   1162      // Force the server to be kept running when the last connection closes.
   1163      // So that another client can connect after the previous one is disconnected.
   1164      devToolsServer.keepAlive = true;
   1165 
   1166      devToolsServer.registerAllActors();
   1167      devToolsServer.allowChromeProcess = true;
   1168      const socketOptions = { portOrPath, webSocket };
   1169 
   1170      const listener = new SocketListener(devToolsServer, socketOptions);
   1171      listener.open();
   1172      dump("Started devtools server on " + portOrPath + "\n");
   1173 
   1174      // Prevent leaks on shutdown.
   1175      const close = () => {
   1176        Services.obs.removeObserver(close, "quit-application");
   1177        dump("Stopped devtools server on " + portOrPath + "\n");
   1178        if (listener) {
   1179          listener.close();
   1180        }
   1181        if (devToolsServer) {
   1182          devToolsServer.destroy();
   1183        }
   1184        releaseDistinctSystemPrincipalLoader(this);
   1185      };
   1186      Services.obs.addObserver(close, "quit-application");
   1187    } catch (e) {
   1188      dump("Unable to start devtools server on " + portOrPath + ": " + e);
   1189    }
   1190 
   1191    if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
   1192      cmdLine.preventDefault = true;
   1193    }
   1194  }
   1195 
   1196  /**
   1197   * Send entry point telemetry explaining how the devtools were launched. This
   1198   * functionality also lives inside `devtools/client/framework/browser-menus.js`
   1199   * because this codepath is only used the first time a toolbox is opened for a
   1200   * tab.
   1201   *
   1202   * @param {string} reason
   1203   *        One of "KeyShortcut", "SystemMenu", "HamburgerMenu", "ContextMenu",
   1204   *        "CommandLine".
   1205   * @param {string} key
   1206   *        The key used by a key shortcut.
   1207   */
   1208  sendEntryPointTelemetry(reason, key = "") {
   1209    if (!reason) {
   1210      return;
   1211    }
   1212 
   1213    let keys = "";
   1214 
   1215    if (reason === "KeyShortcut") {
   1216      let { modifiers, shortcut } = key;
   1217 
   1218      modifiers = modifiers.replace(",", "+");
   1219 
   1220      if (shortcut.startsWith("VK_")) {
   1221        shortcut = shortcut.substr(3);
   1222      }
   1223 
   1224      keys = `${modifiers}+${shortcut}`;
   1225    }
   1226 
   1227    const window = Services.wm.getMostRecentBrowserWindow();
   1228 
   1229    this.telemetry.addEventProperty(
   1230      window,
   1231      "open",
   1232      "tools",
   1233      null,
   1234      "shortcut",
   1235      keys
   1236    );
   1237    this.telemetry.addEventProperty(
   1238      window,
   1239      "open",
   1240      "tools",
   1241      null,
   1242      "entrypoint",
   1243      reason
   1244    );
   1245 
   1246    if (this.recorded) {
   1247      return;
   1248    }
   1249 
   1250    // Only save the first call for each firefox run as next call
   1251    // won't necessarely start the tool. For example key shortcuts may
   1252    // only change the currently selected tool.
   1253    try {
   1254      Glean.devtools.entryPoint[reason].add(1);
   1255    } catch (e) {
   1256      dump("DevTools telemetry entry point failed: " + e + "\n");
   1257    }
   1258    this.recorded = true;
   1259  }
   1260 
   1261  /**
   1262   * Hook the debugger tool to the "Debug Script" button of the slow script dialog.
   1263   */
   1264  setSlowScriptDebugHandler() {
   1265    const debugService = Cc["@mozilla.org/dom/slow-script-debug;1"].getService(
   1266      Ci.nsISlowScriptDebug
   1267    );
   1268 
   1269    debugService.activationHandler = window => {
   1270      const chromeWindow = window.browsingContext.topChromeWindow;
   1271 
   1272      let setupFinished = false;
   1273      this.slowScriptDebugHandler(chromeWindow.gBrowser.selectedTab).then(
   1274        () => {
   1275          setupFinished = true;
   1276        }
   1277      );
   1278 
   1279      // Don't return from the interrupt handler until the debugger is brought
   1280      // up; no reason to continue executing the slow script.
   1281      const utils = window.windowUtils;
   1282      utils.enterModalState();
   1283      Services.tm.spinEventLoopUntil(
   1284        "devtools-browser.js:debugService.activationHandler",
   1285        () => {
   1286          return setupFinished;
   1287        }
   1288      );
   1289      utils.leaveModalState();
   1290    };
   1291 
   1292    debugService.remoteActivationHandler = async (browser, callback) => {
   1293      try {
   1294        // Force selecting the freezing tab
   1295        const chromeWindow = browser.ownerGlobal;
   1296        const tab = chromeWindow.gBrowser.getTabForBrowser(browser);
   1297        chromeWindow.gBrowser.selectedTab = tab;
   1298 
   1299        await this.slowScriptDebugHandler(tab);
   1300      } catch (e) {
   1301        console.error(e);
   1302      }
   1303      callback.finishDebuggerStartup();
   1304    };
   1305  }
   1306 
   1307  /**
   1308   * Called by setSlowScriptDebugHandler, when a tab freeze because of a slow running script
   1309   *
   1310   * @param {XULFrameElement} tab
   1311   */
   1312  async slowScriptDebugHandler(tab) {
   1313    const require = this.initDevTools("SlowScript");
   1314    const { gDevTools } = require("devtools/client/framework/devtools");
   1315    const toolbox = await gDevTools.showToolboxForTab(tab, {
   1316      toolId: "jsdebugger",
   1317    });
   1318    const threadFront = toolbox.threadFront;
   1319 
   1320    // Break in place, which means resuming the debuggee thread and pausing
   1321    // right before the next step happens.
   1322    switch (threadFront.state) {
   1323      case "paused":
   1324        // When the debugger is already paused.
   1325        threadFront.resumeThenPause();
   1326        break;
   1327      case "attached": {
   1328        // When the debugger is already open.
   1329        const onPaused = threadFront.once("paused");
   1330        threadFront.interrupt();
   1331        await onPaused;
   1332        threadFront.resumeThenPause();
   1333        break;
   1334      }
   1335      case "resuming": {
   1336        // The debugger is newly opened.
   1337        const onResumed = threadFront.once("resumed");
   1338        await threadFront.interrupt();
   1339        await onResumed;
   1340        threadFront.resumeThenPause();
   1341        break;
   1342      }
   1343      default:
   1344        throw Error(
   1345          "invalid thread front state in slow script debug handler: " +
   1346            threadFront.state
   1347        );
   1348    }
   1349  }
   1350 
   1351  // Used by tests and the toolbox to register the same key shortcuts in toolboxes loaded
   1352  // in a window window.
   1353  get KeyShortcuts() {
   1354    return lazy.KeyShortcuts;
   1355  }
   1356  get wrappedJSObject() {
   1357    return this;
   1358  }
   1359 
   1360  get jsdebuggerHelpInfo() {
   1361    return `  --jsdebugger [<path>] Open the Browser Toolbox. Defaults to the local build
   1362                     but can be overridden by a firefox path.
   1363  --wait-for-jsdebugger Spin event loop until JS debugger connects.
   1364                     Enables debugging (some) application startup code paths.
   1365                     Only has an effect when \`--jsdebugger\` is also supplied.
   1366  --start-debugger-server [ws:][ <port> | <path> ] Start the devtools server on
   1367                     a TCP port or Unix domain socket path. Defaults to TCP port
   1368                     6000. Use WebSocket protocol if ws: prefix is specified.
   1369 `;
   1370  }
   1371 
   1372  get helpInfo() {
   1373    return `  --jsconsole        Open the Browser Console.
   1374  --devtools         Open DevTools on initial load.
   1375 ${this.jsdebuggerHelpInfo}`;
   1376  }
   1377 
   1378  classID = Components.ID("{9e9a9283-0ce9-4e4a-8f1c-ba129a032c32}");
   1379  QueryInterface = ChromeUtils.generateQI(["nsICommandLineHandler"]);
   1380 }
   1381 
   1382 /**
   1383 * Singleton object that represents the JSON View in-content tool.
   1384 * It has the same lifetime as the browser.
   1385 */
   1386 const JsonView = {
   1387  initialized: false,
   1388 
   1389  initialize() {
   1390    // Prevent loading the frame script multiple times if we call this more than once.
   1391    if (this.initialized) {
   1392      return;
   1393    }
   1394    this.initialized = true;
   1395 
   1396    // Register for messages coming from the child process.
   1397    // This is never removed as there is no particular need to unregister
   1398    // it during shutdown.
   1399    Services.mm.addMessageListener("devtools:jsonview:save", this.onSave);
   1400  },
   1401 
   1402  // Message handlers for events from child processes
   1403 
   1404  /**
   1405   * Save JSON to a file needs to be implemented here
   1406   * in the parent process.
   1407   *
   1408   * @param {object} message
   1409   */
   1410  onSave(message) {
   1411    const browser = message.target;
   1412    const chrome = browser.ownerGlobal;
   1413    if (message.data === null) {
   1414      // Save original contents
   1415      chrome.saveBrowser(browser);
   1416    } else {
   1417      if (
   1418        !message.data.startsWith("blob:resource://devtools/") ||
   1419        browser.contentPrincipal.origin != "resource://devtools"
   1420      ) {
   1421        console.error("Got invalid request to save JSON data");
   1422        return;
   1423      }
   1424      // The following code emulates saveBrowser, but:
   1425      // - Uses the given blob URL containing the custom contents to save.
   1426      // - Obtains the file name from the URL of the document, not the blob.
   1427      // - avoids passing the document and explicitly passes system principal.
   1428      //   We have a blob created by a null principal to save, and the null
   1429      //   principal is from the child. Null principals don't survive crossing
   1430      //   over IPC, so there's no other principal that'll work.
   1431      const persistable = browser.frameLoader;
   1432      persistable.startPersistence(null, {
   1433        onDocumentReady(doc) {
   1434          const uri = chrome.makeURI(doc.documentURI, doc.characterSet);
   1435          const filename = chrome.getDefaultFileName(undefined, uri, doc, null);
   1436          chrome.internalSave(
   1437            message.data,
   1438            null /* originalURL */,
   1439            null,
   1440            filename,
   1441            null,
   1442            doc.contentType,
   1443            false /* bypass cache */,
   1444            null /* filepicker title key */,
   1445            null /* file chosen */,
   1446            null /* referrer */,
   1447            doc.cookieJarSettings,
   1448            null /* initiating document */,
   1449            false /* don't skip prompt for a location */,
   1450            null /* cache key */,
   1451            lazy.PrivateBrowsingUtils.isBrowserPrivate(
   1452              browser
   1453            ) /* private browsing ? */,
   1454            Services.scriptSecurityManager.getSystemPrincipal()
   1455          );
   1456        },
   1457        onError() {
   1458          throw new Error("JSON Viewer's onSave failed in startPersistence");
   1459        },
   1460      });
   1461    }
   1462  },
   1463 };