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/. */
6 * This XPCOM component is loaded very early.
7 * Be careful to lazy load dependencies as much as possible.
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.
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.
23 const kDebuggerPrefs = [
24 "devtools.debugger.remote-enabled",
25 "devtools.chrome.enabled",
28 const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled";
30 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
33 ChromeUtils.defineESModuleGetters(lazy, {
34 CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs",
35 CustomizableWidgets: "resource:///modules/CustomizableWidgets.sys.mjs",
36 PanelMultiView: "resource:///modules/PanelMultiView.sys.mjs",
37 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
39 "resource://devtools/client/performance-new/popup/menu-button.sys.mjs",
40 WebChannel: "resource://gre/modules/WebChannel.sys.mjs",
43 // We don't want to spend time initializing the full loader here so we create
44 // our own lazy require.
45 ChromeUtils.defineLazyGetter(lazy, "Telemetry", function () {
46 const { require } = ChromeUtils.importESModule(
47 "resource://devtools/shared/loader/Loader.sys.mjs"
49 // eslint-disable-next-line no-shadow
50 const Telemetry = require("devtools/client/shared/telemetry");
55 ChromeUtils.defineLazyGetter(lazy, "KeyShortcutsBundle", function () {
56 return new Localization(["devtools/startup/key-shortcuts.ftl"], true);
60 * Safely retrieve a localized DevTools key shortcut from KeyShortcutsBundle.
61 * If the shortcut is not available, this will return null. Consumer code
62 * should rely on this to skip unavailable shortcuts.
64 * Note that all shortcuts should always be available, but there is a notable
65 * exception, which is why we have to do this. When a localization change is
66 * uplifted to beta, language packs will not be updated immediately when the
67 * updated beta is available.
69 * This means that language pack users might get a new Beta version but will not
70 * have a language pack with the new strings yet.
72 function getLocalizedKeyShortcut(id) {
74 return lazy.KeyShortcutsBundle.formatValueSync(id);
76 console.error("Failed to retrieve DevTools localized shortcut for id", id);
81 ChromeUtils.defineLazyGetter(lazy, "KeyShortcuts", function () {
82 const isMac = AppConstants.platform == "macosx";
84 // Common modifier shared by most key shortcuts
85 const modifiers = isMac ? "accel,alt" : "accel,shift";
87 // List of all key shortcuts triggering installation UI
88 // `id` should match tool's id from client/definitions.js
90 // The following keys are also registered in /client/menus.js
91 // And should be synced.
93 // Both are toggling the toolbox on the last selected panel
94 // or the default one.
97 shortcut: getLocalizedKeyShortcut("devtools-commandkey-toggle-toolbox"),
100 // All locales are using F12
102 id: "toggleToolboxF12",
103 shortcut: getLocalizedKeyShortcut(
104 "devtools-commandkey-toggle-toolbox-f12"
106 modifiers: "", // F12 is the only one without modifiers
108 // Open the Browser Toolbox
110 id: "browserToolbox",
111 shortcut: getLocalizedKeyShortcut("devtools-commandkey-browser-toolbox"),
112 modifiers: "accel,alt,shift",
114 // Open the Browser Console
116 id: "browserConsole",
117 shortcut: getLocalizedKeyShortcut("devtools-commandkey-browser-console"),
118 modifiers: "accel,shift",
120 // Toggle the Responsive Design Mode
122 id: "responsiveDesignMode",
123 shortcut: getLocalizedKeyShortcut(
124 "devtools-commandkey-responsive-design-mode"
128 // The following keys are also registered in /client/definitions.js
129 // and should be synced.
131 // Key for opening the Inspector
134 shortcut: getLocalizedKeyShortcut("devtools-commandkey-inspector"),
137 // Key for opening the Web Console
139 toolId: "webconsole",
140 shortcut: getLocalizedKeyShortcut("devtools-commandkey-webconsole"),
143 // Key for opening the Debugger
145 toolId: "jsdebugger",
146 shortcut: getLocalizedKeyShortcut("devtools-commandkey-jsdebugger"),
149 // Key for opening the Network Monitor
151 toolId: "netmonitor",
152 shortcut: getLocalizedKeyShortcut("devtools-commandkey-netmonitor"),
155 // Key for opening the Style Editor
157 toolId: "styleeditor",
158 shortcut: getLocalizedKeyShortcut("devtools-commandkey-styleeditor"),
161 // Key for opening the Performance Panel
163 toolId: "performance",
164 shortcut: getLocalizedKeyShortcut("devtools-commandkey-performance"),
167 // Key for opening the Storage Panel
170 shortcut: getLocalizedKeyShortcut("devtools-commandkey-storage"),
173 // Key for opening the DOM Panel
176 shortcut: getLocalizedKeyShortcut("devtools-commandkey-dom"),
179 // Key for opening the Accessibility Panel
181 toolId: "accessibility",
182 shortcut: getLocalizedKeyShortcut(
183 "devtools-commandkey-accessibility-f12"
190 // Add the extra key command for macOS, so you can open the inspector with cmd+shift+C
191 // like on Chrome DevTools.
195 shortcut: getLocalizedKeyShortcut("devtools-commandkey-inspector"),
196 modifiers: "accel,shift",
200 if (lazy.ProfilerMenuButton.isInNavbar()) {
201 shortcuts.push(...getProfilerKeyShortcuts());
204 // Allow toggling the JavaScript tracing not only from DevTools UI,
205 // but also from the web page when it is focused.
207 Services.prefs.getBoolPref(
208 "devtools.debugger.features.javascript-tracing",
213 id: "javascriptTracingToggle",
214 shortcut: getLocalizedKeyShortcut(
215 "devtools-commandkey-javascript-tracing-toggle"
217 modifiers: "control,shift",
224 function getProfilerKeyShortcuts() {
226 // Start/stop the profiler
228 id: "profilerStartStop",
229 shortcut: getLocalizedKeyShortcut(
230 "devtools-commandkey-profiler-start-stop"
232 modifiers: "control,shift",
236 id: "profilerCapture",
237 shortcut: getLocalizedKeyShortcut("devtools-commandkey-profiler-capture"),
238 modifiers: "control,shift",
240 // Because it's not uncommon for content or extension to bind this
241 // shortcut, allow using alt as well for starting and stopping the profiler
243 id: "profilerStartStopAlternate",
244 shortcut: getLocalizedKeyShortcut(
245 "devtools-commandkey-profiler-start-stop"
247 modifiers: "control,shift,alt",
250 id: "profilerCaptureAlternate",
251 shortcut: getLocalizedKeyShortcut("devtools-commandkey-profiler-capture"),
252 modifiers: "control,shift,alt",
258 * Validate the URL that will be used for the WebChannel for the profiler.
260 * @param {string} targetUrl
263 export function validateProfilerWebChannelUrl(targetUrl) {
264 const frontEndUrl = "https://profiler.firefox.com";
266 if (targetUrl !== frontEndUrl) {
267 // The user can specify either localhost or deploy previews as well as
268 // the official frontend URL for testing.
271 /^https?:\/\/example\.com$/.test(targetUrl) ||
272 // Allows the following:
273 // "http://localhost:4242"
274 // "http://localhost:4242/"
275 // "http://localhost:3"
276 // "http://localhost:334798455"
277 /^http:\/\/localhost:\d+\/?$/.test(targetUrl) ||
278 // Allows the following:
279 // "https://deploy-preview-1234--perf-html.netlify.com"
280 // "https://deploy-preview-1234--perf-html.netlify.com/"
281 // "https://deploy-preview-1234567--perf-html.netlify.app"
282 // "https://main--perf-html.netlify.app"
283 /^https:\/\/(?:deploy-preview-\d+|main)--perf-html\.netlify\.(?:com|app)\/?$/.test(
287 // This URL is one of the allowed ones to be used for configuration.
292 `The preference "devtools.performance.recording.ui-base-url" was set to a ` +
293 "URL that is not allowed. No WebChannel messages will be sent between the " +
294 `browser and that URL. Falling back to ${frontEndUrl}. Only localhost ` +
295 "and deploy previews URLs are allowed.",
303 ChromeUtils.defineLazyGetter(lazy, "ProfilerPopupBackground", function () {
304 return ChromeUtils.importESModule(
305 "resource://devtools/client/performance-new/shared/background.sys.mjs"
309 export function DevToolsStartup() {
310 this.onWindowReady = this.onWindowReady.bind(this);
311 this.addDevToolsItemsToSubview = this.addDevToolsItemsToSubview.bind(this);
312 this.onMoreToolsViewShowing = this.onMoreToolsViewShowing.bind(this);
313 this.toggleProfilerKeyShortcuts = this.toggleProfilerKeyShortcuts.bind(this);
316 DevToolsStartup.prototype = {
318 * Boolean flag to check if DevTools have been already initialized or not.
319 * By initialized, we mean that its main modules are loaded.
324 * Boolean flag to check if the devtools initialization was already sent to telemetry.
325 * We only want to record one devtools entry point per Firefox run, but we are not
326 * interested in all the entry points.
331 if (!this._telemetry) {
332 this._telemetry = new lazy.Telemetry();
333 this._telemetry.setEventRecordingEnabled(true);
335 return this._telemetry;
339 * Flag that indicates if the developer toggle was already added to customizableUI.
341 developerToggleCreated: false,
344 * Flag that indicates if the profiler recording popup was already added to
347 profilerRecordingButtonCreated: false,
349 isDisabledByPolicy() {
350 return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false);
354 const flags = this.readCommandLineFlags(cmdLine);
356 // handle() can be called after browser startup (e.g. opening links from other apps).
357 const isInitialLaunch =
358 cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH;
359 if (isInitialLaunch) {
360 // Store devtoolsFlag to check it later in onWindowReady.
361 this.devtoolsFlag = flags.devtools;
363 /* eslint-disable mozilla/balanced-observers */
364 // We are not expecting to remove those listeners until Firefox closes.
366 // Only top level Firefox Windows fire a browser-delayed-startup-finished event
367 Services.obs.addObserver(
369 "browser-delayed-startup-finished"
372 // Add DevTools menu items to the "More Tools" view.
373 Services.obs.addObserver(
374 this.onMoreToolsViewShowing,
375 "web-developer-tools-view-showing"
377 /* eslint-enable mozilla/balanced-observers */
379 if (!this.isDisabledByPolicy()) {
380 if (AppConstants.MOZ_DEV_EDITION) {
381 // On DevEdition, the developer toggle is displayed by default in the navbar
382 // area and should be created before the first paint.
383 this.hookDeveloperToggle();
386 this.hookProfilerRecordingButton();
391 this.commandLine = true;
392 this.handleConsoleFlag(cmdLine);
394 if (flags.debugger) {
395 this.commandLine = true;
397 typeof flags.debugger == "string" ? flags.debugger : null;
398 this.handleDebuggerFlag(cmdLine, binaryPath);
401 if (flags.devToolsServer) {
402 this.handleDevToolsServerFlag(cmdLine, flags.devToolsServer);
405 // If Firefox is already opened, and DevTools are also already opened,
406 // try to open links passed via command line arguments.
407 if (!isInitialLaunch && this.initialized && cmdLine.length) {
408 this.checkForDebuggerLink(cmdLine);
413 * Lookup in all arguments passed to firefox binary to find
414 * URLs including a precise location, like this:
415 * https://domain.com/file.js:1:10 (URL ending with `:${line}:${number}`)
416 * When such argument exists, try to open this source and precise location
419 * @param {nsICommandLine} cmdLine
421 checkForDebuggerLink(cmdLine) {
422 const urlFlagIdx = cmdLine.findFlag("url", false);
423 // Bail out when there is no -url argument, or if that's last and so there is no URL after it.
424 if (urlFlagIdx == -1 && urlFlagIdx + 1 < cmdLine.length) {
428 // The following code would only work if we have a top level browser window opened
429 const window = Services.wm.getMostRecentWindow("navigator:browser");
434 const urlParam = cmdLine.getArgument(urlFlagIdx + 1);
436 // Avoid processing valid url like:
437 // http://foo@user:123
438 // Note that when loading `http://foo.com` the URL of the default html page will be `http://foo.com/`.
439 // So that there will always be another `/` after `https://`
441 (urlParam.startsWith("http://") || urlParam.startsWith("https://")) &&
442 urlParam.lastIndexOf("/") <= 7
447 let match = urlParam.match(/^(?<url>\w+:.+):(?<line>\d+):(?<column>\d+)$/);
449 // fallback on only having the line when there is no column
450 match = urlParam.match(/^(?<url>\w+:.+):(?<line>\d+)?$/);
456 // line and column are supposed to be 1-based.
457 const { url, line, column } = match.groups;
459 // Debugger internal uses 0-based column number.
460 // NOTE: Non-debugger view-source doesn't use column number.
461 const columnOneBased = parseInt(column || 0, 10);
462 const columnZeroBased = columnOneBased > 0 ? columnOneBased - 1 : 0;
464 // If for any reason the final url is invalid, ignore it
466 Services.io.newURI(url);
471 const require = this.initDevTools("CommandLine");
472 const { gDevTools } = require("devtools/client/framework/devtools");
473 const toolbox = gDevTools.getToolboxForTab(window.gBrowser.selectedTab);
474 // Ignore the url if there is no devtools currently opened for the current tab
479 // Avoid regular Firefox code from processing this argument,
480 // otherwise we would open the source in DevTools and in a new tab.
482 // /!\ This has to be called synchronously from the call to `DevToolsStartup.handle(cmdLine)`
483 // Otherwise the next command lines listener will interpret the argument redundantly.
484 cmdLine.removeArguments(urlFlagIdx, urlFlagIdx + 1);
486 // Avoid opening a new empty top level window if there is no more arguments
487 if (!cmdLine.length) {
488 cmdLine.preventDefault = true;
491 // Immediately focus the browser window in order, to focus devtools, or the view-source tab.
492 // Otherwise, without this, the terminal would still be the topmost window.
495 // Note that the following method is async and returns a promise.
496 // But the current method has to be synchronous because of cmdLine.removeArguments.
497 // Also note that it will fallback to view-source when the source url isn't found in the debugger
498 toolbox.viewSourceInDebugger(
507 readCommandLineFlags(cmdLine) {
508 // All command line flags are disabled if DevTools are disabled by policy.
509 if (this.isDisabledByPolicy()) {
514 devToolsServer: false,
518 const console = cmdLine.handleFlag("jsconsole", false);
519 const devtools = cmdLine.handleFlag("devtools", false);
523 devToolsServer = cmdLine.handleFlagWithParam(
524 "start-debugger-server",
528 // We get an error if the option is given but not followed by a value.
529 // By catching and trying again, the value is effectively optional.
530 devToolsServer = cmdLine.handleFlag("start-debugger-server", false);
535 debuggerFlag = cmdLine.handleFlagWithParam("jsdebugger", false);
537 // We get an error if the option is given but not followed by a value.
538 // By catching and trying again, the value is effectively optional.
539 debuggerFlag = cmdLine.handleFlag("jsdebugger", false);
542 return { console, debugger: debuggerFlag, devtools, devToolsServer };
546 * Called when receiving the "browser-delayed-startup-finished" event for a new
549 onWindowReady(window) {
551 this.isDisabledByPolicy() ||
552 AppConstants.MOZ_APP_NAME == "thunderbird"
557 this.hookWindow(window);
559 // This listener is called for all Firefox windows, but we want to execute some code
561 if (!this._firstWindowReadyReceived) {
562 this.onFirstWindowReady(window);
563 this._firstWindowReadyReceived = true;
566 JsonView.initialize();
569 onFirstWindowReady(window) {
570 if (this.devtoolsFlag) {
571 this.handleDevToolsFlag(window);
573 // In the case of the --jsconsole and --jsdebugger command line parameters
574 // there was no browser window when they were processed so we act on the
575 // this.commandline flag instead.
576 if (this.commandLine) {
577 this.sendEntryPointTelemetry("CommandLine");
580 this.setSlowScriptDebugHandler();
584 * Register listeners to all possible entry points for Developer Tools.
585 * But instead of implementing the actual actions, defer to DevTools codebase.
586 * In most cases, it only needs to call this.initDevTools which handles the rest.
587 * We do that to prevent loading any DevTools module until the user intent to use them.
590 // Key Shortcuts need to be added on all the created windows.
591 this.hookKeyShortcuts(window);
593 // In some situations (e.g. starting Firefox with --jsconsole) DevTools will be
594 // initialized before the first browser-delayed-startup-finished event is received.
595 // We use a dedicated flag because we still need to hook the developer toggle.
596 this.hookDeveloperToggle();
597 this.hookProfilerRecordingButton();
599 // The developer menu hook only needs to be added if devtools have not been
601 if (!this.initialized) {
602 this.hookBrowserToolsMenu(window);
607 * Dynamically register a wrench icon in the customization menu.
608 * You can use this button by right clicking on Firefox toolbar
609 * and dragging it from the customization panel to the toolbar.
610 * (i.e. this isn't displayed by default to users!)
612 * _But_, the "Browser Tools" entry in the hamburger menu (the menu with
613 * 3 horizontal lines), is using this "developer-button" view to populate
614 * its menu. So we have to register this button for the menu to work.
616 * Also, this menu duplicates its own entries from the "Browser Tools"
617 * menu in the system menu, under "Tools" main menu item. The system
618 * menu is being hooked by "hookBrowserToolsMenu" which ends up calling
619 * devtools/client/framework/browser-menus to create the items for real,
620 * initDevTools, from onViewShowing is also calling browser-menu.
622 hookDeveloperToggle() {
623 if (this.developerToggleCreated) {
627 const id = "developer-button";
628 const widget = lazy.CustomizableUI.getWidget(id);
629 if (widget && widget.provider == lazy.CustomizableUI.PROVIDER_API) {
633 const panelviewId = "PanelUI-developer-tools";
634 const subviewId = "PanelUI-developer-tools-view";
640 shortcutId: "key_toggleToolbox",
641 tooltiptext: "developer-button.tooltiptext2",
642 onViewShowing: event => {
643 const doc = event.target.ownerDocument;
644 const developerItems = lazy.PanelMultiView.getViewNode(doc, subviewId);
645 this.addDevToolsItemsToSubview(developerItems);
648 // Since onBeforeCreated already bails out when initialized, we can call
650 this.onBeforeCreated(anchor.ownerDocument);
652 onBeforeCreated: doc => {
653 // The developer toggle needs the "key_toggleToolbox" <key> element.
654 // In DEV EDITION, the toggle is added before 1st paint and hookKeyShortcuts() is
655 // not called yet when CustomizableUI creates the widget.
656 this.hookKeyShortcuts(doc.defaultView);
659 lazy.CustomizableUI.createWidget(item);
660 lazy.CustomizableWidgets.push(item);
662 this.developerToggleCreated = true;
665 addDevToolsItemsToSubview(subview) {
666 // Initialize DevTools to create all menuitems in the system menu before
667 // trying to copy them.
668 this.initDevTools("HamburgerMenu");
670 // Populate the subview with whatever menuitems are in the developer
671 // menu. We skip menu elements, because the menu panel has no way
672 // of dealing with those right now.
673 const doc = subview.ownerDocument;
674 const menu = doc.getElementById("menuWebDeveloperPopup");
675 const itemsToDisplay = [...menu.children];
677 lazy.CustomizableUI.clearSubview(subview);
678 lazy.CustomizableUI.fillSubviewFromMenuItems(itemsToDisplay, subview);
681 onMoreToolsViewShowing(moreToolsView) {
682 this.addDevToolsItemsToSubview(moreToolsView);
686 * Register the profiler recording button. This button will be available
687 * in the customization palette for the Firefox toolbar. In addition, it can be
688 * enabled from profiler.firefox.com.
690 hookProfilerRecordingButton() {
691 if (this.profilerRecordingButtonCreated) {
694 const featureFlagPref = "devtools.performance.popup.feature-flag";
695 const isPopupFeatureFlagEnabled =
696 Services.prefs.getBoolPref(featureFlagPref);
697 this.profilerRecordingButtonCreated = true;
699 // Listen for messages from the front-end. This needs to happen even if the
700 // button isn't enabled yet. This will allow the front-end to turn on the
701 // popup for our users, regardless of if the feature is enabled by default.
702 this.initializeProfilerWebChannel();
704 if (isPopupFeatureFlagEnabled) {
705 // Initialize the CustomizableUI widget.
706 lazy.ProfilerMenuButton.initialize(this.toggleProfilerKeyShortcuts);
708 // The feature flag is not enabled, but watch for it to be enabled. If it is,
709 // initialize everything.
710 const enable = () => {
711 lazy.ProfilerMenuButton.initialize(this.toggleProfilerKeyShortcuts);
712 Services.prefs.removeObserver(featureFlagPref, enable);
714 Services.prefs.addObserver(featureFlagPref, enable);
719 * Initialize the WebChannel for profiler.firefox.com. This function happens at
720 * startup, so care should be taken to minimize its performance impact. The WebChannel
721 * is a mechanism that is used to communicate between the browser, and front-end code.
723 initializeProfilerWebChannel() {
726 // Register a channel for the URL in preferences. Also update the WebChannel if
728 const urlPref = "devtools.performance.recording.ui-base-url";
730 // This method is only run once per Firefox instance, so it should not be
731 // strictly necessary to remove observers here.
732 // eslint-disable-next-line mozilla/balanced-observers
733 Services.prefs.addObserver(urlPref, registerWebChannel);
735 registerWebChannel();
737 function registerWebChannel() {
739 channel.stopListening();
742 const urlForWebChannel = Services.io.newURI(
743 validateProfilerWebChannelUrl(Services.prefs.getStringPref(urlPref))
746 channel = new lazy.WebChannel("profiler.firefox.com", urlForWebChannel);
748 channel.listen((id, message, target) => {
749 // Defer loading the ProfilerPopupBackground script until it's absolutely needed,
750 // as this code path gets loaded at startup.
751 lazy.ProfilerPopupBackground.handleWebChannelMessage(
762 * We listen to the "Browser Tools" system menu, which is under "Tools" main item.
763 * This menu item is hardcoded empty in Firefox UI. We listen for its opening to
764 * populate it lazily. Loading main DevTools module is going to populate it.
766 hookBrowserToolsMenu(window) {
767 const menu = window.document.getElementById("browserToolsMenu");
768 const onPopupShowing = () => {
769 menu.removeEventListener("popupshowing", onPopupShowing);
770 this.initDevTools("SystemMenu");
772 menu.addEventListener("popupshowing", onPopupShowing);
776 * Check if the user is a DevTools user by looking at our selfxss pref.
777 * This preference is incremented everytime the console is used (up to 5).
779 * @return {Boolean} true if the user can be considered as a devtools user.
782 const selfXssCount = Services.prefs.getIntPref("devtools.selfxss.count", 0);
783 return selfXssCount > 0;
786 hookKeyShortcuts(window) {
787 const doc = window.document;
789 // hookKeyShortcuts can be called both from hookWindow and from the developer toggle
790 // onBeforeCreated. Make sure shortcuts are only added once per window.
791 if (doc.getElementById("devtoolsKeyset")) {
795 const keyset = doc.createXULElement("keyset");
796 keyset.setAttribute("id", "devtoolsKeyset");
798 this.attachKeys(doc, lazy.KeyShortcuts, keyset);
800 // Appending a <key> element is not always enough. The <keyset> needs
801 // to be detached and reattached to make sure the <key> is taken into
802 // account (see bug 832984).
803 const mainKeyset = doc.getElementById("mainKeyset");
804 mainKeyset.parentNode.insertBefore(keyset, mainKeyset);
808 * This method attaches on the key elements to the devtools keyset.
810 attachKeys(doc, keyShortcuts, keyset = doc.getElementById("devtoolsKeyset")) {
811 const window = doc.defaultView;
812 for (const key of keyShortcuts) {
814 // Shortcuts might be missing when a user relies on a language packs
815 // which is missing a recently uplifted shortcut. Language packs are
816 // typically updated a few days after a code uplift.
819 const xulKey = this.createKey(doc, key, () => this.onKey(window, key));
820 keyset.appendChild(xulKey);
825 * This method removes keys from the devtools keyset.
827 removeKeys(doc, keyShortcuts) {
828 for (const key of keyShortcuts) {
829 const keyElement = doc.getElementById(this.getKeyElementId(key));
837 * We only want to have the keyboard shortcuts active when the menu button is on.
838 * This function either adds or removes the elements.
839 * @param {boolean} isEnabled
841 toggleProfilerKeyShortcuts(isEnabled) {
842 const profilerKeyShortcuts = getProfilerKeyShortcuts();
843 for (const { document } of Services.wm.getEnumerator(null)) {
844 const devtoolsKeyset = document.getElementById("devtoolsKeyset");
845 const mainKeyset = document.getElementById("mainKeyset");
847 if (!devtoolsKeyset || !mainKeyset) {
848 // There may not be devtools keyset on this window.
852 const areProfilerKeysPresent = !!document.getElementById(
853 "key_profilerStartStop"
855 if (isEnabled === areProfilerKeysPresent) {
856 // Don't double add or double remove the shortcuts.
860 this.attachKeys(document, profilerKeyShortcuts);
862 this.removeKeys(document, profilerKeyShortcuts);
864 // Appending a <key> element is not always enough. The <keyset> needs
865 // to be detached and reattached to make sure the <key> is taken into
866 // account (see bug 832984).
867 mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset);
871 async onKey(window, key) {
873 // The profiler doesn't care if DevTools is loaded, so provide a quick check
874 // first to bail out of checking if DevTools is available.
876 case "profilerStartStop":
877 case "profilerStartStopAlternate": {
878 lazy.ProfilerPopupBackground.toggleProfiler("aboutprofiling");
881 case "profilerCapture":
882 case "profilerCaptureAlternate": {
883 lazy.ProfilerPopupBackground.captureProfile("aboutprofiling");
888 // Ignore the following key shortcut if DevTools aren't yet opened.
889 // The key shortcut is registered in this core component in order to
890 // work even when the web page is focused.
891 if (key.id == "javascriptTracingToggle" && !this.initialized) {
895 // Record the timing at which this event started in order to compute later in
896 // gDevTools.showToolbox, the complete time it takes to open the toolbox.
897 // i.e. especially take `initDevTools` into account.
898 const startTime = Cu.now();
899 const require = this.initDevTools("KeyShortcut", key);
902 } = require("devtools/client/framework/devtools-browser");
903 await gDevToolsBrowser.onKeyShortcut(window, key, startTime);
905 console.error(`Exception while trigerring key ${key}: ${e}\n${e.stack}`);
909 getKeyElementId({ id, toolId }) {
910 return "key_" + (id || toolId);
913 // Create a <xul:key> DOM Element
914 createKey(doc, key, oncommand) {
915 const { shortcut, modifiers: mod } = key;
916 const k = doc.createXULElement("key");
917 k.id = this.getKeyElementId(key);
919 if (shortcut.startsWith("VK_")) {
920 k.setAttribute("keycode", shortcut);
921 if (shortcut.match(/^VK_\d$/)) {
922 // Add the event keydown attribute to ensure that shortcuts work for combinations
923 // such as ctrl shift 1.
924 k.setAttribute("event", "keydown");
927 k.setAttribute("key", shortcut);
931 k.setAttribute("modifiers", mod);
934 k.addEventListener("command", oncommand);
939 initDevTools(reason, key = "") {
940 // In the case of the --jsconsole and --jsdebugger command line parameters
941 // there is no browser window yet so we don't send any telemetry yet.
942 if (reason !== "CommandLine") {
943 this.sendEntryPointTelemetry(reason, key);
946 this.initialized = true;
947 const { require } = ChromeUtils.importESModule(
948 "resource://devtools/shared/loader/Loader.sys.mjs"
950 // Ensure loading main devtools module that hooks up into browser UI
951 // and initialize all devtools machinery.
952 // eslint-disable-next-line import/no-unassigned-import
953 require("devtools/client/framework/devtools-browser");
957 handleConsoleFlag(cmdLine) {
958 const window = Services.wm.getMostRecentWindow("devtools:webconsole");
960 const require = this.initDevTools("CommandLine");
962 BrowserConsoleManager,
963 } = require("devtools/client/webconsole/browser-console-manager");
964 BrowserConsoleManager.toggleBrowserConsole().catch(console.error);
966 // the Browser Console was already open
970 if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
971 cmdLine.preventDefault = true;
975 // Open the toolbox on the selected tab once the browser starts up.
976 async handleDevToolsFlag(window) {
977 const require = this.initDevTools("CommandLine");
978 const { gDevTools } = require("devtools/client/framework/devtools");
979 await gDevTools.showToolboxForTab(window.gBrowser.selectedTab);
982 _isRemoteDebuggingEnabled() {
983 let remoteDebuggingEnabled = false;
985 remoteDebuggingEnabled = kDebuggerPrefs.every(pref => {
986 return Services.prefs.getBoolPref(pref);
992 if (!remoteDebuggingEnabled) {
994 "Could not run chrome debugger! You need the following " +
995 "prefs to be set to true: " +
996 kDebuggerPrefs.join(", ");
997 console.error(new Error(errorMsg));
998 // Dump as well, as we're doing this from a commandline, make sure people
1000 dump(errorMsg + "\n");
1002 return remoteDebuggingEnabled;
1005 handleDebuggerFlag(cmdLine, binaryPath) {
1006 if (!this._isRemoteDebuggingEnabled()) {
1010 let devtoolsThreadResumed = false;
1011 const pauseOnStartup = cmdLine.handleFlag("wait-for-jsdebugger", false);
1012 if (pauseOnStartup) {
1013 const observe = function (subject, topic, data) {
1014 devtoolsThreadResumed = true;
1015 Services.obs.removeObserver(observe, "devtools-thread-ready");
1017 Services.obs.addObserver(observe, "devtools-thread-ready");
1020 const { BrowserToolboxLauncher } = ChromeUtils.importESModule(
1021 "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs"
1023 // --jsdebugger $binaryPath is an helper alias to set MOZ_BROWSER_TOOLBOX_BINARY=$binaryPath
1024 // See comment within BrowserToolboxLauncher.
1025 // Setting it as an environment variable helps it being reused if we restart the browser via CmdOrCtrl+R
1026 Services.env.set("MOZ_BROWSER_TOOLBOX_BINARY", binaryPath);
1028 const browserToolboxLauncherConfig = {};
1030 // If user passed the --jsdebugger in mochitests, we want to enable the
1031 // multiprocess Browser Toolbox (by default it's parent process only)
1032 if (Services.prefs.getBoolPref("devtools.testing", false)) {
1033 browserToolboxLauncherConfig.forceMultiprocess = true;
1035 BrowserToolboxLauncher.init(browserToolboxLauncherConfig);
1037 if (pauseOnStartup) {
1038 // Spin the event loop until the debugger connects.
1039 const tm = Cc["@mozilla.org/thread-manager;1"].getService();
1040 tm.spinEventLoopUntil("DevToolsStartup.jsm:handleDebuggerFlag", () => {
1041 return devtoolsThreadResumed;
1045 if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
1046 cmdLine.preventDefault = true;
1051 * Handle the --start-debugger-server command line flag. The options are:
1052 * --start-debugger-server
1053 * The portOrPath parameter is boolean true in this case. Reads and uses the defaults
1054 * from devtools.debugger.remote-port and devtools.debugger.remote-websocket prefs.
1055 * The default values of these prefs are port 6000, WebSocket disabled.
1057 * --start-debugger-server 6789
1058 * Start the non-WebSocket server on port 6789.
1060 * --start-debugger-server /path/to/filename
1061 * Start the server on a Unix domain socket.
1063 * --start-debugger-server ws:6789
1064 * Start the WebSocket server on port 6789.
1066 * --start-debugger-server ws:
1067 * Start the WebSocket server on the default port (taken from d.d.remote-port)
1069 handleDevToolsServerFlag(cmdLine, portOrPath) {
1070 if (!this._isRemoteDebuggingEnabled()) {
1074 let webSocket = false;
1075 const defaultPort = Services.prefs.getIntPref(
1076 "devtools.debugger.remote-port"
1078 if (portOrPath === true) {
1079 // Default to pref values if no values given on command line
1080 webSocket = Services.prefs.getBoolPref(
1081 "devtools.debugger.remote-websocket"
1083 portOrPath = defaultPort;
1084 } else if (portOrPath.startsWith("ws:")) {
1086 const port = portOrPath.slice(3);
1087 portOrPath = Number(port) ? port : defaultPort;
1091 useDistinctSystemPrincipalLoader,
1092 releaseDistinctSystemPrincipalLoader,
1093 } = ChromeUtils.importESModule(
1094 "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
1098 // Create a separate loader instance, so that we can be sure to receive
1099 // a separate instance of the DebuggingServer from the rest of the
1100 // devtools. This allows us to safely use the tools against even the
1101 // actors and DebuggingServer itself, especially since we can mark
1102 // serverLoader as invisible to the debugger (unlike the usual loader
1104 const serverLoader = useDistinctSystemPrincipalLoader(this);
1105 const { DevToolsServer: devToolsServer } = serverLoader.require(
1106 "resource://devtools/server/devtools-server.js"
1108 const { SocketListener } = serverLoader.require(
1109 "resource://devtools/shared/security/socket.js"
1111 devToolsServer.init();
1113 // Force the server to be kept running when the last connection closes.
1114 // So that another client can connect after the previous one is disconnected.
1115 devToolsServer.keepAlive = true;
1117 devToolsServer.registerAllActors();
1118 devToolsServer.allowChromeProcess = true;
1119 const socketOptions = { portOrPath, webSocket };
1121 const listener = new SocketListener(devToolsServer, socketOptions);
1123 dump("Started devtools server on " + portOrPath + "\n");
1125 // Prevent leaks on shutdown.
1126 const close = () => {
1127 Services.obs.removeObserver(close, "quit-application");
1128 dump("Stopped devtools server on " + portOrPath + "\n");
1132 if (devToolsServer) {
1133 devToolsServer.destroy();
1135 releaseDistinctSystemPrincipalLoader(this);
1137 Services.obs.addObserver(close, "quit-application");
1139 dump("Unable to start devtools server on " + portOrPath + ": " + e);
1142 if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
1143 cmdLine.preventDefault = true;
1148 * Send entry point telemetry explaining how the devtools were launched. This
1149 * functionality also lives inside `devtools/client/framework/browser-menus.js`
1150 * because this codepath is only used the first time a toolbox is opened for a
1153 * @param {String} reason
1154 * One of "KeyShortcut", "SystemMenu", "HamburgerMenu", "ContextMenu",
1156 * @param {String} key
1157 * The key used by a key shortcut.
1159 sendEntryPointTelemetry(reason, key = "") {
1166 if (reason === "KeyShortcut") {
1167 let { modifiers, shortcut } = key;
1169 modifiers = modifiers.replace(",", "+");
1171 if (shortcut.startsWith("VK_")) {
1172 shortcut = shortcut.substr(3);
1175 keys = `${modifiers}+${shortcut}`;
1178 const window = Services.wm.getMostRecentWindow("navigator:browser");
1180 this.telemetry.addEventProperty(
1188 this.telemetry.addEventProperty(
1197 if (this.recorded) {
1201 // Only save the first call for each firefox run as next call
1202 // won't necessarely start the tool. For example key shortcuts may
1203 // only change the currently selected tool.
1205 this.telemetry.getHistogramById("DEVTOOLS_ENTRY_POINT").add(reason);
1207 dump("DevTools telemetry entry point failed: " + e + "\n");
1209 this.recorded = true;
1213 * Hook the debugger tool to the "Debug Script" button of the slow script dialog.
1215 setSlowScriptDebugHandler() {
1216 const debugService = Cc["@mozilla.org/dom/slow-script-debug;1"].getService(
1217 Ci.nsISlowScriptDebug
1220 debugService.activationHandler = window => {
1221 const chromeWindow = window.browsingContext.topChromeWindow;
1223 let setupFinished = false;
1224 this.slowScriptDebugHandler(chromeWindow.gBrowser.selectedTab).then(
1226 setupFinished = true;
1230 // Don't return from the interrupt handler until the debugger is brought
1231 // up; no reason to continue executing the slow script.
1232 const utils = window.windowUtils;
1233 utils.enterModalState();
1234 Services.tm.spinEventLoopUntil(
1235 "devtools-browser.js:debugService.activationHandler",
1237 return setupFinished;
1240 utils.leaveModalState();
1243 debugService.remoteActivationHandler = async (browser, callback) => {
1245 // Force selecting the freezing tab
1246 const chromeWindow = browser.ownerGlobal;
1247 const tab = chromeWindow.gBrowser.getTabForBrowser(browser);
1248 chromeWindow.gBrowser.selectedTab = tab;
1250 await this.slowScriptDebugHandler(tab);
1254 callback.finishDebuggerStartup();
1259 * Called by setSlowScriptDebugHandler, when a tab freeze because of a slow running script
1261 async slowScriptDebugHandler(tab) {
1262 const require = this.initDevTools("SlowScript");
1263 const { gDevTools } = require("devtools/client/framework/devtools");
1264 const toolbox = await gDevTools.showToolboxForTab(tab, {
1265 toolId: "jsdebugger",
1267 const threadFront = toolbox.threadFront;
1269 // Break in place, which means resuming the debuggee thread and pausing
1270 // right before the next step happens.
1271 switch (threadFront.state) {
1273 // When the debugger is already paused.
1274 threadFront.resumeThenPause();
1277 // When the debugger is already open.
1278 const onPaused = threadFront.once("paused");
1279 threadFront.interrupt();
1281 threadFront.resumeThenPause();
1284 // The debugger is newly opened.
1285 const onResumed = threadFront.once("resumed");
1286 await threadFront.interrupt();
1288 threadFront.resumeThenPause();
1292 "invalid thread front state in slow script debug handler: " +
1298 // Used by tests and the toolbox to register the same key shortcuts in toolboxes loaded
1299 // in a window window.
1300 get KeyShortcuts() {
1301 return lazy.KeyShortcuts;
1303 get wrappedJSObject() {
1307 get jsdebuggerHelpInfo() {
1308 return ` --jsdebugger [<path>] Open the Browser Toolbox. Defaults to the local build
1309 but can be overridden by a firefox path.
1310 --wait-for-jsdebugger Spin event loop until JS debugger connects.
1311 Enables debugging (some) application startup code paths.
1312 Only has an effect when \`--jsdebugger\` is also supplied.
1313 --start-debugger-server [ws:][ <port> | <path> ] Start the devtools server on
1314 a TCP port or Unix domain socket path. Defaults to TCP port
1315 6000. Use WebSocket protocol if ws: prefix is specified.
1320 return ` --jsconsole Open the Browser Console.
1321 --devtools Open DevTools on initial load.
1322 ${this.jsdebuggerHelpInfo}`;
1325 classID: Components.ID("{9e9a9283-0ce9-4e4a-8f1c-ba129a032c32}"),
1326 QueryInterface: ChromeUtils.generateQI(["nsICommandLineHandler"]),
1330 * Singleton object that represents the JSON View in-content tool.
1331 * It has the same lifetime as the browser.
1337 // Prevent loading the frame script multiple times if we call this more than once.
1338 if (this.initialized) {
1341 this.initialized = true;
1343 // Register for messages coming from the child process.
1344 // This is never removed as there is no particular need to unregister
1345 // it during shutdown.
1346 Services.mm.addMessageListener("devtools:jsonview:save", this.onSave);
1349 // Message handlers for events from child processes
1352 * Save JSON to a file needs to be implemented here
1353 * in the parent process.
1356 const browser = message.target;
1357 const chrome = browser.ownerGlobal;
1358 if (message.data === null) {
1359 // Save original contents
1360 chrome.saveBrowser(browser);
1363 !message.data.startsWith("blob:resource://devtools/") ||
1364 browser.contentPrincipal.origin != "resource://devtools"
1366 console.error("Got invalid request to save JSON data");
1369 // The following code emulates saveBrowser, but:
1370 // - Uses the given blob URL containing the custom contents to save.
1371 // - Obtains the file name from the URL of the document, not the blob.
1372 // - avoids passing the document and explicitly passes system principal.
1373 // We have a blob created by a null principal to save, and the null
1374 // principal is from the child. Null principals don't survive crossing
1375 // over IPC, so there's no other principal that'll work.
1376 const persistable = browser.frameLoader;
1377 persistable.startPersistence(null, {
1378 onDocumentReady(doc) {
1379 const uri = chrome.makeURI(doc.documentURI, doc.characterSet);
1380 const filename = chrome.getDefaultFileName(undefined, uri, doc, null);
1381 chrome.internalSave(
1383 null /* originalURL */,
1388 false /* bypass cache */,
1389 null /* filepicker title key */,
1390 null /* file chosen */,
1391 null /* referrer */,
1392 doc.cookieJarSettings,
1393 null /* initiating document */,
1394 false /* don't skip prompt for a location */,
1395 null /* cache key */,
1396 lazy.PrivateBrowsingUtils.isBrowserPrivate(
1398 ) /* private browsing ? */,
1399 Services.scriptSecurityManager.getSystemPrincipal()
1403 throw new Error("JSON Viewer's onSave failed in startPersistence");