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