From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001
From: Daniel Baumann <daniel.baumann@progress-linux.org>
Date: Fri, 19 Apr 2024 02:47:55 +0200
Subject: Adding upstream version 124.0.1.

Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
---
 .../startup/AboutDebuggingRegistration.sys.mjs     |   37 +
 .../AboutDevToolsToolboxRegistration.sys.mjs       |   37 +
 devtools/startup/DevToolsShim.sys.mjs              |  330 +++++
 devtools/startup/DevToolsStartup.sys.mjs           | 1408 ++++++++++++++++++++
 devtools/startup/components.conf                   |   30 +
 devtools/startup/jar.mn                            |    8 +
 devtools/startup/locales/en-US/key-shortcuts.ftl   |   38 +
 devtools/startup/locales/jar.mn                    |    7 +
 devtools/startup/locales/moz.build                 |    7 +
 devtools/startup/moz.build                         |   28 +
 devtools/startup/tests/browser/browser.toml        |   11 +
 .../tests/browser/browser_command_line_urls.js     |  154 +++
 .../tests/browser/browser_shim_disable_devtools.js |  165 +++
 devtools/startup/tests/browser/command-line.html   |   10 +
 devtools/startup/tests/browser/command-line.js     |    3 +
 devtools/startup/tests/xpcshell/.eslintrc.js       |    5 +
 .../startup/tests/xpcshell/test_devtools_shim.js   |  202 +++
 devtools/startup/tests/xpcshell/xpcshell.toml      |    6 +
 18 files changed, 2486 insertions(+)
 create mode 100644 devtools/startup/AboutDebuggingRegistration.sys.mjs
 create mode 100644 devtools/startup/AboutDevToolsToolboxRegistration.sys.mjs
 create mode 100644 devtools/startup/DevToolsShim.sys.mjs
 create mode 100644 devtools/startup/DevToolsStartup.sys.mjs
 create mode 100644 devtools/startup/components.conf
 create mode 100644 devtools/startup/jar.mn
 create mode 100644 devtools/startup/locales/en-US/key-shortcuts.ftl
 create mode 100644 devtools/startup/locales/jar.mn
 create mode 100644 devtools/startup/locales/moz.build
 create mode 100644 devtools/startup/moz.build
 create mode 100644 devtools/startup/tests/browser/browser.toml
 create mode 100644 devtools/startup/tests/browser/browser_command_line_urls.js
 create mode 100644 devtools/startup/tests/browser/browser_shim_disable_devtools.js
 create mode 100644 devtools/startup/tests/browser/command-line.html
 create mode 100644 devtools/startup/tests/browser/command-line.js
 create mode 100644 devtools/startup/tests/xpcshell/.eslintrc.js
 create mode 100644 devtools/startup/tests/xpcshell/test_devtools_shim.js
 create mode 100644 devtools/startup/tests/xpcshell/xpcshell.toml

(limited to 'devtools/startup')

diff --git a/devtools/startup/AboutDebuggingRegistration.sys.mjs b/devtools/startup/AboutDebuggingRegistration.sys.mjs
new file mode 100644
index 0000000000..8ae4f380b9
--- /dev/null
+++ b/devtools/startup/AboutDebuggingRegistration.sys.mjs
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Register the about:debugging URL, that allows to debug tabs, extensions, workers on
+// the current instance of Firefox or on a remote Firefox.
+
+const { nsIAboutModule } = Ci;
+
+export function AboutDebugging() {}
+
+AboutDebugging.prototype = {
+  classDescription: "about:debugging",
+  classID: Components.ID("1060afaf-dc9e-43da-8646-23a2faf48493"),
+  contractID: "@mozilla.org/network/protocol/about;1?what=debugging",
+
+  QueryInterface: ChromeUtils.generateQI([nsIAboutModule]),
+
+  newChannel(_, loadInfo) {
+    const chan = Services.io.newChannelFromURIWithLoadInfo(
+      Services.io.newURI("chrome://devtools/content/aboutdebugging/index.html"),
+      loadInfo
+    );
+    chan.owner = Services.scriptSecurityManager.getSystemPrincipal();
+    return chan;
+  },
+
+  getURIFlags(uri) {
+    return nsIAboutModule.ALLOW_SCRIPT | nsIAboutModule.IS_SECURE_CHROME_UI;
+  },
+
+  getChromeURI(_uri) {
+    return Services.io.newURI(
+      "chrome://devtools/content/aboutdebugging/index.html"
+    );
+  },
+};
diff --git a/devtools/startup/AboutDevToolsToolboxRegistration.sys.mjs b/devtools/startup/AboutDevToolsToolboxRegistration.sys.mjs
new file mode 100644
index 0000000000..f94b866bd2
--- /dev/null
+++ b/devtools/startup/AboutDevToolsToolboxRegistration.sys.mjs
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Register about:devtools-toolbox which allows to open a devtools toolbox
+// in a Firefox tab or a custom html iframe in browser.html
+
+const { nsIAboutModule } = Ci;
+
+export function AboutDevtoolsToolbox() {}
+
+AboutDevtoolsToolbox.prototype = {
+  uri: Services.io.newURI("chrome://devtools/content/framework/toolbox.xhtml"),
+  classDescription: "about:devtools-toolbox",
+  classID: Components.ID("11342911-3135-45a8-8d71-737a2b0ad469"),
+  contractID: "@mozilla.org/network/protocol/about;1?what=devtools-toolbox",
+
+  QueryInterface: ChromeUtils.generateQI([nsIAboutModule]),
+
+  newChannel(uri, loadInfo) {
+    const chan = Services.io.newChannelFromURIWithLoadInfo(this.uri, loadInfo);
+    chan.owner = Services.scriptSecurityManager.getSystemPrincipal();
+    return chan;
+  },
+
+  getURIFlags(uri) {
+    return (
+      nsIAboutModule.ALLOW_SCRIPT |
+      nsIAboutModule.ENABLE_INDEXED_DB |
+      nsIAboutModule.HIDE_FROM_ABOUTABOUT
+    );
+  },
+
+  getChromeURI(_uri) {
+    return this.uri;
+  },
+};
diff --git a/devtools/startup/DevToolsShim.sys.mjs b/devtools/startup/DevToolsShim.sys.mjs
new file mode 100644
index 0000000000..41f874a1ca
--- /dev/null
+++ b/devtools/startup/DevToolsShim.sys.mjs
@@ -0,0 +1,330 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const lazy = {};
+ChromeUtils.defineLazyGetter(lazy, "DevToolsStartup", () => {
+  return Cc["@mozilla.org/devtools/startup-clh;1"].getService(
+    Ci.nsICommandLineHandler
+  ).wrappedJSObject;
+});
+
+// We don't want to spend time initializing the full loader here so we create
+// our own lazy require.
+ChromeUtils.defineLazyGetter(lazy, "Telemetry", function () {
+  const { require } = ChromeUtils.importESModule(
+    "resource://devtools/shared/loader/Loader.sys.mjs"
+  );
+  // eslint-disable-next-line no-shadow
+  const Telemetry = require("devtools/client/shared/telemetry");
+
+  return Telemetry;
+});
+
+const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled";
+
+function removeItem(array, callback) {
+  const index = array.findIndex(callback);
+  if (index >= 0) {
+    array.splice(index, 1);
+  }
+}
+
+/**
+ * DevToolsShim is a singleton that provides a set of helpers to interact with DevTools,
+ * that work whether Devtools are enabled or not.
+ *
+ * It can be used to start listening to devtools events before DevTools are ready. As soon
+ * as DevTools are ready, the DevToolsShim will forward all the requests received until
+ * then to the real DevTools instance.
+ */
+export const DevToolsShim = {
+  _gDevTools: null,
+  listeners: [],
+
+  get telemetry() {
+    if (!this._telemetry) {
+      this._telemetry = new lazy.Telemetry();
+      this._telemetry.setEventRecordingEnabled(true);
+    }
+    return this._telemetry;
+  },
+
+  /**
+   * Returns true if DevTools are enabled. This now only depends on the policy.
+   * TODO: Merge isEnabled and isDisabledByPolicy.
+   */
+  isEnabled() {
+    return !this.isDisabledByPolicy();
+  },
+
+  /**
+   * Returns true if the devtools are completely disabled and can not be enabled. All
+   * entry points should return without throwing, initDevTools should never be called.
+   */
+  isDisabledByPolicy() {
+    return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false);
+  },
+
+  /**
+   * Check if DevTools have already been initialized.
+   *
+   * @return {Boolean} true if DevTools are initialized.
+   */
+  isInitialized() {
+    return !!this._gDevTools;
+  },
+
+  /**
+   * Returns the array of the existing toolboxes. This method is part of the compatibility
+   * layer for webextensions.
+   *
+   * @return {Array<Toolbox>}
+   *   An array of toolboxes.
+   */
+  getToolboxes() {
+    if (this.isInitialized()) {
+      return this._gDevTools.getToolboxes();
+    }
+
+    return [];
+  },
+
+  /**
+   * Register an instance of gDevTools. Should be called by DevTools during startup.
+   *
+   * @param {DevTools} a devtools instance (from client/framework/devtools)
+   */
+  register(gDevTools) {
+    this._gDevTools = gDevTools;
+    this._onDevToolsRegistered();
+    this._gDevTools.emit("devtools-registered");
+  },
+
+  /**
+   * Unregister the current instance of gDevTools. Should be called by DevTools during
+   * shutdown.
+   */
+  unregister() {
+    if (this.isInitialized()) {
+      this._gDevTools.emit("devtools-unregistered");
+      this._gDevTools = null;
+    }
+  },
+
+  /**
+   * The following methods can be called before DevTools are initialized:
+   * - on
+   * - off
+   *
+   * If DevTools are not initialized when calling the method, DevToolsShim will call the
+   * appropriate method as soon as a gDevTools instance is registered.
+   */
+
+  /**
+   * This method is used by browser/components/extensions/ext-devtools.js for the events:
+   * - toolbox-ready
+   * - toolbox-destroyed
+   */
+  on(event, listener) {
+    if (this.isInitialized()) {
+      this._gDevTools.on(event, listener);
+    } else {
+      this.listeners.push([event, listener]);
+    }
+  },
+
+  /**
+   * This method is currently only used by devtools code, but is kept here for consistency
+   * with on().
+   */
+  off(event, listener) {
+    if (this.isInitialized()) {
+      this._gDevTools.off(event, listener);
+    } else {
+      removeItem(this.listeners, ([e, l]) => e === event && l === listener);
+    }
+  },
+
+  /**
+   * Called from SessionStore.sys.mjs in mozilla-central when saving the current state.
+   *
+   * @param {Object} state
+   *                 A SessionStore state object that gets modified by reference
+   */
+  saveDevToolsSession(state) {
+    if (!this.isInitialized()) {
+      return;
+    }
+
+    this._gDevTools.saveDevToolsSession(state);
+  },
+
+  /**
+   * Called from SessionStore.sys.mjs in mozilla-central when restoring a previous session.
+   * Will always be called, even if the session does not contain DevTools related items.
+   */
+  restoreDevToolsSession(session) {
+    if (!this.isEnabled()) {
+      return;
+    }
+
+    const { browserConsole, browserToolbox } = session;
+    const hasDevToolsData = browserConsole || browserToolbox;
+    if (!hasDevToolsData) {
+      // Do not initialize DevTools unless there is DevTools specific data in the session.
+      return;
+    }
+
+    this.initDevTools("SessionRestore");
+    this._gDevTools.restoreDevToolsSession(session);
+  },
+
+  isDevToolsUser() {
+    return lazy.DevToolsStartup.isDevToolsUser();
+  },
+
+  /**
+   * Called from nsContextMenu.js in mozilla-central when using the Inspect Accessibility
+   * context menu item.
+   *
+   * @param {XULTab} tab
+   *        The browser tab on which inspect accessibility was used.
+   * @param {ElementIdentifier} domReference
+   *        Identifier generated by ContentDOMReference. It is a unique pair of
+   *        BrowsingContext ID and a numeric ID.
+   * @return {Promise} a promise that resolves when the accessible node is selected in the
+   *         accessibility inspector or that resolves immediately if DevTools are not
+   *         enabled.
+   */
+  inspectA11Y(tab, domReference) {
+    if (!this.isEnabled()) {
+      return Promise.resolve();
+    }
+
+    // Record the timing at which this event started in order to compute later in
+    // gDevTools.showToolbox, the complete time it takes to open the toolbox.
+    // i.e. especially take `DevToolsStartup.initDevTools` into account.
+    const startTime = Cu.now();
+
+    this.initDevTools("ContextMenu");
+
+    return this._gDevTools.inspectA11Y(tab, domReference, startTime);
+  },
+
+  /**
+   * Called from nsContextMenu.js in mozilla-central when using the Inspect Element
+   * context menu item.
+   *
+   * @param {XULTab} tab
+   *        The browser tab on which inspect node was used.
+   * @param {ElementIdentifier} domReference
+   *        Identifier generated by ContentDOMReference. It is a unique pair of
+   *        BrowsingContext ID and a numeric ID.
+   * @return {Promise} a promise that resolves when the node is selected in the inspector
+   *         markup view or that resolves immediately if DevTools are not enabled.
+   */
+  inspectNode(tab, domReference) {
+    if (!this.isEnabled()) {
+      return Promise.resolve();
+    }
+
+    // Record the timing at which this event started in order to compute later in
+    // gDevTools.showToolbox, the complete time it takes to open the toolbox.
+    // i.e. especially take `DevToolsStartup.initDevTools` into account.
+    const startTime = Cu.now();
+
+    this.initDevTools("ContextMenu");
+
+    return this._gDevTools.inspectNode(tab, domReference, startTime);
+  },
+
+  _onDevToolsRegistered() {
+    // Register all pending event listeners on the real gDevTools object.
+    for (const [event, listener] of this.listeners) {
+      this._gDevTools.on(event, listener);
+    }
+
+    this.listeners = [];
+  },
+
+  /**
+   * Initialize DevTools via DevToolsStartup if needed. This method throws if DevTools are
+   * not enabled.
+   *
+   * @param {String} reason
+   *        optional, if provided should be a valid entry point for DEVTOOLS_ENTRY_POINT
+   *        in toolkit/components/telemetry/Histograms.json
+   */
+  initDevTools(reason) {
+    if (!this.isEnabled()) {
+      throw new Error("DevTools are not enabled and can not be initialized.");
+    }
+
+    if (reason) {
+      const window = Services.wm.getMostRecentWindow("navigator:browser");
+
+      this.telemetry.addEventProperty(
+        window,
+        "open",
+        "tools",
+        null,
+        "shortcut",
+        ""
+      );
+      this.telemetry.addEventProperty(
+        window,
+        "open",
+        "tools",
+        null,
+        "entrypoint",
+        reason
+      );
+    }
+
+    if (!this.isInitialized()) {
+      lazy.DevToolsStartup.initDevTools(reason);
+    }
+  },
+};
+
+/**
+ * Compatibility layer for webextensions.
+ *
+ * Those methods are called only after a DevTools webextension was loaded in DevTools,
+ * therefore DevTools should always be available when they are called.
+ */
+const webExtensionsMethods = [
+  "createCommandsForTabForWebExtension",
+  "getTheme",
+  "openBrowserConsole",
+];
+
+/**
+ * Compatibility layer for other third parties.
+ */
+const otherToolMethods = [
+  // gDevTools.showToolboxForTab is used by wptrunner to start devtools
+  // https://github.com/web-platform-tests/wpt
+  // And also, Quick Actions on URL bar.
+  "showToolboxForTab",
+  // Used for Quick Actions on URL bar.
+  "hasToolboxForTab",
+  // Used for Quick Actions test.
+  "getToolboxForTab",
+];
+
+for (const method of [...webExtensionsMethods, ...otherToolMethods]) {
+  DevToolsShim[method] = function () {
+    if (!this.isEnabled()) {
+      throw new Error(
+        "Could not call a DevToolsShim webextension method ('" +
+          method +
+          "'): DevTools are not initialized."
+      );
+    }
+
+    this.initDevTools();
+    return this._gDevTools[method].apply(this._gDevTools, arguments);
+  };
+}
diff --git a/devtools/startup/DevToolsStartup.sys.mjs b/devtools/startup/DevToolsStartup.sys.mjs
new file mode 100644
index 0000000000..dd6be71337
--- /dev/null
+++ b/devtools/startup/DevToolsStartup.sys.mjs
@@ -0,0 +1,1408 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This XPCOM component is loaded very early.
+ * Be careful to lazy load dependencies as much as possible.
+ *
+ * It manages all the possible entry points for DevTools:
+ * - Handles command line arguments like -jsconsole,
+ * - Register all key shortcuts,
+ * - Listen for "Browser Tools" system menu opening, under "Tools",
+ * - Inject the wrench icon in toolbar customization, which is used
+ *   by the "Browser Tools" list displayed in the hamburger menu,
+ * - Register the JSON Viewer protocol handler.
+ * - Inject the profiler recording button in toolbar customization.
+ *
+ * Only once any of these entry point is fired, this module ensures starting
+ * core modules like 'devtools-browser.js' that hooks the browser windows
+ * and ensure setting up tools.
+ **/
+
+const kDebuggerPrefs = [
+  "devtools.debugger.remote-enabled",
+  "devtools.chrome.enabled",
+];
+
+const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled";
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+  CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
+  CustomizableWidgets: "resource:///modules/CustomizableWidgets.sys.mjs",
+  PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
+  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
+  ProfilerMenuButton:
+    "resource://devtools/client/performance-new/popup/menu-button.sys.mjs",
+  WebChannel: "resource://gre/modules/WebChannel.sys.mjs",
+});
+
+// We don't want to spend time initializing the full loader here so we create
+// our own lazy require.
+ChromeUtils.defineLazyGetter(lazy, "Telemetry", function () {
+  const { require } = ChromeUtils.importESModule(
+    "resource://devtools/shared/loader/Loader.sys.mjs"
+  );
+  // eslint-disable-next-line no-shadow
+  const Telemetry = require("devtools/client/shared/telemetry");
+
+  return Telemetry;
+});
+
+ChromeUtils.defineLazyGetter(lazy, "KeyShortcutsBundle", function () {
+  return new Localization(["devtools/startup/key-shortcuts.ftl"], true);
+});
+
+/**
+ * Safely retrieve a localized DevTools key shortcut from KeyShortcutsBundle.
+ * If the shortcut is not available, this will return null. Consumer code
+ * should rely on this to skip unavailable shortcuts.
+ *
+ * Note that all shortcuts should always be available, but there is a notable
+ * exception, which is why we have to do this. When a localization change is
+ * uplifted to beta, language packs will not be updated immediately when the
+ * updated beta is available.
+ *
+ * This means that language pack users might get a new Beta version but will not
+ * have a language pack with the new strings yet.
+ */
+function getLocalizedKeyShortcut(id) {
+  try {
+    return lazy.KeyShortcutsBundle.formatValueSync(id);
+  } catch (e) {
+    console.error("Failed to retrieve DevTools localized shortcut for id", id);
+    return null;
+  }
+}
+
+ChromeUtils.defineLazyGetter(lazy, "KeyShortcuts", function () {
+  const isMac = AppConstants.platform == "macosx";
+
+  // Common modifier shared by most key shortcuts
+  const modifiers = isMac ? "accel,alt" : "accel,shift";
+
+  // List of all key shortcuts triggering installation UI
+  // `id` should match tool's id from client/definitions.js
+  const shortcuts = [
+    // The following keys are also registered in /client/menus.js
+    // And should be synced.
+
+    // Both are toggling the toolbox on the last selected panel
+    // or the default one.
+    {
+      id: "toggleToolbox",
+      shortcut: getLocalizedKeyShortcut("devtools-commandkey-toggle-toolbox"),
+      modifiers,
+    },
+    // All locales are using F12
+    {
+      id: "toggleToolboxF12",
+      shortcut: getLocalizedKeyShortcut(
+        "devtools-commandkey-toggle-toolbox-f12"
+      ),
+      modifiers: "", // F12 is the only one without modifiers
+    },
+    // Open the Browser Toolbox
+    {
+      id: "browserToolbox",
+      shortcut: getLocalizedKeyShortcut("devtools-commandkey-browser-toolbox"),
+      modifiers: "accel,alt,shift",
+    },
+    // Open the Browser Console
+    {
+      id: "browserConsole",
+      shortcut: getLocalizedKeyShortcut("devtools-commandkey-browser-console"),
+      modifiers: "accel,shift",
+    },
+    // Toggle the Responsive Design Mode
+    {
+      id: "responsiveDesignMode",
+      shortcut: getLocalizedKeyShortcut(
+        "devtools-commandkey-responsive-design-mode"
+      ),
+      modifiers,
+    },
+    // The following keys are also registered in /client/definitions.js
+    // and should be synced.
+
+    // Key for opening the Inspector
+    {
+      toolId: "inspector",
+      shortcut: getLocalizedKeyShortcut("devtools-commandkey-inspector"),
+      modifiers,
+    },
+    // Key for opening the Web Console
+    {
+      toolId: "webconsole",
+      shortcut: getLocalizedKeyShortcut("devtools-commandkey-webconsole"),
+      modifiers,
+    },
+    // Key for opening the Debugger
+    {
+      toolId: "jsdebugger",
+      shortcut: getLocalizedKeyShortcut("devtools-commandkey-jsdebugger"),
+      modifiers,
+    },
+    // Key for opening the Network Monitor
+    {
+      toolId: "netmonitor",
+      shortcut: getLocalizedKeyShortcut("devtools-commandkey-netmonitor"),
+      modifiers,
+    },
+    // Key for opening the Style Editor
+    {
+      toolId: "styleeditor",
+      shortcut: getLocalizedKeyShortcut("devtools-commandkey-styleeditor"),
+      modifiers: "shift",
+    },
+    // Key for opening the Performance Panel
+    {
+      toolId: "performance",
+      shortcut: getLocalizedKeyShortcut("devtools-commandkey-performance"),
+      modifiers: "shift",
+    },
+    // Key for opening the Storage Panel
+    {
+      toolId: "storage",
+      shortcut: getLocalizedKeyShortcut("devtools-commandkey-storage"),
+      modifiers: "shift",
+    },
+    // Key for opening the DOM Panel
+    {
+      toolId: "dom",
+      shortcut: getLocalizedKeyShortcut("devtools-commandkey-dom"),
+      modifiers,
+    },
+    // Key for opening the Accessibility Panel
+    {
+      toolId: "accessibility",
+      shortcut: getLocalizedKeyShortcut(
+        "devtools-commandkey-accessibility-f12"
+      ),
+      modifiers: "shift",
+    },
+  ];
+
+  if (isMac) {
+    // Add the extra key command for macOS, so you can open the inspector with cmd+shift+C
+    // like on Chrome DevTools.
+    shortcuts.push({
+      id: "inspectorMac",
+      toolId: "inspector",
+      shortcut: getLocalizedKeyShortcut("devtools-commandkey-inspector"),
+      modifiers: "accel,shift",
+    });
+  }
+
+  if (lazy.ProfilerMenuButton.isInNavbar()) {
+    shortcuts.push(...getProfilerKeyShortcuts());
+  }
+
+  // Allow toggling the JavaScript tracing not only from DevTools UI,
+  // but also from the web page when it is focused.
+  if (
+    Services.prefs.getBoolPref(
+      "devtools.debugger.features.javascript-tracing",
+      false
+    )
+  ) {
+    shortcuts.push({
+      id: "javascriptTracingToggle",
+      shortcut: getLocalizedKeyShortcut(
+        "devtools-commandkey-javascript-tracing-toggle"
+      ),
+      modifiers: "control,shift",
+    });
+  }
+
+  return shortcuts;
+});
+
+function getProfilerKeyShortcuts() {
+  return [
+    // Start/stop the profiler
+    {
+      id: "profilerStartStop",
+      shortcut: getLocalizedKeyShortcut(
+        "devtools-commandkey-profiler-start-stop"
+      ),
+      modifiers: "control,shift",
+    },
+    // Capture a profile
+    {
+      id: "profilerCapture",
+      shortcut: getLocalizedKeyShortcut("devtools-commandkey-profiler-capture"),
+      modifiers: "control,shift",
+    },
+    // Because it's not uncommon for content or extension to bind this
+    // shortcut, allow using alt as well for starting and stopping the profiler
+    {
+      id: "profilerStartStopAlternate",
+      shortcut: getLocalizedKeyShortcut(
+        "devtools-commandkey-profiler-start-stop"
+      ),
+      modifiers: "control,shift,alt",
+    },
+    {
+      id: "profilerCaptureAlternate",
+      shortcut: getLocalizedKeyShortcut("devtools-commandkey-profiler-capture"),
+      modifiers: "control,shift,alt",
+    },
+  ];
+}
+
+/**
+ * Validate the URL that will be used for the WebChannel for the profiler.
+ *
+ * @param {string} targetUrl
+ * @returns {string}
+ */
+export function validateProfilerWebChannelUrl(targetUrl) {
+  const frontEndUrl = "https://profiler.firefox.com";
+
+  if (targetUrl !== frontEndUrl) {
+    // The user can specify either localhost or deploy previews as well as
+    // the official frontend URL for testing.
+    if (
+      // Allow a test URL.
+      /^https?:\/\/example\.com$/.test(targetUrl) ||
+      // Allows the following:
+      //   "http://localhost:4242"
+      //   "http://localhost:4242/"
+      //   "http://localhost:3"
+      //   "http://localhost:334798455"
+      /^http:\/\/localhost:\d+\/?$/.test(targetUrl) ||
+      // Allows the following:
+      //   "https://deploy-preview-1234--perf-html.netlify.com"
+      //   "https://deploy-preview-1234--perf-html.netlify.com/"
+      //   "https://deploy-preview-1234567--perf-html.netlify.app"
+      //   "https://main--perf-html.netlify.app"
+      /^https:\/\/(?:deploy-preview-\d+|main)--perf-html\.netlify\.(?:com|app)\/?$/.test(
+        targetUrl
+      )
+    ) {
+      // This URL is one of the allowed ones to be used for configuration.
+      return targetUrl;
+    }
+
+    console.error(
+      `The preference "devtools.performance.recording.ui-base-url" was set to a ` +
+        "URL that is not allowed. No WebChannel messages will be sent between the " +
+        `browser and that URL. Falling back to ${frontEndUrl}. Only localhost ` +
+        "and deploy previews URLs are allowed.",
+      targetUrl
+    );
+  }
+
+  return frontEndUrl;
+}
+
+ChromeUtils.defineLazyGetter(lazy, "ProfilerPopupBackground", function () {
+  return ChromeUtils.importESModule(
+    "resource://devtools/client/performance-new/shared/background.sys.mjs"
+  );
+});
+
+export function DevToolsStartup() {
+  this.onWindowReady = this.onWindowReady.bind(this);
+  this.addDevToolsItemsToSubview = this.addDevToolsItemsToSubview.bind(this);
+  this.onMoreToolsViewShowing = this.onMoreToolsViewShowing.bind(this);
+  this.toggleProfilerKeyShortcuts = this.toggleProfilerKeyShortcuts.bind(this);
+}
+
+DevToolsStartup.prototype = {
+  /**
+   * Boolean flag to check if DevTools have been already initialized or not.
+   * By initialized, we mean that its main modules are loaded.
+   */
+  initialized: false,
+
+  /**
+   * Boolean flag to check if the devtools initialization was already sent to telemetry.
+   * We only want to record one devtools entry point per Firefox run, but we are not
+   * interested in all the entry points.
+   */
+  recorded: false,
+
+  get telemetry() {
+    if (!this._telemetry) {
+      this._telemetry = new lazy.Telemetry();
+      this._telemetry.setEventRecordingEnabled(true);
+    }
+    return this._telemetry;
+  },
+
+  /**
+   * Flag that indicates if the developer toggle was already added to customizableUI.
+   */
+  developerToggleCreated: false,
+
+  /**
+   * Flag that indicates if the profiler recording popup was already added to
+   * customizableUI.
+   */
+  profilerRecordingButtonCreated: false,
+
+  isDisabledByPolicy() {
+    return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false);
+  },
+
+  handle(cmdLine) {
+    const flags = this.readCommandLineFlags(cmdLine);
+
+    // handle() can be called after browser startup (e.g. opening links from other apps).
+    const isInitialLaunch =
+      cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH;
+    if (isInitialLaunch) {
+      // Store devtoolsFlag to check it later in onWindowReady.
+      this.devtoolsFlag = flags.devtools;
+
+      /* eslint-disable mozilla/balanced-observers */
+      // We are not expecting to remove those listeners until Firefox closes.
+
+      // Only top level Firefox Windows fire a browser-delayed-startup-finished event
+      Services.obs.addObserver(
+        this.onWindowReady,
+        "browser-delayed-startup-finished"
+      );
+
+      // Add DevTools menu items to the "More Tools" view.
+      Services.obs.addObserver(
+        this.onMoreToolsViewShowing,
+        "web-developer-tools-view-showing"
+      );
+      /* eslint-enable mozilla/balanced-observers */
+
+      if (!this.isDisabledByPolicy()) {
+        if (AppConstants.MOZ_DEV_EDITION) {
+          // On DevEdition, the developer toggle is displayed by default in the navbar
+          // area and should be created before the first paint.
+          this.hookDeveloperToggle();
+        }
+
+        this.hookProfilerRecordingButton();
+      }
+    }
+
+    if (flags.console) {
+      this.commandLine = true;
+      this.handleConsoleFlag(cmdLine);
+    }
+    if (flags.debugger) {
+      this.commandLine = true;
+      const binaryPath =
+        typeof flags.debugger == "string" ? flags.debugger : null;
+      this.handleDebuggerFlag(cmdLine, binaryPath);
+    }
+
+    if (flags.devToolsServer) {
+      this.handleDevToolsServerFlag(cmdLine, flags.devToolsServer);
+    }
+
+    // If Firefox is already opened, and DevTools are also already opened,
+    // try to open links passed via command line arguments.
+    if (!isInitialLaunch && this.initialized && cmdLine.length) {
+      this.checkForDebuggerLink(cmdLine);
+    }
+  },
+
+  /**
+   * Lookup in all arguments passed to firefox binary to find
+   * URLs including a precise location, like this:
+   *   https://domain.com/file.js:1:10 (URL ending with `:${line}:${number}`)
+   * When such argument exists, try to open this source and precise location
+   * in the debugger.
+   *
+   * @param {nsICommandLine} cmdLine
+   */
+  checkForDebuggerLink(cmdLine) {
+    const urlFlagIdx = cmdLine.findFlag("url", false);
+    // Bail out when there is no -url argument, or if that's last and so there is no URL after it.
+    if (urlFlagIdx == -1 && urlFlagIdx + 1 < cmdLine.length) {
+      return;
+    }
+
+    // The following code would only work if we have a top level browser window opened
+    const window = Services.wm.getMostRecentWindow("navigator:browser");
+    if (!window) {
+      return;
+    }
+
+    const urlParam = cmdLine.getArgument(urlFlagIdx + 1);
+
+    // Avoid processing valid url like:
+    //   http://foo@user:123
+    // Note that when loading `http://foo.com` the URL of the default html page will be `http://foo.com/`.
+    // So that there will always be another `/` after `https://`
+    if (
+      (urlParam.startsWith("http://") || urlParam.startsWith("https://")) &&
+      urlParam.lastIndexOf("/") <= 7
+    ) {
+      return;
+    }
+
+    let match = urlParam.match(/^(?<url>\w+:.+):(?<line>\d+):(?<column>\d+)$/);
+    if (!match) {
+      // fallback on only having the line when there is no column
+      match = urlParam.match(/^(?<url>\w+:.+):(?<line>\d+)?$/);
+      if (!match) {
+        return;
+      }
+    }
+
+    // line and column are supposed to be 1-based.
+    const { url, line, column } = match.groups;
+
+    // Debugger internal uses 0-based column number.
+    // NOTE: Non-debugger view-source doesn't use column number.
+    const columnOneBased = parseInt(column || 0, 10);
+    const columnZeroBased = columnOneBased > 0 ? columnOneBased - 1 : 0;
+
+    // If for any reason the final url is invalid, ignore it
+    try {
+      Services.io.newURI(url);
+    } catch (e) {
+      return;
+    }
+
+    const require = this.initDevTools("CommandLine");
+    const { gDevTools } = require("devtools/client/framework/devtools");
+    const toolbox = gDevTools.getToolboxForTab(window.gBrowser.selectedTab);
+    // Ignore the url if there is no devtools currently opened for the current tab
+    if (!toolbox) {
+      return;
+    }
+
+    // Avoid regular Firefox code from processing this argument,
+    // otherwise we would open the source in DevTools and in a new tab.
+    //
+    // /!\ This has to be called synchronously from the call to `DevToolsStartup.handle(cmdLine)`
+    //     Otherwise the next command lines listener will interpret the argument redundantly.
+    cmdLine.removeArguments(urlFlagIdx, urlFlagIdx + 1);
+
+    // Avoid opening a new empty top level window if there is no more arguments
+    if (!cmdLine.length) {
+      cmdLine.preventDefault = true;
+    }
+
+    // Immediately focus the browser window in order, to focus devtools, or the view-source tab.
+    // Otherwise, without this, the terminal would still be the topmost window.
+    toolbox.win.focus();
+
+    // Note that the following method is async and returns a promise.
+    // But the current method has to be synchronous because of cmdLine.removeArguments.
+    // Also note that it will fallback to view-source when the source url isn't found in the debugger
+    toolbox.viewSourceInDebugger(
+      url,
+      parseInt(line, 10),
+      columnZeroBased,
+      null,
+      "CommandLine"
+    );
+  },
+
+  readCommandLineFlags(cmdLine) {
+    // All command line flags are disabled if DevTools are disabled by policy.
+    if (this.isDisabledByPolicy()) {
+      return {
+        console: false,
+        debugger: false,
+        devtools: false,
+        devToolsServer: false,
+      };
+    }
+
+    const console = cmdLine.handleFlag("jsconsole", false);
+    const devtools = cmdLine.handleFlag("devtools", false);
+
+    let devToolsServer;
+    try {
+      devToolsServer = cmdLine.handleFlagWithParam(
+        "start-debugger-server",
+        false
+      );
+    } catch (e) {
+      // We get an error if the option is given but not followed by a value.
+      // By catching and trying again, the value is effectively optional.
+      devToolsServer = cmdLine.handleFlag("start-debugger-server", false);
+    }
+
+    let debuggerFlag;
+    try {
+      debuggerFlag = cmdLine.handleFlagWithParam("jsdebugger", false);
+    } catch (e) {
+      // We get an error if the option is given but not followed by a value.
+      // By catching and trying again, the value is effectively optional.
+      debuggerFlag = cmdLine.handleFlag("jsdebugger", false);
+    }
+
+    return { console, debugger: debuggerFlag, devtools, devToolsServer };
+  },
+
+  /**
+   * Called when receiving the "browser-delayed-startup-finished" event for a new
+   * top-level window.
+   */
+  onWindowReady(window) {
+    if (
+      this.isDisabledByPolicy() ||
+      AppConstants.MOZ_APP_NAME == "thunderbird"
+    ) {
+      return;
+    }
+
+    this.hookWindow(window);
+
+    // This listener is called for all Firefox windows, but we want to execute some code
+    // only once.
+    if (!this._firstWindowReadyReceived) {
+      this.onFirstWindowReady(window);
+      this._firstWindowReadyReceived = true;
+    }
+
+    JsonView.initialize();
+  },
+
+  onFirstWindowReady(window) {
+    if (this.devtoolsFlag) {
+      this.handleDevToolsFlag(window);
+
+      // In the case of the --jsconsole and --jsdebugger command line parameters
+      // there was no browser window when they were processed so we act on the
+      // this.commandline flag instead.
+      if (this.commandLine) {
+        this.sendEntryPointTelemetry("CommandLine");
+      }
+    }
+    this.setSlowScriptDebugHandler();
+  },
+
+  /**
+   * Register listeners to all possible entry points for Developer Tools.
+   * But instead of implementing the actual actions, defer to DevTools codebase.
+   * In most cases, it only needs to call this.initDevTools which handles the rest.
+   * We do that to prevent loading any DevTools module until the user intent to use them.
+   */
+  hookWindow(window) {
+    // Key Shortcuts need to be added on all the created windows.
+    this.hookKeyShortcuts(window);
+
+    // In some situations (e.g. starting Firefox with --jsconsole) DevTools will be
+    // initialized before the first browser-delayed-startup-finished event is received.
+    // We use a dedicated flag because we still need to hook the developer toggle.
+    this.hookDeveloperToggle();
+    this.hookProfilerRecordingButton();
+
+    // The developer menu hook only needs to be added if devtools have not been
+    // initialized yet.
+    if (!this.initialized) {
+      this.hookBrowserToolsMenu(window);
+    }
+  },
+
+  /**
+   * Dynamically register a wrench icon in the customization menu.
+   * You can use this button by right clicking on Firefox toolbar
+   * and dragging it from the customization panel to the toolbar.
+   * (i.e. this isn't displayed by default to users!)
+   *
+   * _But_, the "Browser Tools" entry in the hamburger menu (the menu with
+   * 3 horizontal lines), is using this "developer-button" view to populate
+   * its menu. So we have to register this button for the menu to work.
+   *
+   * Also, this menu duplicates its own entries from the "Browser Tools"
+   * menu in the system menu, under "Tools" main menu item. The system
+   * menu is being hooked by "hookBrowserToolsMenu" which ends up calling
+   * devtools/client/framework/browser-menus to create the items for real,
+   * initDevTools, from onViewShowing is also calling browser-menu.
+   */
+  hookDeveloperToggle() {
+    if (this.developerToggleCreated) {
+      return;
+    }
+
+    const id = "developer-button";
+    const widget = lazy.CustomizableUI.getWidget(id);
+    if (widget && widget.provider == lazy.CustomizableUI.PROVIDER_API) {
+      return;
+    }
+
+    const panelviewId = "PanelUI-developer-tools";
+    const subviewId = "PanelUI-developer-tools-view";
+
+    const item = {
+      id,
+      type: "view",
+      viewId: panelviewId,
+      shortcutId: "key_toggleToolbox",
+      tooltiptext: "developer-button.tooltiptext2",
+      onViewShowing: event => {
+        const doc = event.target.ownerDocument;
+        const developerItems = lazy.PanelMultiView.getViewNode(doc, subviewId);
+        this.addDevToolsItemsToSubview(developerItems);
+      },
+      onInit(anchor) {
+        // Since onBeforeCreated already bails out when initialized, we can call
+        // it right away.
+        this.onBeforeCreated(anchor.ownerDocument);
+      },
+      onBeforeCreated: doc => {
+        // The developer toggle needs the "key_toggleToolbox" <key> element.
+        // In DEV EDITION, the toggle is added before 1st paint and hookKeyShortcuts() is
+        // not called yet when CustomizableUI creates the widget.
+        this.hookKeyShortcuts(doc.defaultView);
+      },
+    };
+    lazy.CustomizableUI.createWidget(item);
+    lazy.CustomizableWidgets.push(item);
+
+    this.developerToggleCreated = true;
+  },
+
+  addDevToolsItemsToSubview(subview) {
+    // Initialize DevTools to create all menuitems in the system menu before
+    // trying to copy them.
+    this.initDevTools("HamburgerMenu");
+
+    // Populate the subview with whatever menuitems are in the developer
+    // menu. We skip menu elements, because the menu panel has no way
+    // of dealing with those right now.
+    const doc = subview.ownerDocument;
+    const menu = doc.getElementById("menuWebDeveloperPopup");
+    const itemsToDisplay = [...menu.children];
+
+    lazy.CustomizableUI.clearSubview(subview);
+    lazy.CustomizableUI.fillSubviewFromMenuItems(itemsToDisplay, subview);
+  },
+
+  onMoreToolsViewShowing(moreToolsView) {
+    this.addDevToolsItemsToSubview(moreToolsView);
+  },
+
+  /**
+   * Register the profiler recording button. This button will be available
+   * in the customization palette for the Firefox toolbar. In addition, it can be
+   * enabled from profiler.firefox.com.
+   */
+  hookProfilerRecordingButton() {
+    if (this.profilerRecordingButtonCreated) {
+      return;
+    }
+    const featureFlagPref = "devtools.performance.popup.feature-flag";
+    const isPopupFeatureFlagEnabled =
+      Services.prefs.getBoolPref(featureFlagPref);
+    this.profilerRecordingButtonCreated = true;
+
+    // Listen for messages from the front-end. This needs to happen even if the
+    // button isn't enabled yet. This will allow the front-end to turn on the
+    // popup for our users, regardless of if the feature is enabled by default.
+    this.initializeProfilerWebChannel();
+
+    if (isPopupFeatureFlagEnabled) {
+      // Initialize the CustomizableUI widget.
+      lazy.ProfilerMenuButton.initialize(this.toggleProfilerKeyShortcuts);
+    } else {
+      // The feature flag is not enabled, but watch for it to be enabled. If it is,
+      // initialize everything.
+      const enable = () => {
+        lazy.ProfilerMenuButton.initialize(this.toggleProfilerKeyShortcuts);
+        Services.prefs.removeObserver(featureFlagPref, enable);
+      };
+      Services.prefs.addObserver(featureFlagPref, enable);
+    }
+  },
+
+  /**
+   * Initialize the WebChannel for profiler.firefox.com. This function happens at
+   * startup, so care should be taken to minimize its performance impact. The WebChannel
+   * is a mechanism that is used to communicate between the browser, and front-end code.
+   */
+  initializeProfilerWebChannel() {
+    let channel;
+
+    // Register a channel for the URL in preferences. Also update the WebChannel if
+    // the URL changes.
+    const urlPref = "devtools.performance.recording.ui-base-url";
+
+    // This method is only run once per Firefox instance, so it should not be
+    // strictly necessary to remove observers here.
+    // eslint-disable-next-line mozilla/balanced-observers
+    Services.prefs.addObserver(urlPref, registerWebChannel);
+
+    registerWebChannel();
+
+    function registerWebChannel() {
+      if (channel) {
+        channel.stopListening();
+      }
+
+      const urlForWebChannel = Services.io.newURI(
+        validateProfilerWebChannelUrl(Services.prefs.getStringPref(urlPref))
+      );
+
+      channel = new lazy.WebChannel("profiler.firefox.com", urlForWebChannel);
+
+      channel.listen((id, message, target) => {
+        // Defer loading the ProfilerPopupBackground script until it's absolutely needed,
+        // as this code path gets loaded at startup.
+        lazy.ProfilerPopupBackground.handleWebChannelMessage(
+          channel,
+          id,
+          message,
+          target
+        );
+      });
+    }
+  },
+
+  /*
+   * We listen to the "Browser Tools" system menu, which is under "Tools" main item.
+   * This menu item is hardcoded empty in Firefox UI. We listen for its opening to
+   * populate it lazily. Loading main DevTools module is going to populate it.
+   */
+  hookBrowserToolsMenu(window) {
+    const menu = window.document.getElementById("browserToolsMenu");
+    const onPopupShowing = () => {
+      menu.removeEventListener("popupshowing", onPopupShowing);
+      this.initDevTools("SystemMenu");
+    };
+    menu.addEventListener("popupshowing", onPopupShowing);
+  },
+
+  /**
+   * Check if the user is a DevTools user by looking at our selfxss pref.
+   * This preference is incremented everytime the console is used (up to 5).
+   *
+   * @return {Boolean} true if the user can be considered as a devtools user.
+   */
+  isDevToolsUser() {
+    const selfXssCount = Services.prefs.getIntPref("devtools.selfxss.count", 0);
+    return selfXssCount > 0;
+  },
+
+  hookKeyShortcuts(window) {
+    const doc = window.document;
+
+    // hookKeyShortcuts can be called both from hookWindow and from the developer toggle
+    // onBeforeCreated. Make sure shortcuts are only added once per window.
+    if (doc.getElementById("devtoolsKeyset")) {
+      return;
+    }
+
+    const keyset = doc.createXULElement("keyset");
+    keyset.setAttribute("id", "devtoolsKeyset");
+
+    this.attachKeys(doc, lazy.KeyShortcuts, keyset);
+
+    // Appending a <key> element is not always enough. The <keyset> needs
+    // to be detached and reattached to make sure the <key> is taken into
+    // account (see bug 832984).
+    const mainKeyset = doc.getElementById("mainKeyset");
+    mainKeyset.parentNode.insertBefore(keyset, mainKeyset);
+  },
+
+  /**
+   * This method attaches on the key elements to the devtools keyset.
+   */
+  attachKeys(doc, keyShortcuts, keyset = doc.getElementById("devtoolsKeyset")) {
+    const window = doc.defaultView;
+    for (const key of keyShortcuts) {
+      if (!key.shortcut) {
+        // Shortcuts might be missing when a user relies on a language packs
+        // which is missing a recently uplifted shortcut. Language packs are
+        // typically updated a few days after a code uplift.
+        continue;
+      }
+      const xulKey = this.createKey(doc, key, () => this.onKey(window, key));
+      keyset.appendChild(xulKey);
+    }
+  },
+
+  /**
+   * This method removes keys from the devtools keyset.
+   */
+  removeKeys(doc, keyShortcuts) {
+    for (const key of keyShortcuts) {
+      const keyElement = doc.getElementById(this.getKeyElementId(key));
+      if (keyElement) {
+        keyElement.remove();
+      }
+    }
+  },
+
+  /**
+   * We only want to have the keyboard shortcuts active when the menu button is on.
+   * This function either adds or removes the elements.
+   * @param {boolean} isEnabled
+   */
+  toggleProfilerKeyShortcuts(isEnabled) {
+    const profilerKeyShortcuts = getProfilerKeyShortcuts();
+    for (const { document } of Services.wm.getEnumerator(null)) {
+      const devtoolsKeyset = document.getElementById("devtoolsKeyset");
+      const mainKeyset = document.getElementById("mainKeyset");
+
+      if (!devtoolsKeyset || !mainKeyset) {
+        // There may not be devtools keyset on this window.
+        continue;
+      }
+
+      const areProfilerKeysPresent = !!document.getElementById(
+        "key_profilerStartStop"
+      );
+      if (isEnabled === areProfilerKeysPresent) {
+        // Don't double add or double remove the shortcuts.
+        continue;
+      }
+      if (isEnabled) {
+        this.attachKeys(document, profilerKeyShortcuts);
+      } else {
+        this.removeKeys(document, profilerKeyShortcuts);
+      }
+      // Appending a <key> element is not always enough. The <keyset> needs
+      // to be detached and reattached to make sure the <key> is taken into
+      // account (see bug 832984).
+      mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset);
+    }
+  },
+
+  async onKey(window, key) {
+    try {
+      // The profiler doesn't care if DevTools is loaded, so provide a quick check
+      // first to bail out of checking if DevTools is available.
+      switch (key.id) {
+        case "profilerStartStop":
+        case "profilerStartStopAlternate": {
+          lazy.ProfilerPopupBackground.toggleProfiler("aboutprofiling");
+          return;
+        }
+        case "profilerCapture":
+        case "profilerCaptureAlternate": {
+          lazy.ProfilerPopupBackground.captureProfile("aboutprofiling");
+          return;
+        }
+      }
+
+      // Ignore the following key shortcut if DevTools aren't yet opened.
+      // The key shortcut is registered in this core component in order to
+      // work even when the web page is focused.
+      if (key.id == "javascriptTracingToggle" && !this.initialized) {
+        return;
+      }
+
+      // Record the timing at which this event started in order to compute later in
+      // gDevTools.showToolbox, the complete time it takes to open the toolbox.
+      // i.e. especially take `initDevTools` into account.
+      const startTime = Cu.now();
+      const require = this.initDevTools("KeyShortcut", key);
+      const {
+        gDevToolsBrowser,
+      } = require("devtools/client/framework/devtools-browser");
+      await gDevToolsBrowser.onKeyShortcut(window, key, startTime);
+    } catch (e) {
+      console.error(`Exception while trigerring key ${key}: ${e}\n${e.stack}`);
+    }
+  },
+
+  getKeyElementId({ id, toolId }) {
+    return "key_" + (id || toolId);
+  },
+
+  // Create a <xul:key> DOM Element
+  createKey(doc, key, oncommand) {
+    const { shortcut, modifiers: mod } = key;
+    const k = doc.createXULElement("key");
+    k.id = this.getKeyElementId(key);
+
+    if (shortcut.startsWith("VK_")) {
+      k.setAttribute("keycode", shortcut);
+      if (shortcut.match(/^VK_\d$/)) {
+        // Add the event keydown attribute to ensure that shortcuts work for combinations
+        // such as ctrl shift 1.
+        k.setAttribute("event", "keydown");
+      }
+    } else {
+      k.setAttribute("key", shortcut);
+    }
+
+    if (mod) {
+      k.setAttribute("modifiers", mod);
+    }
+
+    k.addEventListener("command", oncommand);
+
+    return k;
+  },
+
+  initDevTools(reason, key = "") {
+    // In the case of the --jsconsole and --jsdebugger command line parameters
+    // there is no browser window yet so we don't send any telemetry yet.
+    if (reason !== "CommandLine") {
+      this.sendEntryPointTelemetry(reason, key);
+    }
+
+    this.initialized = true;
+    const { require } = ChromeUtils.importESModule(
+      "resource://devtools/shared/loader/Loader.sys.mjs"
+    );
+    // Ensure loading main devtools module that hooks up into browser UI
+    // and initialize all devtools machinery.
+    // eslint-disable-next-line import/no-unassigned-import
+    require("devtools/client/framework/devtools-browser");
+    return require;
+  },
+
+  handleConsoleFlag(cmdLine) {
+    const window = Services.wm.getMostRecentWindow("devtools:webconsole");
+    if (!window) {
+      const require = this.initDevTools("CommandLine");
+      const {
+        BrowserConsoleManager,
+      } = require("devtools/client/webconsole/browser-console-manager");
+      BrowserConsoleManager.toggleBrowserConsole().catch(console.error);
+    } else {
+      // the Browser Console was already open
+      window.focus();
+    }
+
+    if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
+      cmdLine.preventDefault = true;
+    }
+  },
+
+  // Open the toolbox on the selected tab once the browser starts up.
+  async handleDevToolsFlag(window) {
+    const require = this.initDevTools("CommandLine");
+    const { gDevTools } = require("devtools/client/framework/devtools");
+    await gDevTools.showToolboxForTab(window.gBrowser.selectedTab);
+  },
+
+  _isRemoteDebuggingEnabled() {
+    let remoteDebuggingEnabled = false;
+    try {
+      remoteDebuggingEnabled = kDebuggerPrefs.every(pref => {
+        return Services.prefs.getBoolPref(pref);
+      });
+    } catch (ex) {
+      console.error(ex);
+      return false;
+    }
+    if (!remoteDebuggingEnabled) {
+      const errorMsg =
+        "Could not run chrome debugger! You need the following " +
+        "prefs to be set to true: " +
+        kDebuggerPrefs.join(", ");
+      console.error(new Error(errorMsg));
+      // Dump as well, as we're doing this from a commandline, make sure people
+      // don't miss it:
+      dump(errorMsg + "\n");
+    }
+    return remoteDebuggingEnabled;
+  },
+
+  handleDebuggerFlag(cmdLine, binaryPath) {
+    if (!this._isRemoteDebuggingEnabled()) {
+      return;
+    }
+
+    let devtoolsThreadResumed = false;
+    const pauseOnStartup = cmdLine.handleFlag("wait-for-jsdebugger", false);
+    if (pauseOnStartup) {
+      const observe = function (subject, topic, data) {
+        devtoolsThreadResumed = true;
+        Services.obs.removeObserver(observe, "devtools-thread-ready");
+      };
+      Services.obs.addObserver(observe, "devtools-thread-ready");
+    }
+
+    const { BrowserToolboxLauncher } = ChromeUtils.importESModule(
+      "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs"
+    );
+    // --jsdebugger $binaryPath is an helper alias to set MOZ_BROWSER_TOOLBOX_BINARY=$binaryPath
+    // See comment within BrowserToolboxLauncher.
+    // Setting it as an environment variable helps it being reused if we restart the browser via CmdOrCtrl+R
+    Services.env.set("MOZ_BROWSER_TOOLBOX_BINARY", binaryPath);
+
+    const browserToolboxLauncherConfig = {};
+
+    // If user passed the --jsdebugger in mochitests, we want to enable the
+    // multiprocess Browser Toolbox (by default it's parent process only)
+    if (Services.prefs.getBoolPref("devtools.testing", false)) {
+      browserToolboxLauncherConfig.forceMultiprocess = true;
+    }
+    BrowserToolboxLauncher.init(browserToolboxLauncherConfig);
+
+    if (pauseOnStartup) {
+      // Spin the event loop until the debugger connects.
+      const tm = Cc["@mozilla.org/thread-manager;1"].getService();
+      tm.spinEventLoopUntil("DevToolsStartup.jsm:handleDebuggerFlag", () => {
+        return devtoolsThreadResumed;
+      });
+    }
+
+    if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
+      cmdLine.preventDefault = true;
+    }
+  },
+
+  /**
+   * Handle the --start-debugger-server command line flag. The options are:
+   * --start-debugger-server
+   *   The portOrPath parameter is boolean true in this case. Reads and uses the defaults
+   *   from devtools.debugger.remote-port and devtools.debugger.remote-websocket prefs.
+   *   The default values of these prefs are port 6000, WebSocket disabled.
+   *
+   * --start-debugger-server 6789
+   *   Start the non-WebSocket server on port 6789.
+   *
+   * --start-debugger-server /path/to/filename
+   *   Start the server on a Unix domain socket.
+   *
+   * --start-debugger-server ws:6789
+   *   Start the WebSocket server on port 6789.
+   *
+   * --start-debugger-server ws:
+   *   Start the WebSocket server on the default port (taken from d.d.remote-port)
+   */
+  handleDevToolsServerFlag(cmdLine, portOrPath) {
+    if (!this._isRemoteDebuggingEnabled()) {
+      return;
+    }
+
+    let webSocket = false;
+    const defaultPort = Services.prefs.getIntPref(
+      "devtools.debugger.remote-port"
+    );
+    if (portOrPath === true) {
+      // Default to pref values if no values given on command line
+      webSocket = Services.prefs.getBoolPref(
+        "devtools.debugger.remote-websocket"
+      );
+      portOrPath = defaultPort;
+    } else if (portOrPath.startsWith("ws:")) {
+      webSocket = true;
+      const port = portOrPath.slice(3);
+      portOrPath = Number(port) ? port : defaultPort;
+    }
+
+    const {
+      useDistinctSystemPrincipalLoader,
+      releaseDistinctSystemPrincipalLoader,
+    } = ChromeUtils.importESModule(
+      "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
+    );
+
+    try {
+      // Create a separate loader instance, so that we can be sure to receive
+      // a separate instance of the DebuggingServer from the rest of the
+      // devtools.  This allows us to safely use the tools against even the
+      // actors and DebuggingServer itself, especially since we can mark
+      // serverLoader as invisible to the debugger (unlike the usual loader
+      // settings).
+      const serverLoader = useDistinctSystemPrincipalLoader(this);
+      const { DevToolsServer: devToolsServer } = serverLoader.require(
+        "resource://devtools/server/devtools-server.js"
+      );
+      const { SocketListener } = serverLoader.require(
+        "resource://devtools/shared/security/socket.js"
+      );
+      devToolsServer.init();
+
+      // Force the server to be kept running when the last connection closes.
+      // So that another client can connect after the previous one is disconnected.
+      devToolsServer.keepAlive = true;
+
+      devToolsServer.registerAllActors();
+      devToolsServer.allowChromeProcess = true;
+      const socketOptions = { portOrPath, webSocket };
+
+      const listener = new SocketListener(devToolsServer, socketOptions);
+      listener.open();
+      dump("Started devtools server on " + portOrPath + "\n");
+
+      // Prevent leaks on shutdown.
+      const close = () => {
+        Services.obs.removeObserver(close, "quit-application");
+        dump("Stopped devtools server on " + portOrPath + "\n");
+        if (listener) {
+          listener.close();
+        }
+        if (devToolsServer) {
+          devToolsServer.destroy();
+        }
+        releaseDistinctSystemPrincipalLoader(this);
+      };
+      Services.obs.addObserver(close, "quit-application");
+    } catch (e) {
+      dump("Unable to start devtools server on " + portOrPath + ": " + e);
+    }
+
+    if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
+      cmdLine.preventDefault = true;
+    }
+  },
+
+  /**
+   * Send entry point telemetry explaining how the devtools were launched. This
+   * functionality also lives inside `devtools/client/framework/browser-menus.js`
+   * because this codepath is only used the first time a toolbox is opened for a
+   * tab.
+   *
+   * @param {String} reason
+   *        One of "KeyShortcut", "SystemMenu", "HamburgerMenu", "ContextMenu",
+   *        "CommandLine".
+   * @param {String} key
+   *        The key used by a key shortcut.
+   */
+  sendEntryPointTelemetry(reason, key = "") {
+    if (!reason) {
+      return;
+    }
+
+    let keys = "";
+
+    if (reason === "KeyShortcut") {
+      let { modifiers, shortcut } = key;
+
+      modifiers = modifiers.replace(",", "+");
+
+      if (shortcut.startsWith("VK_")) {
+        shortcut = shortcut.substr(3);
+      }
+
+      keys = `${modifiers}+${shortcut}`;
+    }
+
+    const window = Services.wm.getMostRecentWindow("navigator:browser");
+
+    this.telemetry.addEventProperty(
+      window,
+      "open",
+      "tools",
+      null,
+      "shortcut",
+      keys
+    );
+    this.telemetry.addEventProperty(
+      window,
+      "open",
+      "tools",
+      null,
+      "entrypoint",
+      reason
+    );
+
+    if (this.recorded) {
+      return;
+    }
+
+    // Only save the first call for each firefox run as next call
+    // won't necessarely start the tool. For example key shortcuts may
+    // only change the currently selected tool.
+    try {
+      this.telemetry.getHistogramById("DEVTOOLS_ENTRY_POINT").add(reason);
+    } catch (e) {
+      dump("DevTools telemetry entry point failed: " + e + "\n");
+    }
+    this.recorded = true;
+  },
+
+  /**
+   * Hook the debugger tool to the "Debug Script" button of the slow script dialog.
+   */
+  setSlowScriptDebugHandler() {
+    const debugService = Cc["@mozilla.org/dom/slow-script-debug;1"].getService(
+      Ci.nsISlowScriptDebug
+    );
+
+    debugService.activationHandler = window => {
+      const chromeWindow = window.browsingContext.topChromeWindow;
+
+      let setupFinished = false;
+      this.slowScriptDebugHandler(chromeWindow.gBrowser.selectedTab).then(
+        () => {
+          setupFinished = true;
+        }
+      );
+
+      // Don't return from the interrupt handler until the debugger is brought
+      // up; no reason to continue executing the slow script.
+      const utils = window.windowUtils;
+      utils.enterModalState();
+      Services.tm.spinEventLoopUntil(
+        "devtools-browser.js:debugService.activationHandler",
+        () => {
+          return setupFinished;
+        }
+      );
+      utils.leaveModalState();
+    };
+
+    debugService.remoteActivationHandler = async (browser, callback) => {
+      try {
+        // Force selecting the freezing tab
+        const chromeWindow = browser.ownerGlobal;
+        const tab = chromeWindow.gBrowser.getTabForBrowser(browser);
+        chromeWindow.gBrowser.selectedTab = tab;
+
+        await this.slowScriptDebugHandler(tab);
+      } catch (e) {
+        console.error(e);
+      }
+      callback.finishDebuggerStartup();
+    };
+  },
+
+  /**
+   * Called by setSlowScriptDebugHandler, when a tab freeze because of a slow running script
+   */
+  async slowScriptDebugHandler(tab) {
+    const require = this.initDevTools("SlowScript");
+    const { gDevTools } = require("devtools/client/framework/devtools");
+    const toolbox = await gDevTools.showToolboxForTab(tab, {
+      toolId: "jsdebugger",
+    });
+    const threadFront = toolbox.threadFront;
+
+    // Break in place, which means resuming the debuggee thread and pausing
+    // right before the next step happens.
+    switch (threadFront.state) {
+      case "paused":
+        // When the debugger is already paused.
+        threadFront.resumeThenPause();
+        break;
+      case "attached":
+        // When the debugger is already open.
+        const onPaused = threadFront.once("paused");
+        threadFront.interrupt();
+        await onPaused;
+        threadFront.resumeThenPause();
+        break;
+      case "resuming":
+        // The debugger is newly opened.
+        const onResumed = threadFront.once("resumed");
+        await threadFront.interrupt();
+        await onResumed;
+        threadFront.resumeThenPause();
+        break;
+      default:
+        throw Error(
+          "invalid thread front state in slow script debug handler: " +
+            threadFront.state
+        );
+    }
+  },
+
+  // Used by tests and the toolbox to register the same key shortcuts in toolboxes loaded
+  // in a window window.
+  get KeyShortcuts() {
+    return lazy.KeyShortcuts;
+  },
+  get wrappedJSObject() {
+    return this;
+  },
+
+  get jsdebuggerHelpInfo() {
+    return `  --jsdebugger [<path>] Open the Browser Toolbox. Defaults to the local build
+                     but can be overridden by a firefox path.
+  --wait-for-jsdebugger Spin event loop until JS debugger connects.
+                     Enables debugging (some) application startup code paths.
+                     Only has an effect when \`--jsdebugger\` is also supplied.
+  --start-debugger-server [ws:][ <port> | <path> ] Start the devtools server on
+                     a TCP port or Unix domain socket path. Defaults to TCP port
+                     6000. Use WebSocket protocol if ws: prefix is specified.
+`;
+  },
+
+  get helpInfo() {
+    return `  --jsconsole        Open the Browser Console.
+  --devtools         Open DevTools on initial load.
+${this.jsdebuggerHelpInfo}`;
+  },
+
+  classID: Components.ID("{9e9a9283-0ce9-4e4a-8f1c-ba129a032c32}"),
+  QueryInterface: ChromeUtils.generateQI(["nsICommandLineHandler"]),
+};
+
+/**
+ * Singleton object that represents the JSON View in-content tool.
+ * It has the same lifetime as the browser.
+ */
+const JsonView = {
+  initialized: false,
+
+  initialize() {
+    // Prevent loading the frame script multiple times if we call this more than once.
+    if (this.initialized) {
+      return;
+    }
+    this.initialized = true;
+
+    // Register for messages coming from the child process.
+    // This is never removed as there is no particular need to unregister
+    // it during shutdown.
+    Services.mm.addMessageListener("devtools:jsonview:save", this.onSave);
+  },
+
+  // Message handlers for events from child processes
+
+  /**
+   * Save JSON to a file needs to be implemented here
+   * in the parent process.
+   */
+  onSave(message) {
+    const browser = message.target;
+    const chrome = browser.ownerGlobal;
+    if (message.data === null) {
+      // Save original contents
+      chrome.saveBrowser(browser);
+    } else {
+      if (
+        !message.data.startsWith("blob:resource://devtools/") ||
+        browser.contentPrincipal.origin != "resource://devtools"
+      ) {
+        console.error("Got invalid request to save JSON data");
+        return;
+      }
+      // The following code emulates saveBrowser, but:
+      // - Uses the given blob URL containing the custom contents to save.
+      // - Obtains the file name from the URL of the document, not the blob.
+      // - avoids passing the document and explicitly passes system principal.
+      //   We have a blob created by a null principal to save, and the null
+      //   principal is from the child. Null principals don't survive crossing
+      //   over IPC, so there's no other principal that'll work.
+      const persistable = browser.frameLoader;
+      persistable.startPersistence(null, {
+        onDocumentReady(doc) {
+          const uri = chrome.makeURI(doc.documentURI, doc.characterSet);
+          const filename = chrome.getDefaultFileName(undefined, uri, doc, null);
+          chrome.internalSave(
+            message.data,
+            null /* originalURL */,
+            null,
+            filename,
+            null,
+            doc.contentType,
+            false /* bypass cache */,
+            null /* filepicker title key */,
+            null /* file chosen */,
+            null /* referrer */,
+            doc.cookieJarSettings,
+            null /* initiating document */,
+            false /* don't skip prompt for a location */,
+            null /* cache key */,
+            lazy.PrivateBrowsingUtils.isBrowserPrivate(
+              browser
+            ) /* private browsing ? */,
+            Services.scriptSecurityManager.getSystemPrincipal()
+          );
+        },
+        onError(status) {
+          throw new Error("JSON Viewer's onSave failed in startPersistence");
+        },
+      });
+    }
+  },
+};
diff --git a/devtools/startup/components.conf b/devtools/startup/components.conf
new file mode 100644
index 0000000000..f88b9146da
--- /dev/null
+++ b/devtools/startup/components.conf
@@ -0,0 +1,30 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = []
+
+if buildconfig.substs['MOZ_DEVTOOLS'] == 'all':
+    Classes += [
+        {
+            'cid': '{9e9a9283-0ce9-4e4a-8f1c-ba129a032c32}',
+            'contract_ids': ['@mozilla.org/devtools/startup-clh;1'],
+            'esModule': 'resource:///modules/DevToolsStartup.sys.mjs',
+            'constructor': 'DevToolsStartup',
+            'categories': {'command-line-handler': 'm-devtools'},
+        },
+        {
+            'cid': '{1060afaf-dc9e-43da-8646-23a2faf48493}',
+            'contract_ids': ['@mozilla.org/network/protocol/about;1?what=debugging'],
+            'esModule': 'resource:///modules/AboutDebuggingRegistration.sys.mjs',
+            'constructor': 'AboutDebugging',
+        },
+        {
+            'cid': '{11342911-3135-45a8-8d71-737a2b0ad469}',
+            'contract_ids': ['@mozilla.org/network/protocol/about;1?what=devtools-toolbox'],
+            'esModule': 'resource:///modules/AboutDevToolsToolboxRegistration.sys.mjs',
+            'constructor': 'AboutDevtoolsToolbox',
+        },
+    ]
diff --git a/devtools/startup/jar.mn b/devtools/startup/jar.mn
new file mode 100644
index 0000000000..784b7c3ab7
--- /dev/null
+++ b/devtools/startup/jar.mn
@@ -0,0 +1,8 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+devtools-startup.jar:
+%   content devtools-startup %content/
+
+    content/DevToolsShim.sys.mjs  (DevToolsShim.sys.mjs)
diff --git a/devtools/startup/locales/en-US/key-shortcuts.ftl b/devtools/startup/locales/en-US/key-shortcuts.ftl
new file mode 100644
index 0000000000..bbc2c7ca1e
--- /dev/null
+++ b/devtools/startup/locales/en-US/key-shortcuts.ftl
@@ -0,0 +1,38 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Key pressed to open a toolbox with the default panel selected
+devtools-commandkey-toggle-toolbox = I
+# Alternative key pressed to open a toolbox with the default panel selected
+devtools-commandkey-toggle-toolbox-f12 = VK_F12
+# Key pressed to open the Browser Toolbox, used for debugging Firefox itself
+devtools-commandkey-browser-toolbox = I
+# Key pressed to open the Browser Console, used for debugging Firefox itself
+devtools-commandkey-browser-console = J
+# Key pressed to toggle on the Responsive Design Mode
+devtools-commandkey-responsive-design-mode = M
+# Key pressed to open a toolbox with the inspector panel selected
+devtools-commandkey-inspector = C
+# Key pressed to open a toolbox with the web console panel selected
+devtools-commandkey-webconsole = K
+# Key pressed to open a toolbox with the debugger panel selected
+devtools-commandkey-jsdebugger = Z
+# Key pressed to open a toolbox with the network monitor panel selected
+devtools-commandkey-netmonitor = E
+# Key pressed to open a toolbox with the style editor panel selected
+devtools-commandkey-styleeditor = VK_F7
+# Key pressed to open a toolbox with the performance panel selected
+devtools-commandkey-performance = VK_F5
+# Key pressed to open a toolbox with the storage panel selected
+devtools-commandkey-storage = VK_F9
+# Key pressed to open a toolbox with the DOM panel selected
+devtools-commandkey-dom = W
+# Key pressed to open a toolbox with the accessibility panel selected
+devtools-commandkey-accessibility-f12 = VK_F12
+# Key pressed to start or stop the performance profiler
+devtools-commandkey-profiler-start-stop = VK_1
+# Key pressed to capture a recorded performance profile
+devtools-commandkey-profiler-capture = VK_2
+# Key pressed to toggle the JavaScript tracing
+devtools-commandkey-javascript-tracing-toggle = VK_5
diff --git a/devtools/startup/locales/jar.mn b/devtools/startup/locales/jar.mn
new file mode 100644
index 0000000000..56bbc39203
--- /dev/null
+++ b/devtools/startup/locales/jar.mn
@@ -0,0 +1,7 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[localization] @AB_CD@.jar:
+    devtools/startup                         (%*.ftl)
diff --git a/devtools/startup/locales/moz.build b/devtools/startup/locales/moz.build
new file mode 100644
index 0000000000..d988c0ff9b
--- /dev/null
+++ b/devtools/startup/locales/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/devtools/startup/moz.build b/devtools/startup/moz.build
new file mode 100644
index 0000000000..22ca205602
--- /dev/null
+++ b/devtools/startup/moz.build
@@ -0,0 +1,28 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ["jar.mn"]
+
+# Register the startup components only for 'all' builds.
+if CONFIG["MOZ_DEVTOOLS"] == "all":
+    EXTRA_JS_MODULES += [
+        "AboutDebuggingRegistration.sys.mjs",
+        "AboutDevToolsToolboxRegistration.sys.mjs",
+        "DevToolsStartup.sys.mjs",
+    ]
+
+    DIRS += [
+        "locales",
+    ]
+
+XPCOM_MANIFESTS += [
+    "components.conf",
+]
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
+
+if CONFIG["MOZ_BUILD_APP"] != "mobile/android":
+    BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
diff --git a/devtools/startup/tests/browser/browser.toml b/devtools/startup/tests/browser/browser.toml
new file mode 100644
index 0000000000..ff9433663f
--- /dev/null
+++ b/devtools/startup/tests/browser/browser.toml
@@ -0,0 +1,11 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+  "command-line.html",
+  "command-line.js",
+]
+
+["browser_command_line_urls.js"]
+
+["browser_shim_disable_devtools.js"]
diff --git a/devtools/startup/tests/browser/browser_command_line_urls.js b/devtools/startup/tests/browser/browser_command_line_urls.js
new file mode 100644
index 0000000000..4494d635c6
--- /dev/null
+++ b/devtools/startup/tests/browser/browser_command_line_urls.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test command line processing of URLs meant to be intepreted by DevTools.
+ */
+
+/* eslint-env browser */
+
+const { DevToolsStartup } = ChromeUtils.importESModule(
+  "resource:///modules/DevToolsStartup.sys.mjs"
+);
+
+const { require } = ChromeUtils.importESModule(
+  "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+const { gDevTools } = require("devtools/client/framework/devtools");
+
+const URL_ROOT = "https://example.org/browser/devtools/startup/tests/browser/";
+
+const startup = new DevToolsStartup();
+// The feature covered here only work when calling firefox from command line
+// while it is already opened. So fake Firefox being already opened:
+startup.initialized = true;
+
+add_task(async function ignoredUrls() {
+  const tabCount = gBrowser.tabs.length;
+
+  // We explicitely try to ignore these URL which looks like line, but are passwords
+  sendUrlViaCommandLine("https://foo@user:123");
+  sendUrlViaCommandLine("https://foo@user:123");
+  sendUrlViaCommandLine("https://foo@123:456");
+
+  // The following is an invalid URL (domain with space)
+  sendUrlViaCommandLine("https://foo /index.html:123:456");
+
+  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+  await new Promise(r => setTimeout(r, 1000));
+
+  is(tabCount, gBrowser.tabs.length);
+});
+
+/**
+ * With DevTools closed, but DevToolsStartup "initialized",
+ * the url will be ignored
+ */
+add_task(async function openingWithDevToolsClosed() {
+  const url = URL_ROOT + "command-line.html:5:2";
+
+  const tabCount = gBrowser.tabs.length;
+  const ignoredUrl = sendUrlViaCommandLine(url);
+  ok(ignoredUrl, "The url is ignored when no devtools are opened");
+
+  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+  await new Promise(r => setTimeout(r, 1000));
+
+  is(tabCount, gBrowser.tabs.length);
+});
+
+/**
+ * With DevTools opened, but the source isn't in the debugged tab,
+ * the url will also be opened via view-source
+ */
+add_task(async function openingWithDevToolsButUnknownSource() {
+  const url = URL_ROOT + "command-line.html:5:2";
+
+  const tab = BrowserTestUtils.addTab(
+    gBrowser,
+    "data:text/html;charset=utf-8,<title>foo</title>"
+  );
+
+  const toolbox = await gDevTools.showToolboxForTab(gBrowser.selectedTab, {
+    toolId: "jsdebugger",
+  });
+
+  const newTabOpened = BrowserTestUtils.waitForNewTab(gBrowser);
+  sendUrlViaCommandLine(url);
+  const newTab = await newTabOpened;
+  is(
+    newTab.linkedBrowser.documentURI.spec,
+    "view-source:" + URL_ROOT + "command-line.html"
+  );
+
+  await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+    const selection = content.getSelection();
+    Assert.equal(
+      selection.toString(),
+      "  <title>Command line test page</title>",
+      "The 5th line is selected in view-source"
+    );
+  });
+  await gBrowser.removeTab(newTab);
+
+  await toolbox.destroy();
+  await gBrowser.removeTab(tab);
+});
+
+/**
+ * With DevTools opened, and the source is debugged by the debugger,
+ * the url will be opened in the debugger.
+ */
+add_task(async function openingWithDevToolsAndKnownSource() {
+  const url = URL_ROOT + "command-line.js:5:2";
+
+  const tab = await BrowserTestUtils.openNewForegroundTab(
+    gBrowser,
+    URL_ROOT + "command-line.html"
+  );
+  const toolbox = await gDevTools.showToolboxForTab(tab, {
+    toolId: "jsdebugger",
+  });
+
+  info("Open a first URL with line and column");
+  sendUrlViaCommandLine(url);
+
+  const dbg = toolbox.getPanel("jsdebugger");
+  const selectedLocation = await BrowserTestUtils.waitForCondition(() => {
+    return dbg._selectors.getSelectedLocation(dbg._getState());
+  });
+  is(selectedLocation.source.url, URL_ROOT + "command-line.js");
+  is(selectedLocation.line, 5);
+  is(selectedLocation.column, 1);
+
+  info("Open another URL with only a line");
+  const url2 = URL_ROOT + "command-line.js:6";
+  sendUrlViaCommandLine(url2);
+  const selectedLocation2 = await BrowserTestUtils.waitForCondition(() => {
+    const location = dbg._selectors.getSelectedLocation(dbg._getState());
+    return location.line == 6 ? location : false;
+  });
+  is(selectedLocation2.source.url, URL_ROOT + "command-line.js");
+  is(selectedLocation2.line, 6);
+  is(selectedLocation2.column, 0);
+
+  await toolbox.destroy();
+  await gBrowser.removeTab(tab);
+});
+
+// Fake opening an existing firefox instance with a URL
+// passed via `-url` command line argument
+function sendUrlViaCommandLine(url) {
+  const cmdLine = Cu.createCommandLine(
+    ["-url", url],
+    null,
+    Ci.nsICommandLine.STATE_REMOTE_EXPLICIT
+  );
+  startup.handle(cmdLine);
+
+  // Return true if DevToolsStartup ignored the url
+  // and let it be in the command line object.
+  return cmdLine.findFlag("url", false) != -1;
+}
diff --git a/devtools/startup/tests/browser/browser_shim_disable_devtools.js b/devtools/startup/tests/browser/browser_shim_disable_devtools.js
new file mode 100644
index 0000000000..1a16902099
--- /dev/null
+++ b/devtools/startup/tests/browser/browser_shim_disable_devtools.js
@@ -0,0 +1,165 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* eslint-env browser */
+
+const { require } = ChromeUtils.importESModule(
+  "resource://devtools/shared/loader/Loader.sys.mjs"
+);
+
+const { gDevTools } = require("devtools/client/framework/devtools");
+
+async function simulateMenuOpen(menu) {
+  return new Promise(resolve => {
+    menu.addEventListener("popupshown", resolve, { once: true });
+    menu.dispatchEvent(new MouseEvent("popupshowing"));
+    menu.dispatchEvent(new MouseEvent("popupshown"));
+  });
+}
+
+async function simulateMenuClosed(menu) {
+  return new Promise(resolve => {
+    menu.addEventListener("popuphidden", resolve, { once: true });
+    menu.dispatchEvent(new MouseEvent("popuphiding"));
+    menu.dispatchEvent(new MouseEvent("popuphidden"));
+  });
+}
+
+/**
+ * Test that the preference devtools.policy.disabled disables entry points for devtools.
+ */
+add_task(async function () {
+  info(
+    "Disable DevTools entry points (does not apply to the already created window"
+  );
+  await new Promise(resolve => {
+    const options = { set: [["devtools.policy.disabled", true]] };
+    SpecialPowers.pushPrefEnv(options, resolve);
+  });
+
+  // In DEV_EDITION the browser starts with the developer-button in the toolbar. This
+  // applies to all new windows and forces creating keyboard shortcuts. The preference
+  // tested should not change without restart, but for the needs of the test, remove the
+  // developer-button from the UI before opening a new window.
+  if (AppConstants.MOZ_DEV_EDITION) {
+    CustomizableUI.removeWidgetFromArea("developer-button");
+  }
+
+  info(
+    "Open a new window, all window-specific hooks for DevTools will be disabled."
+  );
+  const win = OpenBrowserWindow({ private: false });
+  await waitForDelayedStartupFinished(win);
+
+  info(
+    "Open a new tab on the new window to ensure the focus is on the new window"
+  );
+  const tab = BrowserTestUtils.addTab(
+    win.gBrowser,
+    "data:text/html;charset=utf-8,<title>foo</title>"
+  );
+  await BrowserTestUtils.browserLoaded(win.gBrowser.getBrowserForTab(tab));
+
+  info(
+    "Synthesize a DevTools shortcut, the toolbox should not open on this new window."
+  );
+  synthesizeToggleToolboxKey(win);
+
+  // There is no event to wait for here as this shortcut should have no effect.
+  /* eslint-disable mozilla/no-arbitrary-setTimeout */
+  await new Promise(r => setTimeout(r, 1000));
+
+  is(gDevTools._toolboxesPerCommands.size, 0, "No toolbox has been opened");
+
+  info("Open the context menu for the content page.");
+  const contextMenu = win.document.getElementById("contentAreaContextMenu");
+  const popupShownPromise = BrowserTestUtils.waitForEvent(
+    contextMenu,
+    "popupshown"
+  );
+  EventUtils.synthesizeMouseAtCenter(
+    win.document.documentElement,
+    { type: "contextmenu", button: 2 },
+    win
+  );
+  await popupShownPromise;
+
+  const inspectElementItem = contextMenu.querySelector(`#context-inspect`);
+  ok(
+    inspectElementItem.hidden,
+    "The inspect element item is hidden in the context menu"
+  );
+
+  info("Close the context menu");
+  const onContextMenuHidden = BrowserTestUtils.waitForEvent(
+    contextMenu,
+    "popuphidden"
+  );
+  contextMenu.hidePopup();
+  await onContextMenuHidden;
+
+  info("Open the menubar Tools menu");
+  const toolsMenuPopup = win.document.getElementById("menu_ToolsPopup");
+  const browserToolsMenu = win.document.getElementById("browserToolsMenu");
+  ok(
+    !browserToolsMenu.hidden,
+    "The Browser Tools item of the tools menu is visible"
+  );
+
+  await simulateMenuOpen(toolsMenuPopup);
+  const subMenu = win.document.getElementById("menuWebDeveloperPopup");
+
+  info("Open the Browser Tools sub-menu");
+  await simulateMenuOpen(subMenu);
+
+  const visibleMenuItems = Array.from(
+    subMenu.querySelectorAll("menuitem")
+  ).filter(item => !item.hidden);
+
+  const { menuitems } = require("devtools/client/menus");
+  for (const devtoolsItem of menuitems) {
+    ok(
+      !visibleMenuItems.some(item => item.id === devtoolsItem.id),
+      "DevTools menu item is not visible in the Browser Tools menu"
+    );
+  }
+
+  info("Close out the menu popups");
+  await simulateMenuClosed(subMenu);
+  await simulateMenuClosed(toolsMenuPopup);
+
+  win.gBrowser.removeTab(tab);
+
+  info("Close the test window");
+  const winClosed = BrowserTestUtils.windowClosed(win);
+  win.BrowserTryToCloseWindow();
+  await winClosed;
+});
+
+function waitForDelayedStartupFinished(win) {
+  return new Promise(resolve => {
+    Services.obs.addObserver(function observer(subject, topic) {
+      if (win == subject) {
+        Services.obs.removeObserver(
+          observer,
+          "browser-delayed-startup-finished"
+        );
+        resolve();
+      }
+    }, "browser-delayed-startup-finished");
+  });
+}
+
+/**
+ * Helper to call the toggle devtools shortcut.
+ */
+function synthesizeToggleToolboxKey(win) {
+  info("Trigger the toogle toolbox shortcut");
+  if (Services.appinfo.OS == "Darwin") {
+    EventUtils.synthesizeKey("i", { accelKey: true, altKey: true }, win);
+  } else {
+    EventUtils.synthesizeKey("i", { accelKey: true, shiftKey: true }, win);
+  }
+}
diff --git a/devtools/startup/tests/browser/command-line.html b/devtools/startup/tests/browser/command-line.html
new file mode 100644
index 0000000000..c8f14b95bd
--- /dev/null
+++ b/devtools/startup/tests/browser/command-line.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <meta charset="UTF-8">
+  <title>Command line test page</title>
+</head>
+<body>
+  <script src="command-line.js"></script>
+</body>
+</html>
diff --git a/devtools/startup/tests/browser/command-line.js b/devtools/startup/tests/browser/command-line.js
new file mode 100644
index 0000000000..c15c83407b
--- /dev/null
+++ b/devtools/startup/tests/browser/command-line.js
@@ -0,0 +1,3 @@
+"use strict";
+
+console.log("command line script");
diff --git a/devtools/startup/tests/xpcshell/.eslintrc.js b/devtools/startup/tests/xpcshell/.eslintrc.js
new file mode 100644
index 0000000000..6581a0bf32
--- /dev/null
+++ b/devtools/startup/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+  extends: "../../../.eslintrc.xpcshell.js",
+};
diff --git a/devtools/startup/tests/xpcshell/test_devtools_shim.js b/devtools/startup/tests/xpcshell/test_devtools_shim.js
new file mode 100644
index 0000000000..6e0ef2789d
--- /dev/null
+++ b/devtools/startup/tests/xpcshell/test_devtools_shim.js
@@ -0,0 +1,202 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { DevToolsShim } = ChromeUtils.importESModule(
+  "chrome://devtools-startup/content/DevToolsShim.sys.mjs"
+);
+
+// Test the DevToolsShim
+
+/**
+ * Create a mocked version of DevTools that records all calls made to methods expected
+ * to be called by DevToolsShim.
+ */
+function createMockDevTools() {
+  const methods = [
+    "on",
+    "off",
+    "emit",
+    "saveDevToolsSession",
+    "restoreDevToolsSession",
+  ];
+
+  const mock = {
+    callLog: {},
+  };
+
+  for (const method of methods) {
+    // Create a stub for method, that only pushes its arguments in the inner callLog
+    mock[method] = function (...args) {
+      mock.callLog[method].push(args);
+    };
+    mock.callLog[method] = [];
+  }
+
+  return mock;
+}
+
+/**
+ * Check if a given method was called an expected number of times, and finally check the
+ * arguments provided to the last call, if appropriate.
+ */
+function checkCalls(mock, method, length, lastArgs) {
+  Assert.strictEqual(
+    mock.callLog[method].length,
+    length,
+    "Devtools.on was called the expected number of times"
+  );
+
+  // If we don't want to check the last call or if the method was never called, bail out.
+  if (!lastArgs || length === 0) {
+    return;
+  }
+
+  for (let i = 0; i < lastArgs.length; i++) {
+    const expectedArg = lastArgs[i];
+    Assert.strictEqual(
+      mock.callLog[method][length - 1][i],
+      expectedArg,
+      `Devtools.${method} was called with the expected argument (index ${i})`
+    );
+  }
+}
+
+function test_register_unregister() {
+  ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
+
+  DevToolsShim.register(createMockDevTools());
+  ok(DevToolsShim.isInitialized(), "DevTools are initialized");
+
+  DevToolsShim.unregister();
+  ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
+}
+
+function test_on_is_forwarded_to_devtools() {
+  ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
+
+  function cb1() {}
+  function cb2() {}
+  const mock = createMockDevTools();
+
+  DevToolsShim.on("test_event", cb1);
+  DevToolsShim.register(mock);
+  checkCalls(mock, "on", 1, ["test_event", cb1]);
+
+  DevToolsShim.on("other_event", cb2);
+  checkCalls(mock, "on", 2, ["other_event", cb2]);
+}
+
+function test_off_called_before_registering_devtools() {
+  ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
+
+  function cb1() {}
+  const mock = createMockDevTools();
+
+  DevToolsShim.on("test_event", cb1);
+  DevToolsShim.off("test_event", cb1);
+
+  DevToolsShim.register(mock);
+  checkCalls(mock, "on", 0);
+}
+
+function test_off_called_before_with_bad_callback() {
+  ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
+
+  function cb1() {}
+  function cb2() {}
+  const mock = createMockDevTools();
+
+  DevToolsShim.on("test_event", cb1);
+  DevToolsShim.off("test_event", cb2);
+
+  DevToolsShim.register(mock);
+  // on should still be called
+  checkCalls(mock, "on", 1, ["test_event", cb1]);
+  // Calls to off should not be held and forwarded.
+  checkCalls(mock, "off", 0);
+}
+
+function test_events() {
+  ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
+
+  const mock = createMockDevTools();
+  // Check emit was not called.
+  checkCalls(mock, "emit", 0);
+
+  // Check emit is called once with the devtools-registered event.
+  DevToolsShim.register(mock);
+  checkCalls(mock, "emit", 1, ["devtools-registered"]);
+
+  // Check emit is called once with the devtools-unregistered event.
+  DevToolsShim.unregister();
+  checkCalls(mock, "emit", 2, ["devtools-unregistered"]);
+}
+
+function test_restore_session_apis() {
+  // Backup method that will be updated for the test.
+  const initDevToolsBackup = DevToolsShim.initDevTools;
+
+  // Create fake session objects to restore.
+  const sessionWithoutDevTools = {};
+  const sessionWithDevTools = {
+    browserConsole: true,
+  };
+
+  Services.prefs.setBoolPref("devtools.policy.disabled", true);
+  ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
+  ok(!DevToolsShim.isEnabled(), "DevTools are not enabled");
+
+  // Check that save & restore DevToolsSession don't initialize the tools and don't
+  // crash.
+  DevToolsShim.saveDevToolsSession({});
+  DevToolsShim.restoreDevToolsSession(sessionWithDevTools);
+  ok(!DevToolsShim.isInitialized(), "DevTools are still not initialized");
+
+  Services.prefs.setBoolPref("devtools.policy.disabled", false);
+  ok(DevToolsShim.isEnabled(), "DevTools are enabled");
+  ok(!DevToolsShim.isInitialized(), "DevTools are not initialized");
+
+  // Check that DevTools are not initialized when calling restoreDevToolsSession without
+  // DevTools related data.
+  DevToolsShim.restoreDevToolsSession(sessionWithoutDevTools);
+  ok(!DevToolsShim.isInitialized(), "DevTools are still not initialized");
+
+  const mock = createMockDevTools();
+  DevToolsShim.initDevTools = () => {
+    // Next call to restoreDevToolsSession is expected to initialize DevTools, which we
+    // simulate here by registering our mock.
+    DevToolsShim.register(mock);
+  };
+
+  DevToolsShim.restoreDevToolsSession(sessionWithDevTools);
+  checkCalls(mock, "restoreDevToolsSession", 1, [sessionWithDevTools]);
+
+  ok(DevToolsShim.isInitialized(), "DevTools are initialized");
+
+  DevToolsShim.saveDevToolsSession({});
+  checkCalls(mock, "saveDevToolsSession", 1, []);
+
+  // Restore initDevTools backup.
+  DevToolsShim.initDevTools = initDevToolsBackup;
+}
+
+function run_test() {
+  test_register_unregister();
+  DevToolsShim.unregister();
+
+  test_on_is_forwarded_to_devtools();
+  DevToolsShim.unregister();
+
+  test_off_called_before_registering_devtools();
+  DevToolsShim.unregister();
+
+  test_off_called_before_with_bad_callback();
+  DevToolsShim.unregister();
+
+  test_restore_session_apis();
+  DevToolsShim.unregister();
+
+  test_events();
+}
diff --git a/devtools/startup/tests/xpcshell/xpcshell.toml b/devtools/startup/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..2e7e69614b
--- /dev/null
+++ b/devtools/startup/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+tags = "devtools"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"]
+
+["test_devtools_shim.js"]
-- 
cgit v1.2.3