Bug 1885580 - Add a MenuGroup component for the menu redesign r=android-reviewers,007
[gecko.git] / devtools / startup / DevToolsStartup.sys.mjs
blobe9e24af41e89bb9b6b30ee6b7041b883a73193fb
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   ProfilerMenuButton:
39     "resource://devtools/client/performance-new/popup/menu-button.sys.mjs",
40   WebChannel: "resource://gre/modules/WebChannel.sys.mjs",
41 });
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"
48   );
49   // eslint-disable-next-line no-shadow
50   const Telemetry = require("devtools/client/shared/telemetry");
52   return Telemetry;
53 });
55 ChromeUtils.defineLazyGetter(lazy, "KeyShortcutsBundle", function () {
56   return new Localization(["devtools/startup/key-shortcuts.ftl"], true);
57 });
59 /**
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.
63  *
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.
68  *
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.
71  */
72 function getLocalizedKeyShortcut(id) {
73   try {
74     return lazy.KeyShortcutsBundle.formatValueSync(id);
75   } catch (e) {
76     console.error("Failed to retrieve DevTools localized shortcut for id", id);
77     return null;
78   }
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
89   const shortcuts = [
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.
95     {
96       id: "toggleToolbox",
97       shortcut: getLocalizedKeyShortcut("devtools-commandkey-toggle-toolbox"),
98       modifiers,
99     },
100     // All locales are using F12
101     {
102       id: "toggleToolboxF12",
103       shortcut: getLocalizedKeyShortcut(
104         "devtools-commandkey-toggle-toolbox-f12"
105       ),
106       modifiers: "", // F12 is the only one without modifiers
107     },
108     // Open the Browser Toolbox
109     {
110       id: "browserToolbox",
111       shortcut: getLocalizedKeyShortcut("devtools-commandkey-browser-toolbox"),
112       modifiers: "accel,alt,shift",
113     },
114     // Open the Browser Console
115     {
116       id: "browserConsole",
117       shortcut: getLocalizedKeyShortcut("devtools-commandkey-browser-console"),
118       modifiers: "accel,shift",
119     },
120     // Toggle the Responsive Design Mode
121     {
122       id: "responsiveDesignMode",
123       shortcut: getLocalizedKeyShortcut(
124         "devtools-commandkey-responsive-design-mode"
125       ),
126       modifiers,
127     },
128     // The following keys are also registered in /client/definitions.js
129     // and should be synced.
131     // Key for opening the Inspector
132     {
133       toolId: "inspector",
134       shortcut: getLocalizedKeyShortcut("devtools-commandkey-inspector"),
135       modifiers,
136     },
137     // Key for opening the Web Console
138     {
139       toolId: "webconsole",
140       shortcut: getLocalizedKeyShortcut("devtools-commandkey-webconsole"),
141       modifiers,
142     },
143     // Key for opening the Debugger
144     {
145       toolId: "jsdebugger",
146       shortcut: getLocalizedKeyShortcut("devtools-commandkey-jsdebugger"),
147       modifiers,
148     },
149     // Key for opening the Network Monitor
150     {
151       toolId: "netmonitor",
152       shortcut: getLocalizedKeyShortcut("devtools-commandkey-netmonitor"),
153       modifiers,
154     },
155     // Key for opening the Style Editor
156     {
157       toolId: "styleeditor",
158       shortcut: getLocalizedKeyShortcut("devtools-commandkey-styleeditor"),
159       modifiers: "shift",
160     },
161     // Key for opening the Performance Panel
162     {
163       toolId: "performance",
164       shortcut: getLocalizedKeyShortcut("devtools-commandkey-performance"),
165       modifiers: "shift",
166     },
167     // Key for opening the Storage Panel
168     {
169       toolId: "storage",
170       shortcut: getLocalizedKeyShortcut("devtools-commandkey-storage"),
171       modifiers: "shift",
172     },
173     // Key for opening the DOM Panel
174     {
175       toolId: "dom",
176       shortcut: getLocalizedKeyShortcut("devtools-commandkey-dom"),
177       modifiers,
178     },
179     // Key for opening the Accessibility Panel
180     {
181       toolId: "accessibility",
182       shortcut: getLocalizedKeyShortcut(
183         "devtools-commandkey-accessibility-f12"
184       ),
185       modifiers: "shift",
186     },
187   ];
189   if (isMac) {
190     // Add the extra key command for macOS, so you can open the inspector with cmd+shift+C
191     // like on Chrome DevTools.
192     shortcuts.push({
193       id: "inspectorMac",
194       toolId: "inspector",
195       shortcut: getLocalizedKeyShortcut("devtools-commandkey-inspector"),
196       modifiers: "accel,shift",
197     });
198   }
200   if (lazy.ProfilerMenuButton.isInNavbar()) {
201     shortcuts.push(...getProfilerKeyShortcuts());
202   }
204   // Allow toggling the JavaScript tracing not only from DevTools UI,
205   // but also from the web page when it is focused.
206   if (
207     Services.prefs.getBoolPref(
208       "devtools.debugger.features.javascript-tracing",
209       false
210     )
211   ) {
212     shortcuts.push({
213       id: "javascriptTracingToggle",
214       shortcut: getLocalizedKeyShortcut(
215         "devtools-commandkey-javascript-tracing-toggle"
216       ),
217       modifiers: "control,shift",
218     });
219   }
221   return shortcuts;
224 function getProfilerKeyShortcuts() {
225   return [
226     // Start/stop the profiler
227     {
228       id: "profilerStartStop",
229       shortcut: getLocalizedKeyShortcut(
230         "devtools-commandkey-profiler-start-stop"
231       ),
232       modifiers: "control,shift",
233     },
234     // Capture a profile
235     {
236       id: "profilerCapture",
237       shortcut: getLocalizedKeyShortcut("devtools-commandkey-profiler-capture"),
238       modifiers: "control,shift",
239     },
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
242     {
243       id: "profilerStartStopAlternate",
244       shortcut: getLocalizedKeyShortcut(
245         "devtools-commandkey-profiler-start-stop"
246       ),
247       modifiers: "control,shift,alt",
248     },
249     {
250       id: "profilerCaptureAlternate",
251       shortcut: getLocalizedKeyShortcut("devtools-commandkey-profiler-capture"),
252       modifiers: "control,shift,alt",
253     },
254   ];
258  * Validate the URL that will be used for the WebChannel for the profiler.
260  * @param {string} targetUrl
261  * @returns {string}
262  */
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.
269     if (
270       // Allow a test URL.
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(
284         targetUrl
285       )
286     ) {
287       // This URL is one of the allowed ones to be used for configuration.
288       return targetUrl;
289     }
291     console.error(
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.",
296       targetUrl
297     );
298   }
300   return frontEndUrl;
303 ChromeUtils.defineLazyGetter(lazy, "ProfilerPopupBackground", function () {
304   return ChromeUtils.importESModule(
305     "resource://devtools/client/performance-new/shared/background.sys.mjs"
306   );
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 = {
317   /**
318    * Boolean flag to check if DevTools have been already initialized or not.
319    * By initialized, we mean that its main modules are loaded.
320    */
321   initialized: false,
323   /**
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.
327    */
328   recorded: false,
330   get telemetry() {
331     if (!this._telemetry) {
332       this._telemetry = new lazy.Telemetry();
333       this._telemetry.setEventRecordingEnabled(true);
334     }
335     return this._telemetry;
336   },
338   /**
339    * Flag that indicates if the developer toggle was already added to customizableUI.
340    */
341   developerToggleCreated: false,
343   /**
344    * Flag that indicates if the profiler recording popup was already added to
345    * customizableUI.
346    */
347   profilerRecordingButtonCreated: false,
349   isDisabledByPolicy() {
350     return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false);
351   },
353   handle(cmdLine) {
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(
368         this.onWindowReady,
369         "browser-delayed-startup-finished"
370       );
372       // Add DevTools menu items to the "More Tools" view.
373       Services.obs.addObserver(
374         this.onMoreToolsViewShowing,
375         "web-developer-tools-view-showing"
376       );
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();
384         }
386         this.hookProfilerRecordingButton();
387       }
388     }
390     if (flags.console) {
391       this.commandLine = true;
392       this.handleConsoleFlag(cmdLine);
393     }
394     if (flags.debugger) {
395       this.commandLine = true;
396       const binaryPath =
397         typeof flags.debugger == "string" ? flags.debugger : null;
398       this.handleDebuggerFlag(cmdLine, binaryPath);
399     }
401     if (flags.devToolsServer) {
402       this.handleDevToolsServerFlag(cmdLine, flags.devToolsServer);
403     }
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);
409     }
410   },
412   /**
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
417    * in the debugger.
418    *
419    * @param {nsICommandLine} cmdLine
420    */
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) {
425       return;
426     }
428     // The following code would only work if we have a top level browser window opened
429     const window = Services.wm.getMostRecentWindow("navigator:browser");
430     if (!window) {
431       return;
432     }
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://`
440     if (
441       (urlParam.startsWith("http://") || urlParam.startsWith("https://")) &&
442       urlParam.lastIndexOf("/") <= 7
443     ) {
444       return;
445     }
447     let match = urlParam.match(/^(?<url>\w+:.+):(?<line>\d+):(?<column>\d+)$/);
448     if (!match) {
449       // fallback on only having the line when there is no column
450       match = urlParam.match(/^(?<url>\w+:.+):(?<line>\d+)?$/);
451       if (!match) {
452         return;
453       }
454     }
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
465     try {
466       Services.io.newURI(url);
467     } catch (e) {
468       return;
469     }
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
475     if (!toolbox) {
476       return;
477     }
479     // Avoid regular Firefox code from processing this argument,
480     // otherwise we would open the source in DevTools and in a new tab.
481     //
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;
489     }
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.
493     toolbox.win.focus();
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(
499       url,
500       parseInt(line, 10),
501       columnZeroBased,
502       null,
503       "CommandLine"
504     );
505   },
507   readCommandLineFlags(cmdLine) {
508     // All command line flags are disabled if DevTools are disabled by policy.
509     if (this.isDisabledByPolicy()) {
510       return {
511         console: false,
512         debugger: false,
513         devtools: false,
514         devToolsServer: false,
515       };
516     }
518     const console = cmdLine.handleFlag("jsconsole", false);
519     const devtools = cmdLine.handleFlag("devtools", false);
521     let devToolsServer;
522     try {
523       devToolsServer = cmdLine.handleFlagWithParam(
524         "start-debugger-server",
525         false
526       );
527     } catch (e) {
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);
531     }
533     let debuggerFlag;
534     try {
535       debuggerFlag = cmdLine.handleFlagWithParam("jsdebugger", false);
536     } catch (e) {
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);
540     }
542     return { console, debugger: debuggerFlag, devtools, devToolsServer };
543   },
545   /**
546    * Called when receiving the "browser-delayed-startup-finished" event for a new
547    * top-level window.
548    */
549   onWindowReady(window) {
550     if (
551       this.isDisabledByPolicy() ||
552       AppConstants.MOZ_APP_NAME == "thunderbird"
553     ) {
554       return;
555     }
557     this.hookWindow(window);
559     // This listener is called for all Firefox windows, but we want to execute some code
560     // only once.
561     if (!this._firstWindowReadyReceived) {
562       this.onFirstWindowReady(window);
563       this._firstWindowReadyReceived = true;
564     }
566     JsonView.initialize();
567   },
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");
578       }
579     }
580     this.setSlowScriptDebugHandler();
581   },
583   /**
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.
588    */
589   hookWindow(window) {
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
600     // initialized yet.
601     if (!this.initialized) {
602       this.hookBrowserToolsMenu(window);
603     }
604   },
606   /**
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!)
611    *
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.
615    *
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.
621    */
622   hookDeveloperToggle() {
623     if (this.developerToggleCreated) {
624       return;
625     }
627     const id = "developer-button";
628     const widget = lazy.CustomizableUI.getWidget(id);
629     if (widget && widget.provider == lazy.CustomizableUI.PROVIDER_API) {
630       return;
631     }
633     const panelviewId = "PanelUI-developer-tools";
634     const subviewId = "PanelUI-developer-tools-view";
636     const item = {
637       id,
638       type: "view",
639       viewId: panelviewId,
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);
646       },
647       onInit(anchor) {
648         // Since onBeforeCreated already bails out when initialized, we can call
649         // it right away.
650         this.onBeforeCreated(anchor.ownerDocument);
651       },
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);
657       },
658     };
659     lazy.CustomizableUI.createWidget(item);
660     lazy.CustomizableWidgets.push(item);
662     this.developerToggleCreated = true;
663   },
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);
679   },
681   onMoreToolsViewShowing(moreToolsView) {
682     this.addDevToolsItemsToSubview(moreToolsView);
683   },
685   /**
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.
689    */
690   hookProfilerRecordingButton() {
691     if (this.profilerRecordingButtonCreated) {
692       return;
693     }
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);
707     } else {
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);
713       };
714       Services.prefs.addObserver(featureFlagPref, enable);
715     }
716   },
718   /**
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.
722    */
723   initializeProfilerWebChannel() {
724     let channel;
726     // Register a channel for the URL in preferences. Also update the WebChannel if
727     // the URL changes.
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() {
738       if (channel) {
739         channel.stopListening();
740       }
742       const urlForWebChannel = Services.io.newURI(
743         validateProfilerWebChannelUrl(Services.prefs.getStringPref(urlPref))
744       );
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(
752           channel,
753           id,
754           message,
755           target
756         );
757       });
758     }
759   },
761   /*
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.
765    */
766   hookBrowserToolsMenu(window) {
767     const menu = window.document.getElementById("browserToolsMenu");
768     const onPopupShowing = () => {
769       menu.removeEventListener("popupshowing", onPopupShowing);
770       this.initDevTools("SystemMenu");
771     };
772     menu.addEventListener("popupshowing", onPopupShowing);
773   },
775   /**
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).
778    *
779    * @return {Boolean} true if the user can be considered as a devtools user.
780    */
781   isDevToolsUser() {
782     const selfXssCount = Services.prefs.getIntPref("devtools.selfxss.count", 0);
783     return selfXssCount > 0;
784   },
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")) {
792       return;
793     }
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);
805   },
807   /**
808    * This method attaches on the key elements to the devtools keyset.
809    */
810   attachKeys(doc, keyShortcuts, keyset = doc.getElementById("devtoolsKeyset")) {
811     const window = doc.defaultView;
812     for (const key of keyShortcuts) {
813       if (!key.shortcut) {
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.
817         continue;
818       }
819       const xulKey = this.createKey(doc, key, () => this.onKey(window, key));
820       keyset.appendChild(xulKey);
821     }
822   },
824   /**
825    * This method removes keys from the devtools keyset.
826    */
827   removeKeys(doc, keyShortcuts) {
828     for (const key of keyShortcuts) {
829       const keyElement = doc.getElementById(this.getKeyElementId(key));
830       if (keyElement) {
831         keyElement.remove();
832       }
833     }
834   },
836   /**
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
840    */
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.
849         continue;
850       }
852       const areProfilerKeysPresent = !!document.getElementById(
853         "key_profilerStartStop"
854       );
855       if (isEnabled === areProfilerKeysPresent) {
856         // Don't double add or double remove the shortcuts.
857         continue;
858       }
859       if (isEnabled) {
860         this.attachKeys(document, profilerKeyShortcuts);
861       } else {
862         this.removeKeys(document, profilerKeyShortcuts);
863       }
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);
868     }
869   },
871   async onKey(window, key) {
872     try {
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.
875       switch (key.id) {
876         case "profilerStartStop":
877         case "profilerStartStopAlternate": {
878           lazy.ProfilerPopupBackground.toggleProfiler("aboutprofiling");
879           return;
880         }
881         case "profilerCapture":
882         case "profilerCaptureAlternate": {
883           lazy.ProfilerPopupBackground.captureProfile("aboutprofiling");
884           return;
885         }
886       }
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) {
892         return;
893       }
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);
900       const {
901         gDevToolsBrowser,
902       } = require("devtools/client/framework/devtools-browser");
903       await gDevToolsBrowser.onKeyShortcut(window, key, startTime);
904     } catch (e) {
905       console.error(`Exception while trigerring key ${key}: ${e}\n${e.stack}`);
906     }
907   },
909   getKeyElementId({ id, toolId }) {
910     return "key_" + (id || toolId);
911   },
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");
925       }
926     } else {
927       k.setAttribute("key", shortcut);
928     }
930     if (mod) {
931       k.setAttribute("modifiers", mod);
932     }
934     k.addEventListener("command", oncommand);
936     return k;
937   },
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);
944     }
946     this.initialized = true;
947     const { require } = ChromeUtils.importESModule(
948       "resource://devtools/shared/loader/Loader.sys.mjs"
949     );
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");
954     return require;
955   },
957   handleConsoleFlag(cmdLine) {
958     const window = Services.wm.getMostRecentWindow("devtools:webconsole");
959     if (!window) {
960       const require = this.initDevTools("CommandLine");
961       const {
962         BrowserConsoleManager,
963       } = require("devtools/client/webconsole/browser-console-manager");
964       BrowserConsoleManager.toggleBrowserConsole().catch(console.error);
965     } else {
966       // the Browser Console was already open
967       window.focus();
968     }
970     if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
971       cmdLine.preventDefault = true;
972     }
973   },
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);
980   },
982   _isRemoteDebuggingEnabled() {
983     let remoteDebuggingEnabled = false;
984     try {
985       remoteDebuggingEnabled = kDebuggerPrefs.every(pref => {
986         return Services.prefs.getBoolPref(pref);
987       });
988     } catch (ex) {
989       console.error(ex);
990       return false;
991     }
992     if (!remoteDebuggingEnabled) {
993       const errorMsg =
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
999       // don't miss it:
1000       dump(errorMsg + "\n");
1001     }
1002     return remoteDebuggingEnabled;
1003   },
1005   handleDebuggerFlag(cmdLine, binaryPath) {
1006     if (!this._isRemoteDebuggingEnabled()) {
1007       return;
1008     }
1010     let devtoolsThreadResumed = false;
1011     const pauseOnStartup = cmdLine.handleFlag("wait-for-jsdebugger", false);
1012     if (pauseOnStartup) {
1013       const observe = function () {
1014         devtoolsThreadResumed = true;
1015         Services.obs.removeObserver(observe, "devtools-thread-ready");
1016       };
1017       Services.obs.addObserver(observe, "devtools-thread-ready");
1018     }
1020     const { BrowserToolboxLauncher } = ChromeUtils.importESModule(
1021       "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs"
1022     );
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;
1034     }
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;
1042       });
1043     }
1045     if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
1046       cmdLine.preventDefault = true;
1047     }
1048   },
1050   /**
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.
1056    *
1057    * --start-debugger-server 6789
1058    *   Start the non-WebSocket server on port 6789.
1059    *
1060    * --start-debugger-server /path/to/filename
1061    *   Start the server on a Unix domain socket.
1062    *
1063    * --start-debugger-server ws:6789
1064    *   Start the WebSocket server on port 6789.
1065    *
1066    * --start-debugger-server ws:
1067    *   Start the WebSocket server on the default port (taken from d.d.remote-port)
1068    */
1069   handleDevToolsServerFlag(cmdLine, portOrPath) {
1070     if (!this._isRemoteDebuggingEnabled()) {
1071       return;
1072     }
1074     let webSocket = false;
1075     const defaultPort = Services.prefs.getIntPref(
1076       "devtools.debugger.remote-port"
1077     );
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"
1082       );
1083       portOrPath = defaultPort;
1084     } else if (portOrPath.startsWith("ws:")) {
1085       webSocket = true;
1086       const port = portOrPath.slice(3);
1087       portOrPath = Number(port) ? port : defaultPort;
1088     }
1090     const {
1091       useDistinctSystemPrincipalLoader,
1092       releaseDistinctSystemPrincipalLoader,
1093     } = ChromeUtils.importESModule(
1094       "resource://devtools/shared/loader/DistinctSystemPrincipalLoader.sys.mjs"
1095     );
1097     try {
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
1103       // settings).
1104       const serverLoader = useDistinctSystemPrincipalLoader(this);
1105       const { DevToolsServer: devToolsServer } = serverLoader.require(
1106         "resource://devtools/server/devtools-server.js"
1107       );
1108       const { SocketListener } = serverLoader.require(
1109         "resource://devtools/shared/security/socket.js"
1110       );
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);
1122       listener.open();
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");
1129         if (listener) {
1130           listener.close();
1131         }
1132         if (devToolsServer) {
1133           devToolsServer.destroy();
1134         }
1135         releaseDistinctSystemPrincipalLoader(this);
1136       };
1137       Services.obs.addObserver(close, "quit-application");
1138     } catch (e) {
1139       dump("Unable to start devtools server on " + portOrPath + ": " + e);
1140     }
1142     if (cmdLine.state == Ci.nsICommandLine.STATE_REMOTE_AUTO) {
1143       cmdLine.preventDefault = true;
1144     }
1145   },
1147   /**
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
1151    * tab.
1152    *
1153    * @param {String} reason
1154    *        One of "KeyShortcut", "SystemMenu", "HamburgerMenu", "ContextMenu",
1155    *        "CommandLine".
1156    * @param {String} key
1157    *        The key used by a key shortcut.
1158    */
1159   sendEntryPointTelemetry(reason, key = "") {
1160     if (!reason) {
1161       return;
1162     }
1164     let keys = "";
1166     if (reason === "KeyShortcut") {
1167       let { modifiers, shortcut } = key;
1169       modifiers = modifiers.replace(",", "+");
1171       if (shortcut.startsWith("VK_")) {
1172         shortcut = shortcut.substr(3);
1173       }
1175       keys = `${modifiers}+${shortcut}`;
1176     }
1178     const window = Services.wm.getMostRecentWindow("navigator:browser");
1180     this.telemetry.addEventProperty(
1181       window,
1182       "open",
1183       "tools",
1184       null,
1185       "shortcut",
1186       keys
1187     );
1188     this.telemetry.addEventProperty(
1189       window,
1190       "open",
1191       "tools",
1192       null,
1193       "entrypoint",
1194       reason
1195     );
1197     if (this.recorded) {
1198       return;
1199     }
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.
1204     try {
1205       this.telemetry.getHistogramById("DEVTOOLS_ENTRY_POINT").add(reason);
1206     } catch (e) {
1207       dump("DevTools telemetry entry point failed: " + e + "\n");
1208     }
1209     this.recorded = true;
1210   },
1212   /**
1213    * Hook the debugger tool to the "Debug Script" button of the slow script dialog.
1214    */
1215   setSlowScriptDebugHandler() {
1216     const debugService = Cc["@mozilla.org/dom/slow-script-debug;1"].getService(
1217       Ci.nsISlowScriptDebug
1218     );
1220     debugService.activationHandler = window => {
1221       const chromeWindow = window.browsingContext.topChromeWindow;
1223       let setupFinished = false;
1224       this.slowScriptDebugHandler(chromeWindow.gBrowser.selectedTab).then(
1225         () => {
1226           setupFinished = true;
1227         }
1228       );
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",
1236         () => {
1237           return setupFinished;
1238         }
1239       );
1240       utils.leaveModalState();
1241     };
1243     debugService.remoteActivationHandler = async (browser, callback) => {
1244       try {
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);
1251       } catch (e) {
1252         console.error(e);
1253       }
1254       callback.finishDebuggerStartup();
1255     };
1256   },
1258   /**
1259    * Called by setSlowScriptDebugHandler, when a tab freeze because of a slow running script
1260    */
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",
1266     });
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) {
1272       case "paused":
1273         // When the debugger is already paused.
1274         threadFront.resumeThenPause();
1275         break;
1276       case "attached":
1277         // When the debugger is already open.
1278         const onPaused = threadFront.once("paused");
1279         threadFront.interrupt();
1280         await onPaused;
1281         threadFront.resumeThenPause();
1282         break;
1283       case "resuming":
1284         // The debugger is newly opened.
1285         const onResumed = threadFront.once("resumed");
1286         await threadFront.interrupt();
1287         await onResumed;
1288         threadFront.resumeThenPause();
1289         break;
1290       default:
1291         throw Error(
1292           "invalid thread front state in slow script debug handler: " +
1293             threadFront.state
1294         );
1295     }
1296   },
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;
1302   },
1303   get wrappedJSObject() {
1304     return this;
1305   },
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.
1317   },
1319   get helpInfo() {
1320     return `  --jsconsole        Open the Browser Console.
1321   --devtools         Open DevTools on initial load.
1322 ${this.jsdebuggerHelpInfo}`;
1323   },
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.
1332  */
1333 const JsonView = {
1334   initialized: false,
1336   initialize() {
1337     // Prevent loading the frame script multiple times if we call this more than once.
1338     if (this.initialized) {
1339       return;
1340     }
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);
1347   },
1349   // Message handlers for events from child processes
1351   /**
1352    * Save JSON to a file needs to be implemented here
1353    * in the parent process.
1354    */
1355   onSave(message) {
1356     const browser = message.target;
1357     const chrome = browser.ownerGlobal;
1358     if (message.data === null) {
1359       // Save original contents
1360       chrome.saveBrowser(browser);
1361     } else {
1362       if (
1363         !message.data.startsWith("blob:resource://devtools/") ||
1364         browser.contentPrincipal.origin != "resource://devtools"
1365       ) {
1366         console.error("Got invalid request to save JSON data");
1367         return;
1368       }
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(
1382             message.data,
1383             null /* originalURL */,
1384             null,
1385             filename,
1386             null,
1387             doc.contentType,
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(
1397               browser
1398             ) /* private browsing ? */,
1399             Services.scriptSecurityManager.getSystemPrincipal()
1400           );
1401         },
1402         onError() {
1403           throw new Error("JSON Viewer's onSave failed in startPersistence");
1404         },
1405       });
1406     }
1407   },