devtools.js (31629B)
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 "use strict"; 6 7 const { DevToolsShim } = ChromeUtils.importESModule( 8 "chrome://devtools-startup/content/DevToolsShim.sys.mjs" 9 ); 10 11 const { DEFAULT_SANDBOX_NAME } = ChromeUtils.importESModule( 12 "resource://devtools/shared/loader/Loader.sys.mjs" 13 ); 14 15 const lazy = {}; 16 ChromeUtils.defineESModuleGetters(lazy, { 17 BrowserToolboxLauncher: 18 "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs", 19 }); 20 21 loader.lazyRequireGetter( 22 this, 23 "LocalTabCommandsFactory", 24 "resource://devtools/client/framework/local-tab-commands-factory.js", 25 true 26 ); 27 loader.lazyRequireGetter( 28 this, 29 "CommandsFactory", 30 "resource://devtools/shared/commands/commands-factory.js", 31 true 32 ); 33 loader.lazyRequireGetter( 34 this, 35 "ToolboxHostManager", 36 "resource://devtools/client/framework/toolbox-host-manager.js", 37 true 38 ); 39 loader.lazyRequireGetter( 40 this, 41 "BrowserConsoleManager", 42 "resource://devtools/client/webconsole/browser-console-manager.js", 43 true 44 ); 45 loader.lazyRequireGetter( 46 this, 47 "Toolbox", 48 "resource://devtools/client/framework/toolbox.js", 49 true 50 ); 51 52 loader.lazyRequireGetter( 53 this, 54 "Telemetry", 55 "resource://devtools/client/shared/telemetry.js" 56 ); 57 58 const { 59 defaultTools: DefaultTools, 60 defaultThemes: DefaultThemes, 61 } = require("resource://devtools/client/definitions.js"); 62 const EventEmitter = require("resource://devtools/shared/event-emitter.js"); 63 const { 64 getTheme, 65 setTheme, 66 getAutoTheme, 67 addThemeObserver, 68 removeThemeObserver, 69 } = require("resource://devtools/client/shared/theme.js"); 70 71 const FORBIDDEN_IDS = new Set(["toolbox", ""]); 72 const MAX_ORDINAL = 99; 73 const POPUP_DEBUG_PREF = "devtools.popups.debug"; 74 const DEVTOOLS_ALWAYS_ON_TOP = "devtools.toolbox.alwaysOnTop"; 75 76 /** 77 * DevTools is a class that represents a set of developer tools, it holds a 78 * set of tools and keeps track of open toolboxes in the browser. 79 */ 80 class DevTools { 81 // We should be careful to always load a unique instance of this module: 82 // - only in the parent process 83 // - only in the "shared JSM global" spawn by mozJSModuleLoader 84 // The server codebase typically use another global named "DevTools global", 85 // which will load duplicated instances of all the modules -or- another 86 // DevTools module loader named "DevTools (Server Module loader)". 87 // Also the realm location is appended the loading callsite, so only check 88 // the beginning of the string. 89 constructor() { 90 if ( 91 Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT || 92 !Cu.getRealmLocation(globalThis).startsWith(DEFAULT_SANDBOX_NAME) 93 ) { 94 throw new Error( 95 "This module should be loaded in the parent process only, in the shared global." 96 ); 97 } 98 99 this._tools = new Map(); // Map<toolId, tool> 100 this._themes = new Map(); // Map<themeId, theme> 101 this._toolboxesPerCommands = new Map(); // Map<commands, toolbox> 102 // List of toolboxes that are still in process of creation 103 this._creatingToolboxes = new Map(); // Map<commands, toolbox Promise> 104 105 EventEmitter.decorate(this); 106 this._telemetry = new Telemetry(); 107 108 // List of all commands of debugged local Web Extension. 109 this._commandsPromiseByWebExtId = new Map(); // Map<extensionId, commands> 110 111 // Listen for changes to the theme pref. 112 this._onThemeChanged = this._onThemeChanged.bind(this); 113 addThemeObserver(this._onThemeChanged); 114 115 // This is important step in initialization codepath where we are going to 116 // start registering all default tools and themes: create menuitems, keys, emit 117 // related events. 118 this.registerDefaults(); 119 120 // Register this DevTools instance on the DevToolsShim, which is used by non-devtools 121 // code to interact with DevTools. 122 DevToolsShim.register(this); 123 } 124 125 // The windowtype of the main window, used in various tools. This may be set 126 // to something different by other gecko apps. 127 chromeWindowType = "navigator:browser"; 128 129 registerDefaults() { 130 // Ensure registering items in the sorted order (getDefault* functions 131 // return sorted lists) 132 this.getDefaultTools().forEach(definition => this.registerTool(definition)); 133 this.getDefaultThemes().forEach(definition => 134 this.registerTheme(definition) 135 ); 136 } 137 138 unregisterDefaults() { 139 for (const definition of this.getToolDefinitionArray()) { 140 this.unregisterTool(definition.id); 141 } 142 for (const definition of this.getThemeDefinitionArray()) { 143 this.unregisterTheme(definition.id); 144 } 145 } 146 147 /** 148 * Register a new developer tool. 149 * 150 * A definition is a light object that holds different information about a 151 * developer tool. This object is not supposed to have any operational code. 152 * See it as a "manifest". 153 * The only actual code lives in the build() function, which will be used to 154 * start an instance of this tool. 155 * 156 * Each toolDefinition has the following properties: 157 * - id: Unique identifier for this tool (string|required) 158 * - visibilityswitch: Property name to allow us to hide this tool from the 159 * DevTools Toolbox. 160 * A falsy value indicates that it cannot be hidden. 161 * - icon: URL pointing to a graphic which will be used as the src for an 162 * 16x16 img tag (string|required) 163 * - url: URL pointing to a XUL/XHTML document containing the user interface 164 * (string|required) 165 * - label: Localized name for the tool to be displayed to the user 166 * (string|required) 167 * - hideInOptions: Boolean indicating whether or not this tool should be 168 shown in toolbox options or not. Defaults to false. 169 * (boolean) 170 * - build: Function that takes an iframe, which has been populated with the 171 * markup from |url|, and also the toolbox containing the panel. 172 * And returns an instance of ToolPanel (function|required) 173 */ 174 registerTool(toolDefinition) { 175 const toolId = toolDefinition.id; 176 177 if (!toolId || FORBIDDEN_IDS.has(toolId)) { 178 throw new Error("Invalid definition.id"); 179 } 180 181 // Make sure that additional tools will always be able to be hidden. 182 // When being called from main.js, defaultTools has not yet been exported. 183 // But, we can assume that in this case, it is a default tool. 184 if (!DefaultTools.includes(toolDefinition)) { 185 toolDefinition.visibilityswitch = "devtools." + toolId + ".enabled"; 186 } 187 188 this._tools.set(toolId, toolDefinition); 189 190 this.emit("tool-registered", toolId); 191 } 192 193 /** 194 * Removes all tools that match the given |toolId| 195 * Needed so that add-ons can remove themselves when they are deactivated 196 * 197 * @param {string} toolId 198 * The id of the tool to unregister. 199 * @param {boolean} isQuitApplication 200 * true to indicate that the call is due to app quit, so we should not 201 * cause a cascade of costly events 202 */ 203 unregisterTool(toolId, isQuitApplication) { 204 this._tools.delete(toolId); 205 206 if (!isQuitApplication) { 207 this.emit("tool-unregistered", toolId); 208 } 209 } 210 211 /** 212 * Sorting function used for sorting tools based on their ordinals. 213 */ 214 ordinalSort(d1, d2) { 215 const o1 = typeof d1.ordinal == "number" ? d1.ordinal : MAX_ORDINAL; 216 const o2 = typeof d2.ordinal == "number" ? d2.ordinal : MAX_ORDINAL; 217 return o1 - o2; 218 } 219 220 getDefaultTools() { 221 return DefaultTools.sort(this.ordinalSort); 222 } 223 224 getAdditionalTools() { 225 const tools = []; 226 for (const [, value] of this._tools) { 227 if (!DefaultTools.includes(value)) { 228 tools.push(value); 229 } 230 } 231 return tools.sort(this.ordinalSort); 232 } 233 234 getDefaultThemes() { 235 return DefaultThemes.sort(this.ordinalSort); 236 } 237 238 /** 239 * Get a tool definition if it exists and is enabled. 240 * 241 * @param {string} toolId 242 * The id of the tool to show 243 * 244 * @return {ToolDefinition|null} tool 245 * The ToolDefinition for the id or null. 246 */ 247 getToolDefinition(toolId) { 248 const tool = this._tools.get(toolId); 249 if (!tool) { 250 return null; 251 } else if (!tool.visibilityswitch) { 252 return tool; 253 } 254 255 const enabled = Services.prefs.getBoolPref(tool.visibilityswitch, true); 256 257 return enabled ? tool : null; 258 } 259 260 /** 261 * Allow ToolBoxes to get at the list of tools that they should populate 262 * themselves with. 263 * 264 * @return {Map} tools 265 * A map of the the tool definitions registered in this instance 266 */ 267 getToolDefinitionMap() { 268 const tools = new Map(); 269 270 for (const [id, definition] of this._tools) { 271 if (this.getToolDefinition(id)) { 272 tools.set(id, definition); 273 } 274 } 275 276 return tools; 277 } 278 279 /** 280 * Tools have an inherent ordering that can't be represented in a Map so 281 * getToolDefinitionArray provides an alternative representation of the 282 * definitions sorted by ordinal value. 283 * 284 * @return {Array} tools 285 * A sorted array of the tool definitions registered in this instance 286 */ 287 getToolDefinitionArray() { 288 const definitions = []; 289 290 for (const [id, definition] of this._tools) { 291 if (this.getToolDefinition(id)) { 292 definitions.push(definition); 293 } 294 } 295 296 return definitions.sort(this.ordinalSort); 297 } 298 299 /** 300 * Returns the name of the current theme for devtools. 301 * 302 * @return {string} theme 303 * The name of the current devtools theme. 304 */ 305 getTheme() { 306 return getTheme(); 307 } 308 309 /** 310 * Returns the name of the default (auto) theme for devtools. 311 * 312 * @return {string} theme 313 */ 314 getAutoTheme() { 315 return getAutoTheme(); 316 } 317 318 /** 319 * Called when the developer tools theme changes. 320 */ 321 _onThemeChanged() { 322 this.emit("theme-changed", getTheme()); 323 } 324 325 /** 326 * Register a new theme for developer tools toolbox. 327 * 328 * A definition is a light object that holds various information about a 329 * theme. 330 * 331 * Each themeDefinition has the following properties: 332 * - id: Unique identifier for this theme (string|required) 333 * - label: Localized name for the theme to be displayed to the user 334 * (string|required) 335 * - stylesheets: Array of URLs pointing to a CSS document(s) containing 336 * the theme style rules (array|required) 337 * - classList: Array of class names identifying the theme within a document. 338 * These names are set to document element when applying 339 * the theme (array|required) 340 * - onApply: Function that is executed by the framework when the theme 341 * is applied. The function takes the current iframe window 342 * and the previous theme id as arguments (function) 343 * - onUnapply: Function that is executed by the framework when the theme 344 * is unapplied. The function takes the current iframe window 345 * and the new theme id as arguments (function) 346 */ 347 registerTheme(themeDefinition) { 348 const themeId = themeDefinition.id; 349 350 if (!themeId) { 351 throw new Error("Invalid theme id"); 352 } 353 354 if (this._themes.get(themeId)) { 355 throw new Error("Theme with the same id is already registered"); 356 } 357 358 this._themes.set(themeId, themeDefinition); 359 360 this.emit("theme-registered", themeId); 361 } 362 363 /** 364 * Removes an existing theme from the list of registered themes. 365 * Needed so that add-ons can remove themselves when they are deactivated 366 * 367 * @param {string|object} theme 368 * Definition or the id of the theme to unregister. 369 */ 370 unregisterTheme(theme) { 371 let themeId = null; 372 if (typeof theme == "string") { 373 themeId = theme; 374 theme = this._themes.get(theme); 375 } else { 376 themeId = theme.id; 377 } 378 379 const currTheme = getTheme(); 380 381 // Note that we can't check if `theme` is an item 382 // of `DefaultThemes` as we end up reloading definitions 383 // module and end up with different theme objects 384 const isCoreTheme = DefaultThemes.some(t => t.id === themeId); 385 386 // Reset the theme if an extension theme that's currently applied 387 // is being removed. 388 // Ignore shutdown since addons get disabled during that time. 389 if ( 390 !Services.startup.shuttingDown && 391 !isCoreTheme && 392 theme.id == currTheme 393 ) { 394 setTheme("auto"); 395 396 this.emit("theme-unregistered", theme); 397 } 398 399 this._themes.delete(themeId); 400 } 401 402 /** 403 * Get a theme definition if it exists. 404 * 405 * @param {string} themeId 406 * The id of the theme 407 * 408 * @return {ThemeDefinition|null} theme 409 * The ThemeDefinition for the id or null. 410 */ 411 getThemeDefinition(themeId) { 412 const theme = this._themes.get(themeId); 413 if (!theme) { 414 return null; 415 } 416 return theme; 417 } 418 419 /** 420 * Get map of registered themes. 421 * 422 * @return {Map} themes 423 * A map of the the theme definitions registered in this instance 424 */ 425 getThemeDefinitionMap() { 426 const themes = new Map(); 427 428 for (const [id, definition] of this._themes) { 429 if (this.getThemeDefinition(id)) { 430 themes.set(id, definition); 431 } 432 } 433 434 return themes; 435 } 436 437 /** 438 * Get registered themes definitions sorted by ordinal value. 439 * 440 * @return {Array} themes 441 * A sorted array of the theme definitions registered in this instance 442 */ 443 getThemeDefinitionArray() { 444 const definitions = []; 445 446 for (const [id, definition] of this._themes) { 447 if (this.getThemeDefinition(id)) { 448 definitions.push(definition); 449 } 450 } 451 452 return definitions.sort(this.ordinalSort); 453 } 454 455 /** 456 * Called from SessionStore.sys.mjs in mozilla-central when saving the current state. 457 * 458 * @param {object} state 459 * A SessionStore state object that gets modified by reference 460 */ 461 saveDevToolsSession(state) { 462 state.browserConsole = 463 BrowserConsoleManager.getBrowserConsoleSessionState(); 464 state.browserToolbox = 465 lazy.BrowserToolboxLauncher.getBrowserToolboxSessionState(); 466 } 467 468 /** 469 * Restore the devtools session state as provided by SessionStore. 470 */ 471 async restoreDevToolsSession({ browserConsole, browserToolbox }) { 472 if (browserToolbox) { 473 lazy.BrowserToolboxLauncher.init(); 474 } 475 476 if (browserConsole && !BrowserConsoleManager.getBrowserConsole()) { 477 await BrowserConsoleManager.toggleBrowserConsole(); 478 } 479 } 480 481 /** 482 * Boolean, true, if we never opened a toolbox. 483 * Used to implement the telemetry tracking toolbox opening. 484 */ 485 _firstShowToolbox = true; 486 487 /** 488 * Show a Toolbox for a given "commands" (either by creating a new one, or if a 489 * toolbox already exists for the commands, by bringing to the front the 490 * existing one). 491 * 492 * If a Toolbox already exists, we will still update it based on some of the 493 * provided parameters: 494 * - if |toolId| is provided then the toolbox will switch to the specified 495 * tool. 496 * - if |hostType| is provided then the toolbox will be switched to the 497 * specified HostType. 498 * 499 * @param {Commands Object} commands 500 * The commands object which designates which context the toolbox will debug 501 * @param {object} options 502 * @param {string} options.toolId 503 * The id of the tool to show 504 * @param {object} options.toolOptions 505 * Options that will be passed to the tool init function 506 * @param {Toolbox.HostType}options. hostType 507 * The type of host (bottom, window, left, right) 508 * @param {object} options.hostOptions 509 * Options for host specifically 510 * @param {number} options.startTime 511 * Indicates the time at which the user event related to 512 * this toolbox opening started. This is a `ChromeUtils.now()` timing. 513 * @param {string} options.reason 514 * Reason the tool was opened 515 * @param {boolean} options.raise 516 * Whether we need to raise the toolbox or not. 517 * 518 * @return {Toolbox} toolbox 519 * The toolbox that was opened 520 */ 521 async showToolbox( 522 commands, 523 { 524 toolId, 525 toolOptions, 526 hostType, 527 startTime, 528 raise = true, 529 reason = "toolbox_show", 530 hostOptions, 531 } = {} 532 ) { 533 let toolbox = this._toolboxesPerCommands.get(commands); 534 535 if (toolbox) { 536 if (hostType != null && toolbox.hostType != hostType) { 537 await toolbox.switchHost(hostType); 538 } 539 540 if (toolId != null) { 541 // selectTool will either select the tool if not currently selected, or wait for 542 // the tool to be loaded if needed. 543 await toolbox.selectTool(toolId, reason, toolOptions); 544 } 545 546 if (raise) { 547 await toolbox.raise(); 548 } 549 } else { 550 // Toolbox creation is async, we have to be careful about races. 551 // Check if we are already waiting for a Toolbox for the provided 552 // commands before creating a new one. 553 const promise = this._creatingToolboxes.get(commands); 554 if (promise) { 555 return promise; 556 } 557 const toolboxPromise = this._createToolbox(commands, { 558 toolId, 559 toolOptions, 560 hostType, 561 hostOptions, 562 }); 563 this._creatingToolboxes.set(commands, toolboxPromise); 564 toolbox = await toolboxPromise; 565 this._creatingToolboxes.delete(commands); 566 567 if (startTime) { 568 this.logToolboxOpenTime(toolbox, startTime); 569 } 570 this._firstShowToolbox = false; 571 } 572 573 // We send the "enter" width here to ensure it is always sent *after* 574 // the "open" event. 575 const width = Math.ceil(toolbox.win.outerWidth / 50) * 50; 576 const panelName = this.makeToolIdHumanReadable( 577 toolId || toolbox.defaultToolId 578 ); 579 this._telemetry.addEventProperty( 580 toolbox, 581 "enter", 582 panelName, 583 null, 584 "width", 585 width 586 ); 587 588 return toolbox; 589 } 590 591 /** 592 * Show the toolbox for a given tab. If a toolbox already exists for this tab 593 * the existing toolbox will be raised. Otherwise a new toolbox is created. 594 * 595 * Relies on `showToolbox`, see its jsDoc for additional information and 596 * arguments description. 597 * 598 * Also used by 3rd party tools (eg wptrunner) and exposed by 599 * DevToolsShim.sys.mjs. 600 * 601 * @param {XULTab} tab 602 * The tab the toolbox will debug 603 * @param {object} options 604 * Various options that will be forwarded to `showToolbox`. See the 605 * JSDoc on this method. 606 */ 607 async showToolboxForTab( 608 tab, 609 { 610 toolId, 611 toolOptions, 612 hostType, 613 startTime, 614 raise, 615 reason, 616 hostOptions, 617 } = {} 618 ) { 619 // Popups are debugged via the toolbox of their opener document/tab. 620 // So avoid opening dedicated toolbox for them. 621 if ( 622 tab.linkedBrowser.browsingContext.opener && 623 Services.prefs.getBoolPref(POPUP_DEBUG_PREF) 624 ) { 625 const openerTab = tab.ownerGlobal.gBrowser.getTabForBrowser( 626 tab.linkedBrowser.browsingContext.opener.embedderElement 627 ); 628 const openerCommands = 629 await LocalTabCommandsFactory.getCommandsForTab(openerTab); 630 if (this.getToolboxForCommands(openerCommands)) { 631 console.log( 632 "Can't open a toolbox for this document as this is debugged from its opener tab" 633 ); 634 return null; 635 } 636 } 637 const commands = await LocalTabCommandsFactory.createCommandsForTab(tab); 638 return this.showToolbox(commands, { 639 toolId, 640 toolOptions, 641 hostType, 642 startTime, 643 raise, 644 reason, 645 hostOptions, 646 }); 647 } 648 649 /** 650 * Open a Toolbox in a dedicated top-level window for debugging a local WebExtension. 651 * This will re-open a previously opened toolbox if we try to re-debug the same extension. 652 * 653 * Note that this will spawn a new DevToolsClient. 654 * 655 * @param {string} extensionId 656 * ID of the extension to debug. 657 * @param {object} (optional) 658 * - {String} toolId 659 * The id of the tool to show 660 */ 661 async showToolboxForWebExtension(extensionId, { toolId } = {}) { 662 // Ensure spawning only one commands instance per extension at a time by caching its commands. 663 // showToolbox will later reopen the previously opened toolbox if called with the same 664 // commands. 665 let commandsPromise = this._commandsPromiseByWebExtId.get(extensionId); 666 if (!commandsPromise) { 667 commandsPromise = CommandsFactory.forAddon(extensionId); 668 this._commandsPromiseByWebExtId.set(extensionId, commandsPromise); 669 } 670 const commands = await commandsPromise; 671 commands.client.once("closed").then(() => { 672 this._commandsPromiseByWebExtId.delete(extensionId); 673 }); 674 675 return this.showToolbox(commands, { 676 hostType: Toolbox.HostType.WINDOW, 677 hostOptions: { 678 // The toolbox is always displayed on top so that we can keep 679 // the DevTools visible while interacting with the Firefox window. 680 alwaysOnTop: Services.prefs.getBoolPref(DEVTOOLS_ALWAYS_ON_TOP, false), 681 }, 682 toolId, 683 }); 684 } 685 686 /** 687 * Log telemetry related to toolbox opening. 688 * Two distinct probes are logged. One for cold startup, when we open the very first 689 * toolbox. This one includes devtools framework loading. And a second one for all 690 * subsequent toolbox opening, which should all be faster. 691 * These two probes are indexed by Tool ID. 692 * 693 * @param {string} toolbox 694 * Toolbox instance. 695 * @param {number} startTime 696 * Indicates the time at which the user event related to the toolbox 697 * opening started. This is a `ChromeUtils.now()` timing. 698 */ 699 logToolboxOpenTime(toolbox, startTime) { 700 const toolId = toolbox.currentToolId || toolbox.defaultToolId; 701 const delay = ChromeUtils.now() - startTime; 702 const panelName = this.makeToolIdHumanReadable(toolId); 703 704 if (this._firstShowToolbox) { 705 Glean.devtools.coldToolboxOpenDelay[toolId].accumulateSingleSample(delay); 706 } else { 707 Glean.devtools.warmToolboxOpenDelay[toolId].accumulateSingleSample(delay); 708 } 709 const browserWin = toolbox.topWindow; 710 this._telemetry.addEventProperty( 711 browserWin, 712 "open", 713 "tools", 714 null, 715 "first_panel", 716 panelName 717 ); 718 } 719 720 makeToolIdHumanReadable(toolId) { 721 if (/^[0-9a-fA-F]{40}_temporary-addon/.test(toolId)) { 722 return "temporary-addon"; 723 } 724 725 let matches = toolId.match( 726 /^_([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})_/ 727 ); 728 if (matches && matches.length === 2) { 729 return matches[1]; 730 } 731 732 matches = toolId.match(/^_?(.*)-\d+-\d+-devtools-panel$/); 733 if (matches && matches.length === 2) { 734 return matches[1]; 735 } 736 737 return toolId; 738 } 739 740 /** 741 * Unconditionally create a new Toolbox instance for the provided commands. 742 * See `showToolbox` for the arguments' jsdoc. 743 */ 744 async _createToolbox( 745 commands, 746 { toolId, toolOptions, hostType, hostOptions } = {} 747 ) { 748 const manager = new ToolboxHostManager(commands, hostType, hostOptions); 749 750 const toolbox = await manager.create(toolId, toolOptions); 751 752 this._toolboxesPerCommands.set(commands, toolbox); 753 754 toolbox.once("destroy", () => { 755 this.emit("toolbox-destroy", toolbox); 756 }); 757 758 toolbox.once("destroyed", () => { 759 this._toolboxesPerCommands.delete(commands); 760 this.emit("toolbox-destroyed", toolbox); 761 }); 762 763 await toolbox.open(); 764 this.emit("toolbox-ready", toolbox); 765 766 return toolbox; 767 } 768 769 /** 770 * Return the toolbox for a given commands object. 771 * 772 * @param {Commands Object} commands 773 * Debugging context commands that owns this toolbox 774 * 775 * @return {Toolbox} toolbox 776 * The toolbox that is debugging the given context designated by the commands 777 */ 778 getToolboxForCommands(commands) { 779 return this._toolboxesPerCommands.get(commands); 780 } 781 782 /** 783 * TabDescriptorFront requires a synchronous method and don't have a reference to its 784 * related commands object. So expose something handcrafted just for this. 785 */ 786 getToolboxForDescriptorFront(descriptorFront) { 787 for (const [commands, toolbox] of this._toolboxesPerCommands) { 788 if (commands.descriptorFront == descriptorFront) { 789 return toolbox; 790 } 791 } 792 return null; 793 } 794 795 /** 796 * Retrieve an existing toolbox for the provided tab if it was created before. 797 * Returns null otherwise. 798 * 799 * @param {XULTab} tab 800 * The browser tab. 801 * @return {Toolbox} 802 * Returns tab's toolbox object. 803 */ 804 getToolboxForTab(tab) { 805 return this.getToolboxes().find( 806 t => t.commands.descriptorFront.localTab === tab 807 ); 808 } 809 810 /** 811 * Close the toolbox for a given tab. 812 * 813 * @return {Promise} Returns a promise that resolves either: 814 * - immediately if no Toolbox was found 815 * - or after toolbox.destroy() resolved if a Toolbox was found 816 */ 817 async closeToolboxForTab(tab) { 818 const commands = await LocalTabCommandsFactory.getCommandsForTab(tab); 819 820 let toolbox = await this._creatingToolboxes.get(commands); 821 if (!toolbox) { 822 toolbox = this._toolboxesPerCommands.get(commands); 823 } 824 if (!toolbox) { 825 return; 826 } 827 await toolbox.destroy(); 828 } 829 830 /** 831 * Compatibility layer for web-extensions. Used by DevToolsShim for 832 * browser/components/extensions/ext-devtools.js 833 * 834 * web-extensions need to use dedicated instances of Commands and cannot reuse the 835 * cached instances managed by DevTools. 836 * Note that is will end up being cached in WebExtension codebase, via 837 * DevToolsExtensionPageContextParent.getDevToolsCommands. 838 */ 839 createCommandsForTabForWebExtension(tab) { 840 return CommandsFactory.forTab(tab, { isWebExtension: true }); 841 } 842 843 /** 844 * Compatibility layer for web-extensions. Used by DevToolsShim for 845 * toolkit/components/extensions/ext-c-toolkit.js 846 */ 847 openBrowserConsole() { 848 const { 849 BrowserConsoleManager, 850 } = require("resource://devtools/client/webconsole/browser-console-manager.js"); 851 BrowserConsoleManager.openBrowserConsoleOrFocus(); 852 } 853 854 /** 855 * Called from the DevToolsShim, used by nsContextMenu.js. 856 * 857 * @param {XULTab} tab 858 * The browser tab on which inspect node was used. 859 * @param {ElementIdentifier} domReference 860 * Identifier generated by ContentDOMReference. It is a unique pair of 861 * BrowsingContext ID and a numeric ID. 862 * @param {number} startTime 863 * Optional, indicates the time at which the user event related to this node 864 * inspection started. This is a `ChromeUtils.now()` timing. 865 * @return {Promise} a promise that resolves when the node is selected in the inspector 866 * markup view. 867 */ 868 async inspectNode(tab, domReference, startTime) { 869 const toolboxWasOpened = !!gDevTools.getToolboxForTab(tab); 870 const toolbox = await gDevTools.showToolboxForTab(tab, { 871 toolId: "inspector", 872 toolOptions: { 873 defaultStartupNodeDomReference: domReference, 874 defaultStartupNodeSelectionReason: "browser-context-menu", 875 }, 876 startTime, 877 reason: "inspect_dom", 878 }); 879 880 // If the toolbox wasn't opened yet, the selection of the node will be handled by 881 // the defaultStartupNodeDomReference option, so we can stop here. 882 if (!toolboxWasOpened) { 883 return; 884 } 885 886 // But if the toolbox was already opened, we need to explicitely select the node. 887 const inspector = toolbox.getCurrentPanel(); 888 889 const nodeFront = 890 await inspector.inspectorFront.getNodeActorFromContentDomReference( 891 domReference 892 ); 893 if (!nodeFront) { 894 return; 895 } 896 897 // "new-node-front" tells us when the node has been selected, whether the 898 // browser is remote or not. 899 const onNewNode = inspector.selection.once("new-node-front"); 900 // Select the final node 901 inspector.selection.setNodeFront(nodeFront, { 902 reason: "browser-context-menu", 903 }); 904 905 await onNewNode; 906 // Now that the node has been selected, wait until the inspector is 907 // fully updated. 908 await inspector.once("inspector-updated"); 909 } 910 911 /** 912 * Called from the DevToolsShim, used by nsContextMenu.js. 913 * 914 * @param {XULTab} tab 915 * The browser tab on which inspect accessibility was used. 916 * @param {ElementIdentifier} domReference 917 * Identifier generated by ContentDOMReference. It is a unique pair of 918 * BrowsingContext ID and a numeric ID. 919 * @param {number} startTime 920 * Optional, indicates the time at which the user event related to this 921 * node inspection started. This is a `ChromeUtils.now()` timing. 922 * @return {Promise} a promise that resolves when the accessible object is 923 * selected in the accessibility inspector. 924 */ 925 async inspectA11Y(tab, domReference, startTime) { 926 const toolbox = await gDevTools.showToolboxForTab(tab, { 927 toolId: "accessibility", 928 startTime, 929 }); 930 const inspectorFront = await toolbox.target.getFront("inspector"); 931 const nodeFront = 932 await inspectorFront.getNodeActorFromContentDomReference(domReference); 933 if (!nodeFront) { 934 return; 935 } 936 937 // Select the accessible object in the panel and wait for the event that 938 // tells us it has been done. 939 const a11yPanel = toolbox.getCurrentPanel(); 940 const onSelected = a11yPanel.once("new-accessible-front-selected"); 941 a11yPanel.selectAccessibleForNode(nodeFront, "browser-context-menu"); 942 await onSelected; 943 } 944 945 /** 946 * Either the DevTools Loader has been destroyed or firefox is shutting down. 947 * 948 * @param {boolean} shuttingDown 949 * True if firefox is currently shutting down. We may prevent doing 950 * some cleanups to speed it up. Otherwise everything need to be 951 * cleaned up in order to be able to load devtools again. 952 */ 953 destroy({ shuttingDown }) { 954 // Do not cleanup everything during firefox shutdown. 955 if (!shuttingDown) { 956 for (const [, toolbox] of this._toolboxesPerCommands) { 957 toolbox.destroy(); 958 } 959 } 960 961 for (const [key] of this.getToolDefinitionMap()) { 962 this.unregisterTool(key, true); 963 } 964 965 gDevTools.unregisterDefaults(); 966 967 removeThemeObserver(this._onThemeChanged); 968 969 // Do not unregister devtools from the DevToolsShim if the destroy is caused by an 970 // application shutdown. For instance SessionStore needs to save the Browser Toolbox 971 // state on shutdown. 972 if (!shuttingDown) { 973 // Notify the DevToolsShim that DevTools are no longer available, particularly if 974 // the destroy was caused by disabling/removing DevTools. 975 DevToolsShim.unregister(); 976 } 977 978 // Cleaning down the toolboxes: i.e. 979 // for (let [, toolbox] of this._toolboxesPerCommands) toolbox.destroy(); 980 // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow 981 } 982 983 /** 984 * Returns the array of the existing toolboxes. 985 * 986 * @return {Array<Toolbox>} 987 * An array of toolboxes. 988 */ 989 getToolboxes() { 990 return Array.from(this._toolboxesPerCommands.values()); 991 } 992 993 /** 994 * Returns whether the given tab has toolbox. 995 * 996 * @param {XULTab} tab 997 * The browser tab. 998 * @return {boolean} 999 * Returns true if the tab has toolbox. 1000 */ 1001 hasToolboxForTab(tab) { 1002 return this.getToolboxes().some( 1003 t => t.commands.descriptorFront.localTab === tab 1004 ); 1005 } 1006 } 1007 1008 const gDevTools = (exports.gDevTools = new DevTools());