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