window-global.js (69266B)
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 // protocol.js uses objects as exceptions in order to define 8 // error packets. 9 /* eslint-disable no-throw-literal */ 10 11 /* 12 * WindowGlobalTargetActor is an abstract class used by target actors that hold 13 * documents, such as frames, chrome windows, etc. 14 * 15 * This class is extended by ParentProcessTargetActor. 16 * 17 * See devtools/docs/contributor/backend/actor-hierarchy.md for more details about all the targets. 18 * 19 * For performance matters, this file should only be loaded in the targeted context's 20 * process. For example, it shouldn't be evaluated in the parent process until we try to 21 * debug a document living in the parent process. 22 */ 23 24 var { 25 ActorRegistry, 26 } = require("resource://devtools/server/actors/utils/actor-registry.js"); 27 var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 28 var { assert } = DevToolsUtils; 29 var { 30 SourcesManager, 31 } = require("resource://devtools/server/actors/utils/sources-manager.js"); 32 var makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js"); 33 const Targets = require("resource://devtools/server/actors/targets/index.js"); 34 35 const lazy = {}; 36 // Modules loaded in the same module loader as this module. 37 // (may spawn distinct module instances with other devtools and firefox frontend) 38 ChromeUtils.defineESModuleGetters( 39 lazy, 40 { 41 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 42 WEBEXTENSION_FALLBACK_DOC_URL: 43 "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs", 44 getAddonIdForWindowGlobal: 45 "resource://devtools/server/actors/watcher/browsing-context-helpers.sys.mjs", 46 }, 47 { global: "contextual" } 48 ); 49 50 // Modules loaded from the shared module loader. 51 // Won't be as debuggable from the Browser Toolbox and may trigger breakpoints. 52 ChromeUtils.defineESModuleGetters( 53 lazy, 54 { 55 TargetActorRegistry: 56 "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs", 57 }, 58 { global: "shared" } 59 ); 60 61 const { Pool } = require("resource://devtools/shared/protocol.js"); 62 const { 63 LazyPool, 64 createExtraActors, 65 } = require("resource://devtools/shared/protocol/lazy-pool.js"); 66 const { 67 windowGlobalTargetSpec, 68 } = require("resource://devtools/shared/specs/targets/window-global.js"); 69 const Resources = require("resource://devtools/server/actors/resources/index.js"); 70 const { 71 BaseTargetActor, 72 } = require("resource://devtools/server/actors/targets/base-target-actor.js"); 73 74 loader.lazyRequireGetter( 75 this, 76 ["ThreadActor"], 77 "resource://devtools/server/actors/thread.js", 78 true 79 ); 80 loader.lazyRequireGetter( 81 this, 82 "WorkerDescriptorActorList", 83 "resource://devtools/server/actors/worker/worker-descriptor-actor-list.js", 84 true 85 ); 86 loader.lazyRequireGetter( 87 this, 88 "StyleSheetsManager", 89 "resource://devtools/server/actors/utils/stylesheets-manager.js", 90 true 91 ); 92 loader.lazyRequireGetter( 93 this, 94 "TouchSimulator", 95 "resource://devtools/server/actors/emulation/touch-simulator.js", 96 true 97 ); 98 99 function getWindowID(window) { 100 return window.windowGlobalChild.innerWindowId; 101 } 102 103 function getDocShellChromeEventHandler(docShell) { 104 let handler = docShell.chromeEventHandler; 105 if (!handler) { 106 try { 107 // Toplevel xul window's docshell doesn't have chromeEventHandler 108 // attribute. The chrome event handler is just the global window object. 109 handler = docShell.domWindow; 110 } catch (e) { 111 // ignore 112 } 113 } 114 return handler; 115 } 116 117 /** 118 * Helper to retrieve all children docshells of a given docshell. 119 * 120 * Given that docshell interfaces can only be used within the same process, 121 * this only returns docshells for children documents that runs in the same process 122 * as the given docshell. 123 */ 124 function getChildDocShells(parentDocShell) { 125 return parentDocShell.browsingContext 126 .getAllBrowsingContextsInSubtree() 127 .filter(browsingContext => { 128 // Filter out browsingContext which don't expose any docshell (e.g. remote frame) 129 return browsingContext.docShell; 130 }) 131 .map(browsingContext => { 132 // Map BrowsingContext to DocShell 133 return browsingContext.docShell; 134 }); 135 } 136 137 exports.getChildDocShells = getChildDocShells; 138 139 /** 140 * Helper to retrieve all the windows (parent, children, or browsing context own window) 141 * that exists in the same process than the given browsing context. 142 * 143 * @param {BrowsingContext} browsingContext 144 * @returns {Array<Window>} 145 */ 146 function getAllSameProcessGlobalsFromBrowsingContext(browsingContext) { 147 const windows = []; 148 const topBrowsingContext = browsingContext.top; 149 for (const bc of topBrowsingContext.getAllBrowsingContextsInSubtree()) { 150 // Filter out browsingContext which don't expose any docshell (e.g. remote frame) 151 if (!bc.docShell) { 152 continue; 153 } 154 try { 155 windows.push(bc.docShell.domWindow); 156 } catch (e) { 157 // docShell.domWindow may throw when the docshell is being destroyed. 158 // Ignore them. We can't use docShell.isBeingDestroyed as it 159 // is flagging too early. e.g it's already true when hitting a breakpoint 160 // in the unload event. 161 } 162 } 163 164 return windows; 165 } 166 167 /** 168 * Browser-specific actors. 169 */ 170 171 function getInnerId(window) { 172 return window.windowGlobalChild.innerWindowId; 173 } 174 175 class WindowGlobalTargetActor extends BaseTargetActor { 176 /** 177 * WindowGlobalTargetActor is the target actor to debug (HTML) documents. 178 * 179 * WindowGlobal's are the Gecko representation for a given document's window object. 180 * It relates to a given nsGlobalWindowInner instance. 181 * 182 * The main goal of this class is to expose the target-scoped actors being registered 183 * via `ActorRegistry.registerModule` and manage their lifetimes. In addition, this 184 * class also tracks the lifetime of the targeted window global. 185 * 186 * ### Main requests: 187 * 188 * `detach`: 189 * Stop document watching and cleanup everything that the target and its children actors created. 190 * It ultimately lead to destroy the target actor. 191 * `switchToFrame`: 192 * Change the targeted document of the whole actor, and its child target-scoped actors 193 * to an iframe or back to its original document. 194 * 195 * Most properties (like `chromeEventHandler` or `docShells`) are meant to be 196 * used by the various child target actors. 197 * 198 * ### RDP events: 199 * 200 * - `tabNavigated`: 201 * Sent when the window global is about to navigate or has just navigated 202 * to a different document. 203 * This event contains the following attributes: 204 * * url (string) 205 * The new URI being loaded. 206 * * state (string) 207 * `start` if we just start requesting the new URL 208 * `stop` if the new URL is done loading 209 * * isFrameSwitching (boolean) 210 * Indicates the event is dispatched when switching the actor context to a 211 * different frame. When we switch to an iframe, there is no document 212 * load. The targeted document is most likely going to be already done 213 * loading. 214 * * title (string) 215 * The document title being loaded. (sent only on state=stop) 216 * 217 * - `frameUpdate`: 218 * Sent when there was a change in the child frames contained in the document 219 * or when the actor's context was switched to another frame. 220 * This event can have four different forms depending on the type of change: 221 * * One or many frames are updated: 222 * { frames: [{ id, url, title, parentID }, ...] } 223 * * One frame got destroyed: 224 * { frames: [{ id, destroy: true }]} 225 * * All frames got destroyed: 226 * { destroyAll: true } 227 * * We switched the context of the actor to a specific frame: 228 * { selected: #id } 229 * 230 * ### Internal, non-rdp events: 231 * 232 * Various events are also dispatched on the actor itself without being sent to 233 * the client. They all relate to the documents tracked by this target actor 234 * (its main targeted document, but also any of its iframes): 235 * - will-navigate 236 * This event fires once navigation starts. All pending user prompts are 237 * dealt with, but it is fired before the first request starts. 238 * - navigate 239 * This event is fired once the document's readyState is "complete". 240 * - window-ready 241 * This event is fired in various distinct scenarios: 242 * * When a new Window object is crafted, equivalent of `DOMWindowCreated`. 243 * It is dispatched before any page script is executed. 244 * * We will have already received a window-ready event for this window 245 * when it was created, but we received a window-destroyed event when 246 * it was frozen into the bfcache, and now the user navigated back to 247 * this page, so it's now live again and we should resume handling it. 248 * * For each existing document, when an `attach` request is received. 249 * At this point scripts in the page will be already loaded. 250 * * When `swapFrameLoaders` is used, such as with moving window globals 251 * between windows or toggling Responsive Design Mode. 252 * - window-destroyed 253 * This event is fired in two cases: 254 * * When the window object is destroyed, i.e. when the related document 255 * is garbage collected. This can happen when the window global is 256 * closed or the iframe is removed from the DOM. 257 * It is equivalent of `inner-window-destroyed` event. 258 * * When the page goes into the bfcache and gets frozen. 259 * The equivalent of `pagehide`. 260 * - changed-toplevel-document 261 * This event fires when we switch the actor's targeted document 262 * to one of its iframes, or back to its original top document. 263 * It is dispatched between window-destroyed and window-ready. 264 * 265 * Note that *all* these events are dispatched in the following order 266 * when we switch the context of the actor to a given iframe: 267 * - will-navigate 268 * - window-destroyed 269 * - changed-toplevel-document 270 * - window-ready 271 * - navigate 272 * 273 * This class is subclassed by ParentProcessTargetActor and others. 274 * Subclasses are expected to implement a getter for the docShell property. 275 * 276 * @param conn DevToolsServerConnection 277 * The conection to the client. 278 * @param options Object 279 * Object with following attributes: 280 * - docShell nsIDocShell 281 * The |docShell| for the debugged frame. 282 * - followWindowGlobalLifeCycle Boolean 283 * If true, the target actor will only inspect the current WindowGlobal (and its children windows). 284 * But won't inspect next document loaded in the same BrowsingContext. 285 * The actor will behave more like a WindowGlobalTarget rather than a BrowsingContextTarget. 286 * This is always true for Tab and web extension debugging, but not yet for parent process target 287 * used by the browser toolbox. 288 * - isTopLevelTarget Boolean 289 * Should be set to true for all top-level targets. A top level target 290 * is the topmost target of a DevTools "session". For instance for a local 291 * tab toolbox, the WindowGlobalTargetActor for the content page is the top level target. 292 * For the Multiprocess Browser Toolbox, the parent process target is the top level 293 * target. 294 * At the moment this only impacts the WindowGlobalTarget `reconfigure` 295 * implementation. But for server-side target switching this flag will be exposed 296 * to the client and should be available for all target actor classes. It will be 297 * used to detect target switching. (Bug 1644397) 298 * - ignoreSubFrames Boolean 299 * If true, the actor will only focus on the passed docShell and not on the whole 300 * docShell tree. This should be enabled when we have targets for all documents. 301 * - sessionContext Object 302 * The Session Context to help know what is debugged. 303 * See devtools/server/actors/watcher/session-context.js 304 */ 305 constructor( 306 conn, 307 { 308 docShell, 309 followWindowGlobalLifeCycle, 310 isTopLevelTarget, 311 ignoreSubFrames, 312 sessionContext, 313 customSpec = windowGlobalTargetSpec, 314 } 315 ) { 316 super(conn, Targets.TYPES.FRAME, customSpec); 317 318 this.followWindowGlobalLifeCycle = followWindowGlobalLifeCycle; 319 this.isTopLevelTarget = !!isTopLevelTarget; 320 this.ignoreSubFrames = ignoreSubFrames; 321 this.sessionContext = sessionContext; 322 323 // A map of actor names to actor instances provided by extensions. 324 this._extraActors = {}; 325 this._sourcesManager = null; 326 327 this._shouldAddNewGlobalAsDebuggee = 328 this._shouldAddNewGlobalAsDebuggee.bind(this); 329 330 this.makeDebugger = makeDebugger.bind(null, { 331 findDebuggees: (dbg, includeAllSameProcessGlobals) => { 332 const result = []; 333 const inspectUAWidgets = Services.prefs.getBoolPref( 334 "devtools.inspector.showAllAnonymousContent", 335 false 336 ); 337 const windows = includeAllSameProcessGlobals 338 ? getAllSameProcessGlobalsFromBrowsingContext(this.browsingContext) 339 : this.windows; 340 for (const win of windows) { 341 result.push(win); 342 // Only expose User Agent internal (like <video controls>) when the 343 // related pref is set. 344 if (inspectUAWidgets) { 345 const principal = win.document.nodePrincipal; 346 // We don't use UA widgets for the system principal. 347 if (!principal.isSystemPrincipal) { 348 result.push(Cu.getUAWidgetScope(principal)); 349 } 350 } 351 } 352 return result; 353 }, 354 shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee, 355 }); 356 357 // Flag eventually overloaded by sub classes in order to watch new docshells 358 // Used by the ParentProcessTargetActor to list all frames in the Browser Toolbox 359 this.watchNewDocShells = false; 360 361 this._workerDescriptorActorList = null; 362 this._workerDescriptorActorPool = null; 363 this._onWorkerDescriptorActorListChanged = 364 this._onWorkerDescriptorActorListChanged.bind(this); 365 366 this._onConsoleApiProfilerEvent = 367 this._onConsoleApiProfilerEvent.bind(this); 368 Services.obs.addObserver( 369 this._onConsoleApiProfilerEvent, 370 "console-api-profiler" 371 ); 372 373 // Start observing navigations as well as sub documents. 374 // (This is probably meant to disappear once EFT is the only supported codepath) 375 this._progressListener = new DebuggerProgressListener(this); 376 377 lazy.TargetActorRegistry.registerTargetActor(this); 378 379 if (docShell) { 380 this.setDocShell(docShell); 381 } 382 } 383 384 // Attribute only set when debugging web extensions, in order to distinguish: 385 // - "fallback-document" created by DevTools for the top level target 386 // - "background" for the background page 387 // - "popup" for any popup document 388 #isFallbackExtensionDocument = false; 389 390 /** 391 * Define the initial docshell. 392 * 393 * This is called from the constructor for WindowGlobalTargetActor, 394 * or from sub class constructor of ParentProcessTargetActor. 395 * 396 * This is to circumvent the fact that sub classes need to call inner method 397 * to compute the initial docshell and we can't call inner methods before calling 398 * the base class constructor... 399 */ 400 setDocShell(docShell) { 401 Object.defineProperty(this, "docShell", { 402 value: docShell, 403 configurable: true, 404 writable: true, 405 }); 406 407 // When this target tracks only one WindowGlobal, set a fixed innerWindowId and window, 408 // so that it can easily be read safely while the related WindowGlobal is being destroyed. 409 if (this.followWindowGlobalLifeCycle) { 410 Object.defineProperty(this, "innerWindowId", { 411 value: this.innerWindowId, 412 configurable: false, 413 writable: false, 414 }); 415 Object.defineProperty(this, "window", { 416 value: this.window, 417 configurable: false, 418 writable: false, 419 }); 420 Object.defineProperty(this, "chromeEventHandler", { 421 value: this.chromeEventHandler, 422 configurable: false, 423 writable: false, 424 }); 425 } 426 427 // Save references to the original document we attached to 428 this._originalWindow = this.window; 429 430 // Update isPrivate as window is based on docShell 431 this.isPrivate = lazy.PrivateBrowsingUtils.isContentWindowPrivate( 432 this.window 433 ); 434 435 // Instantiate the Thread Actor immediately. 436 // This is the only one actor instantiated right away by the target actor. 437 // All the others are instantiated lazily on first request made the client, 438 // via LazyPool API. 439 this._createThreadActor(); 440 441 // Ensure notifying about the target actor first 442 // before notifying about new docshells. 443 // Otherwise we would miss these RDP event as the client hasn't 444 // yet received the target actor's form. 445 // (This is also probably meant to disappear once EFT is the only supported codepath) 446 this._docShellsObserved = false; 447 DevToolsUtils.executeSoon(() => this._watchDocshells()); 448 449 // The `watchedByDevTools` enables gecko behavior tied to this flag, such as: 450 // - reporting the contents of HTML loaded in the docshells, 451 // - or capturing stacks for the network monitor. 452 // 453 // This flag can only be set on top level BrowsingContexts. 454 if (!this.browsingContext.parent) { 455 this.browsingContext.watchedByDevTools = true; 456 } 457 458 if (this.sessionContext.type == "webextension") { 459 if ( 460 this.window.location.href.startsWith(lazy.WEBEXTENSION_FALLBACK_DOC_URL) 461 ) { 462 this.#isFallbackExtensionDocument = true; 463 } 464 } 465 } 466 467 get docShell() { 468 throw new Error( 469 "A docShell should be provided as constructor argument of WindowGlobalTargetActor, or redefined by the subclass" 470 ); 471 } 472 473 /* 474 * Return a Debugger instance or create one if there is none yet 475 */ 476 get dbg() { 477 if (!this._dbg) { 478 this._dbg = this.makeDebugger(); 479 } 480 return this._dbg; 481 } 482 483 /** 484 * Try to locate the console actor if it exists. 485 */ 486 get _consoleActor() { 487 if (this.isDestroyed()) { 488 return null; 489 } 490 const form = this.form(); 491 return this.conn._getOrCreateActor(form.consoleActor); 492 } 493 494 get _memoryActor() { 495 if (this.isDestroyed()) { 496 return null; 497 } 498 const form = this.form(); 499 return this.conn._getOrCreateActor(form.memoryActor); 500 } 501 502 _targetScopedActorPool = null; 503 504 /** 505 * A EventTarget object on which to listen for 'DOMWindowCreated' and 'pageshow' events. 506 */ 507 get chromeEventHandler() { 508 return getDocShellChromeEventHandler(this.docShell); 509 } 510 511 /** 512 * Getter for the list of all `docShell`s in the window global. 513 * 514 * @return {Array} 515 */ 516 get docShells() { 517 if (this.ignoreSubFrames) { 518 return [this.docShell]; 519 } 520 521 return getChildDocShells(this.docShell); 522 } 523 524 /** 525 * Getter for the window global's current DOM window. 526 */ 527 get window() { 528 try { 529 return this.docShell?.domWindow; 530 } catch (e) { 531 // When querying `domWindow` on document's unload `docShell.isBeingDestroyed()` will return true, 532 // whereas `domWindow` is still functional... and useful to return! 533 return null; 534 } 535 } 536 537 get targetGlobal() { 538 return this.window; 539 } 540 541 get outerWindowID() { 542 if (this.docShell) { 543 return this.docShell.outerWindowID; 544 } 545 return null; 546 } 547 548 get browsingContext() { 549 return this.docShell?.browsingContext; 550 } 551 552 get browsingContextID() { 553 return this.browsingContext?.id; 554 } 555 556 get innerWindowId() { 557 return this.window?.windowGlobalChild.innerWindowId; 558 } 559 560 get browserId() { 561 return this.browsingContext?.browserId; 562 } 563 564 get openerBrowserId() { 565 return this.browsingContext?.opener?.browserId; 566 } 567 568 /** 569 * Getter for the list of all content DOM windows in the window global. 570 * 571 * @return {Array} 572 */ 573 get windows() { 574 const windows = []; 575 for (const docShell of this.docShells) { 576 try { 577 windows.push(docShell.domWindow); 578 } catch (e) { 579 // docShell.domWindow may throw when the docshell is being destroyed. 580 // Ignore them. We can't use docShell.isBeingDestroyed as it 581 // is flagging too early. e.g it's already true when hitting a breakpoint 582 // in the unload event. 583 } 584 } 585 return windows; 586 } 587 588 /** 589 * Getter for the original docShell this actor got attached to in the first 590 * place. 591 * Note that your actor should normally *not* rely on this top level docShell 592 * if you want it to show information relative to the iframe that's currently 593 * being inspected in the toolbox. 594 */ 595 get originalDocShell() { 596 if (!this._originalWindow || Cu.isDeadWrapper(this._originalWindow)) { 597 return this.docShell; 598 } 599 600 return this._originalWindow.docShell; 601 } 602 603 /** 604 * Getter for the original window this actor got attached to in the first 605 * place. 606 * Note that your actor should normally *not* rely on this top level window if 607 * you want it to show information relative to the iframe that's currently 608 * being inspected in the toolbox. 609 */ 610 get originalWindow() { 611 return this._originalWindow || this.window; 612 } 613 614 /** 615 * Getter for the nsIWebProgress for watching this window. 616 */ 617 get webProgress() { 618 return this.docShell 619 .QueryInterface(Ci.nsIInterfaceRequestor) 620 .getInterface(Ci.nsIWebProgress); 621 } 622 623 /** 624 * Getter for the nsIWebNavigation for the target. 625 */ 626 get webNavigation() { 627 return this.docShell.QueryInterface(Ci.nsIWebNavigation); 628 } 629 630 /** 631 * Getter for the window global's document. 632 */ 633 get contentDocument() { 634 return this.webNavigation.document; 635 } 636 637 /** 638 * Getter for the window global's title. 639 */ 640 get title() { 641 return this.contentDocument.title; 642 } 643 644 /** 645 * Getter for the window global's URL. 646 */ 647 get url() { 648 if (this.webNavigation.currentURI) { 649 return this.webNavigation.currentURI.spec; 650 } 651 // Abrupt closing of the browser window may leave callbacks without a 652 // currentURI. 653 return null; 654 } 655 656 get sourcesManager() { 657 if (!this._sourcesManager) { 658 this._sourcesManager = new SourcesManager(this.threadActor); 659 } 660 return this._sourcesManager; 661 } 662 663 getStyleSheetsManager() { 664 if (!this._styleSheetsManager) { 665 this._styleSheetsManager = new StyleSheetsManager(this); 666 } 667 return this._styleSheetsManager; 668 } 669 670 _createExtraActors() { 671 // Always use the same Pool, so existing actor instances 672 // (created in createExtraActors) are not lost. 673 if (!this._targetScopedActorPool) { 674 this._targetScopedActorPool = new LazyPool(this.conn); 675 } 676 677 // Walk over target-scoped actor factories and make sure they are all 678 // instantiated and added into the Pool. 679 return createExtraActors( 680 ActorRegistry.targetScopedActorFactories, 681 this._targetScopedActorPool, 682 this 683 ); 684 } 685 686 form() { 687 assert( 688 !this.isDestroyed(), 689 "form() shouldn't be called on destroyed browser actor." 690 ); 691 assert(this.actorID, "Actor should have an actorID."); 692 693 // If the actor or the document is already being destroyed, return a very minimal form, 694 // enough to identify the target actor from the client side and attributes used by the 695 // server code to process the target destruction. 696 // Otherwise, many of the form attributes can't be retrieved and would throw exceptions. 697 // Also, `_createExtraActors` may start re-creating actor as they were already cleared by destroy()... 698 if (this.destroying || !this.originalDocShell) { 699 return { 700 actor: this.actorID, 701 innerWindowId: this.innerWindowId, 702 isTopLevelTarget: this.isTopLevelTarget, 703 }; 704 } 705 706 // Note that we don't want the iframe dropdown to change our BrowsingContext.id/innerWindowId 707 // We only want to refer to the topmost original window we attached to 708 // as that's the one top document this target actor really represent. 709 // The iframe dropdown is just a hack that temporarily focus the scope 710 // of the target actor to a children iframe document. 711 const originalBrowsingContext = this.originalDocShell.browsingContext; 712 713 // When toggling the toolbox on/off many times in a row, 714 // we may try to destroy the actor while the related document is already destroyed. 715 // In such scenario, return the minimum viable form 716 if (!originalBrowsingContext.currentWindowContext) { 717 return { actor: this.actorID }; 718 } 719 720 const browsingContextID = originalBrowsingContext.id; 721 const innerWindowId = 722 originalBrowsingContext.currentWindowContext.innerWindowId; 723 const parentInnerWindowId = 724 originalBrowsingContext.parent?.currentWindowContext.innerWindowId; 725 // Doesn't only check `!!opener` as some iframe might have an opener 726 // if their location was loaded via `window.open(url, "iframe-name")`. 727 // So also ensure that the document is opened in a distinct tab. 728 const isPopup = 729 !!originalBrowsingContext.opener && 730 originalBrowsingContext.browserId != 731 originalBrowsingContext.opener.browserId; 732 733 const response = { 734 actor: this.actorID, 735 targetType: this.targetType, 736 737 browsingContextID, 738 processID: Services.appinfo.processID, 739 // True for targets created by JSWindowActors, see constructor JSDoc. 740 followWindowGlobalLifeCycle: this.followWindowGlobalLifeCycle, 741 innerWindowId, 742 parentInnerWindowId, 743 topInnerWindowId: this.browsingContext.topWindowContext.innerWindowId, 744 isTopLevelTarget: this.isTopLevelTarget, 745 ignoreSubFrames: this.ignoreSubFrames, 746 isPopup, 747 isPrivate: this.isPrivate, 748 title: this.title, 749 url: this.url, 750 outerWindowID: this.outerWindowID, 751 752 // Specific to Web Extension documents 753 isFallbackExtensionDocument: this.#isFallbackExtensionDocument, 754 addonId: lazy.getAddonIdForWindowGlobal(this.window.windowGlobalChild), 755 756 traits: { 757 // @backward-compat { version 64 } Exposes a new trait to help identify 758 // BrowsingContextActor's inherited actors from the client side. 759 isBrowsingContext: true, 760 // Browsing context targets can compute the isTopLevelTarget flag on the 761 // server. But other target actors don't support this yet. See Bug 1709314. 762 supportsTopLevelTargetFlag: true, 763 // Supports frame listing via `listFrames` request and `frameUpdate` events 764 // as well as frame switching via `switchToFrame` request 765 frames: true, 766 // Supports the logInPage request. 767 logInPage: true, 768 // Supports watchpoints in the server. We need to keep this trait because target 769 // actors that don't extend WindowGlobalTargetActor (Worker, ContentProcess, …) 770 // might not support watchpoints. 771 watchpoints: true, 772 // Supports back and forward navigation 773 navigation: true, 774 }, 775 }; 776 777 const actors = this._createExtraActors(); 778 Object.assign(response, actors); 779 780 // The thread actor is the only actor manually created by the target actor. 781 // It is not registered in targetScopedActorFactories and therefore needs 782 // to be added here manually. 783 if (this.threadActor) { 784 Object.assign(response, { 785 threadActor: this.threadActor.actorID, 786 }); 787 } 788 789 return response; 790 } 791 792 /** 793 * Called when the actor is removed from the connection. 794 * 795 * @param {object} options 796 * @param {boolean} options.isTargetSwitching: Set to true when this is called during 797 * a target switch. 798 * @param {boolean} options.isModeSwitching: Set to true true when this is called as the 799 * result of a change to the devtools.browsertoolbox.scope pref. 800 */ 801 destroy({ isTargetSwitching = false, isModeSwitching = false } = {}) { 802 // Avoid reentrancy. We will destroy the Transport when emitting "destroyed", 803 // which will force destroying all actors. 804 if (this.destroying) { 805 return; 806 } 807 this.destroying = true; 808 809 // Force flushing pending resources if the actor isn't already destroyed. 810 // This helps notify the client about pending resources on navigation. 811 if (!this.isDestroyed()) { 812 this.emitResources(); 813 } 814 815 // Tell the thread actor that the window global is closed, so that it may terminate 816 // instead of resuming the debuggee script. 817 // TODO: Bug 997119: Remove this coupling with thread actor 818 if (this.threadActor) { 819 this.threadActor._parentClosed = true; 820 } 821 822 if (this._touchSimulator) { 823 this._touchSimulator.stop(); 824 this._touchSimulator = null; 825 } 826 827 // The watchedByDevTools flag is only set on top level BrowsingContext 828 // (as it then cascades to all its children), 829 // and when destroying the target, we should tell the platform we no longer 830 // observe this BrowsingContext and set this attribute to false. 831 // Ignore this cleanup if the related BrowsingContext is being destroyed. 832 if ( 833 this.browsingContext?.watchedByDevTools && 834 !this.browsingContext.parent && 835 !this.browsingContext.isDiscarded 836 ) { 837 this.browsingContext.watchedByDevTools = false; 838 } 839 840 // Check for `docShell` availability, as it can be already gone during 841 // Firefox shutdown. 842 if (this.docShell) { 843 this._unwatchDocShell(this.docShell); 844 845 // If this target is being destroyed as part of a target switch or a mode switch, 846 // we don't need to restore the configuration (this might cause the content page to 847 // be focused again, causing issues in tests and disturbing the user when switching modes). 848 if (!isTargetSwitching && !isModeSwitching) { 849 this._restoreTargetConfiguration(); 850 } 851 } 852 this._unwatchDocshells(); 853 854 this._destroyThreadActor(); 855 856 if (this._styleSheetsManager) { 857 this._styleSheetsManager.destroy(); 858 this._styleSheetsManager = null; 859 } 860 861 // Shut down actors that belong to this target's pool. 862 if (this._targetScopedActorPool) { 863 this._targetScopedActorPool.destroy(); 864 this._targetScopedActorPool = null; 865 } 866 867 // Make sure that no more workerListChanged notifications are sent. 868 if (this._workerDescriptorActorList !== null) { 869 this._workerDescriptorActorList.destroy(); 870 this._workerDescriptorActorList = null; 871 } 872 873 if (this._workerDescriptorActorPool !== null) { 874 this._workerDescriptorActorPool.destroy(); 875 this._workerDescriptorActorPool = null; 876 } 877 878 if (this._dbg) { 879 this._dbg.disable(); 880 this._dbg = null; 881 } 882 883 // Emit a last event before calling Actor.destroy 884 // which will destroy the EventEmitter API 885 this.emit("destroyed", { isTargetSwitching, isModeSwitching }); 886 887 // Destroy BaseTargetActor before nullifying docShell in case any child actor queries the window/docShell. 888 super.destroy(); 889 890 this.docShell = null; 891 this._extraActors = null; 892 893 Services.obs.removeObserver( 894 this._onConsoleApiProfilerEvent, 895 "console-api-profiler" 896 ); 897 898 lazy.TargetActorRegistry.unregisterTargetActor(this); 899 Resources.unwatchAllResources(this); 900 } 901 902 /** 903 * This is only used by WebExtensionTargetActor, which overrides this method. 904 */ 905 _shouldAddNewGlobalAsDebuggee() { 906 return false; 907 } 908 909 _watchDocshells() { 910 // If for some unexpected reason, the actor is immediately destroyed, 911 // avoid registering leaking observer listener. 912 if (this.isDestroyed()) { 913 return; 914 } 915 916 // This method is called asynchronously and the document may have been destroyed in the meantime. 917 // In such case, automatically destroy the target actor. 918 if (this.docShell.isBeingDestroyed()) { 919 this.destroy(); 920 return; 921 } 922 923 // In child processes, we watch all docshells living in the process. 924 // Avoid watching for all docshell unless we are in non-EFT codepath, 925 // which only happens for the ParentProcess's WindowGlobalTarget 926 // used by the browser toolbox to reach all documents/docshells in the parent process. 927 if (!this.ignoreSubFrames) { 928 Services.obs.addObserver(this, "webnavigation-create"); 929 Services.obs.addObserver(this, "webnavigation-destroy"); 930 this._docShellsObserved = true; 931 } 932 933 // We watch for all child docshells under the current document, 934 this._progressListener.watch(this.docShell); 935 936 // And list all already existing ones. 937 if (!this.ignoreSubFrames) { 938 this._updateChildDocShells(); 939 } 940 } 941 942 _unwatchDocshells() { 943 if (this._progressListener) { 944 this._progressListener.destroy(); 945 this._progressListener = null; 946 this._originalWindow = null; 947 } 948 949 // Removes the observers being set in _watchDocshells, but only 950 // if _watchDocshells has been called. The target actor may be immediately destroyed 951 // and doesn't have time to register them. 952 // (Calling removeObserver without having called addObserver throws) 953 if (this._docShellsObserved) { 954 Services.obs.removeObserver(this, "webnavigation-create"); 955 Services.obs.removeObserver(this, "webnavigation-destroy"); 956 this._docShellsObserved = false; 957 } 958 } 959 960 _unwatchDocShell(docShell) { 961 if (this._progressListener) { 962 this._progressListener.unwatch(docShell); 963 } 964 } 965 966 switchToFrame(request) { 967 const windowId = request.windowId; 968 let win; 969 970 try { 971 win = Services.wm.getOuterWindowWithId(windowId); 972 } catch (e) { 973 // ignore 974 } 975 if (!win) { 976 throw { 977 error: "noWindow", 978 message: "The related docshell is destroyed or not found", 979 }; 980 } else if (win == this.window) { 981 return {}; 982 } 983 984 // Reply first before changing the document 985 DevToolsUtils.executeSoon(() => this._changeTopLevelDocument(win)); 986 987 return {}; 988 } 989 990 listFrames() { 991 const windows = this._docShellsToWindows(this.docShells); 992 return { frames: windows }; 993 } 994 995 ensureWorkerDescriptorActorList() { 996 if (this._workerDescriptorActorList === null) { 997 this._workerDescriptorActorList = new WorkerDescriptorActorList( 998 this.conn, 999 { 1000 type: Ci.nsIWorkerDebugger.TYPE_DEDICATED, 1001 window: this.window, 1002 } 1003 ); 1004 } 1005 return this._workerDescriptorActorList; 1006 } 1007 1008 pauseWorkersUntilAttach(shouldPause) { 1009 this.ensureWorkerDescriptorActorList().workerPauser.setPauseMatching( 1010 shouldPause 1011 ); 1012 } 1013 1014 listWorkers() { 1015 return this.ensureWorkerDescriptorActorList() 1016 .getList() 1017 .then(actors => { 1018 const pool = new Pool(this.conn, "worker-targets"); 1019 for (const actor of actors) { 1020 pool.manage(actor); 1021 } 1022 1023 // Do not destroy the pool before transfering ownership to the newly created 1024 // pool, so that we do not accidently destroy actors that are still in use. 1025 if (this._workerDescriptorActorPool) { 1026 this._workerDescriptorActorPool.destroy(); 1027 } 1028 1029 this._workerDescriptorActorPool = pool; 1030 this._workerDescriptorActorList.onListChanged = 1031 this._onWorkerDescriptorActorListChanged; 1032 1033 return { 1034 workers: actors, 1035 }; 1036 }); 1037 } 1038 1039 logInPage(request) { 1040 const { text, category, flags } = request; 1041 const scriptErrorClass = Cc["@mozilla.org/scripterror;1"]; 1042 const scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError); 1043 scriptError.initWithWindowID( 1044 text, 1045 null, 1046 0, 1047 0, 1048 flags, 1049 category, 1050 getInnerId(this.window) 1051 ); 1052 Services.console.logMessage(scriptError); 1053 return {}; 1054 } 1055 1056 _onWorkerDescriptorActorListChanged() { 1057 this._workerDescriptorActorList.onListChanged = null; 1058 this.emit("workerListChanged"); 1059 } 1060 1061 _onConsoleApiProfilerEvent() { 1062 // TODO: We will receive console-api-profiler events for any browser running 1063 // in the same process as this target. We should filter irrelevant events, 1064 // but console-api-profiler currently doesn't emit any information to identify 1065 // the origin of the event. See Bug 1731033. 1066 1067 // The new performance panel is not compatible with console.profile(). 1068 const warningFlag = 1; 1069 this.logInPage({ 1070 text: 1071 "console.profile is not compatible with the new Performance recorder. " + 1072 "See https://bugzilla.mozilla.org/show_bug.cgi?id=1730896", 1073 category: "console.profile unavailable", 1074 flags: warningFlag, 1075 }); 1076 } 1077 1078 observe(subject, topic) { 1079 // Ignore any event that comes before/after the actor is attached. 1080 // That typically happens during Firefox shutdown. 1081 if (this.isDestroyed()) { 1082 return; 1083 } 1084 1085 subject.QueryInterface(Ci.nsIDocShell); 1086 1087 if (topic == "webnavigation-create") { 1088 this._onDocShellCreated(subject); 1089 } else if (topic == "webnavigation-destroy") { 1090 this._onDocShellDestroy(subject); 1091 } 1092 } 1093 1094 _onDocShellCreated(docShell) { 1095 // (chrome-)webnavigation-create is fired very early during docshell 1096 // construction. In new root docshells within child processes, involving 1097 // BrowserChild, this event is from within this call: 1098 // https://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l912 1099 // whereas the chromeEventHandler (and most likely other stuff) is set 1100 // later: 1101 // https://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l944 1102 // So wait a tick before watching it: 1103 DevToolsUtils.executeSoon(() => { 1104 // Bug 1142752: sometimes, the docshell appears to be immediately 1105 // destroyed, bailout early to prevent random exceptions. 1106 if (docShell.isBeingDestroyed()) { 1107 return; 1108 } 1109 1110 // In child processes, we have new root docshells, 1111 // let's watch them and all their child docshells. 1112 if (this._isRootDocShell(docShell) && this.watchNewDocShells) { 1113 this._progressListener.watch(docShell); 1114 } 1115 this._notifyDocShellsUpdate([docShell]); 1116 }); 1117 } 1118 1119 _onDocShellDestroy(docShell) { 1120 // Stop watching this docshell (the unwatch() method will check if we 1121 // started watching it before). 1122 this._unwatchDocShell(docShell); 1123 1124 const webProgress = docShell 1125 .QueryInterface(Ci.nsIInterfaceRequestor) 1126 .getInterface(Ci.nsIWebProgress); 1127 this._notifyDocShellDestroy(webProgress); 1128 1129 if (webProgress.DOMWindow == this._originalWindow) { 1130 // If the original top level document we connected to is removed, 1131 // we try to switch to any other top level document 1132 const rootDocShells = this.docShells.filter(d => { 1133 // Ignore docshells without a working DOM Window. 1134 // When we close firefox we have a chrome://extensions/content/dummy.xhtml 1135 // which is in process of being destroyed and we might try to fallback to it. 1136 // Unfortunately docshell.isBeingDestroyed() doesn't return true... 1137 return d != this.docShell && this._isRootDocShell(d) && d.DOMWindow; 1138 }); 1139 if (rootDocShells.length) { 1140 const newRoot = rootDocShells[0]; 1141 this._originalWindow = newRoot.DOMWindow; 1142 this._changeTopLevelDocument(this._originalWindow); 1143 } else { 1144 // If for some reason (typically during Firefox shutdown), the original 1145 // document is destroyed, and there is no other top level docshell, 1146 // we detach the actor to unregister all listeners and prevent any 1147 // exception. 1148 this.destroy(); 1149 } 1150 return; 1151 } 1152 1153 // If the currently targeted window global is destroyed, and we aren't on 1154 // the top-level document, we have to switch to the top-level one. 1155 if ( 1156 webProgress.DOMWindow == this.window && 1157 this.window != this._originalWindow 1158 ) { 1159 this._changeTopLevelDocument(this._originalWindow); 1160 } 1161 } 1162 1163 _isRootDocShell(docShell) { 1164 // Should report as root docshell: 1165 // - New top level window's docshells, when using ParentProcessTargetActor against a 1166 // process. It allows tracking iframes of the newly opened windows 1167 // like Browser console or new browser windows. 1168 // - MozActivities or window.open frames on B2G, where a new root docshell 1169 // is spawn in the child process of the app. 1170 return !docShell.parent; 1171 } 1172 1173 _docShellToWindow(docShell) { 1174 const webProgress = docShell 1175 .QueryInterface(Ci.nsIInterfaceRequestor) 1176 .getInterface(Ci.nsIWebProgress); 1177 const window = webProgress.DOMWindow; 1178 const id = docShell.outerWindowID; 1179 let parentID = undefined; 1180 // Ignore the parent of the original document on non-e10s firefox, 1181 // as we get the xul window as parent and don't care about it. 1182 // Furthermore, ignore setting parentID when parent window is same as 1183 // current window in order to deal with front end. e.g. toolbox will be fall 1184 // into infinite loop due to recursive search with by using parent id. 1185 if ( 1186 window.parent && 1187 window.parent != window && 1188 window != this._originalWindow 1189 ) { 1190 parentID = window.parent.docShell.outerWindowID; 1191 } 1192 1193 return { 1194 id, 1195 parentID, 1196 isTopLevel: window == this.originalWindow && this.isTopLevelTarget, 1197 url: window.location.href, 1198 title: window.document.title, 1199 }; 1200 } 1201 1202 // Convert docShell list to windows objects list being sent to the client 1203 _docShellsToWindows(docshells) { 1204 return docshells 1205 .filter(docShell => { 1206 // Ensure docShell.document is available. 1207 docShell.QueryInterface(Ci.nsIWebNavigation); 1208 1209 // don't include transient about:blank documents 1210 if (docShell.document.isUncommittedInitialDocument) { 1211 return false; 1212 } 1213 1214 return true; 1215 }) 1216 .map(docShell => this._docShellToWindow(docShell)); 1217 } 1218 1219 _notifyDocShellsUpdate(docshells) { 1220 // Only top level target uses frameUpdate in order to update the iframe dropdown. 1221 // This may eventually be replaced by Target listening and target switching. 1222 if (!this.isTopLevelTarget) { 1223 return; 1224 } 1225 1226 const windows = this._docShellsToWindows(docshells); 1227 1228 // Do not send the `frameUpdate` event if the windows array is empty. 1229 if (!windows.length) { 1230 return; 1231 } 1232 1233 this.emit("frameUpdate", { 1234 frames: windows, 1235 }); 1236 } 1237 1238 _updateChildDocShells() { 1239 this._notifyDocShellsUpdate(this.docShells); 1240 } 1241 1242 _notifyDocShellDestroy(webProgress) { 1243 // Only top level target uses frameUpdate in order to update the iframe dropdown. 1244 // This may eventually be replaced by Target listening and target switching. 1245 if (!this.isTopLevelTarget) { 1246 return; 1247 } 1248 1249 webProgress = webProgress.QueryInterface(Ci.nsIWebProgress); 1250 const id = webProgress.DOMWindow.docShell.outerWindowID; 1251 this.emit("frameUpdate", { 1252 frames: [ 1253 { 1254 id, 1255 destroy: true, 1256 }, 1257 ], 1258 }); 1259 } 1260 1261 /** 1262 * Creates and manages the thread actor as part of the Browsing Context Target pool. 1263 * This sets up the content window for being debugged 1264 */ 1265 _createThreadActor() { 1266 this.threadActor = new ThreadActor(this); 1267 this.manage(this.threadActor); 1268 } 1269 1270 /** 1271 * Exits the current thread actor and removes it from the Browsing Context Target pool. 1272 * The content window is no longer being debugged after this call. 1273 */ 1274 _destroyThreadActor() { 1275 if (this.threadActor) { 1276 this.threadActor.destroy(); 1277 this.threadActor = null; 1278 } 1279 1280 if (this._sourcesManager) { 1281 this._sourcesManager.destroy(); 1282 this._sourcesManager = null; 1283 } 1284 } 1285 1286 // Protocol Request Handlers 1287 1288 detach() { 1289 // Destroy the actor in the next event loop in order 1290 // to ensure responding to the `detach` request. 1291 DevToolsUtils.executeSoon(() => { 1292 this.destroy(); 1293 }); 1294 1295 return {}; 1296 } 1297 1298 /** 1299 * Bring the window global's window to front. 1300 */ 1301 focus() { 1302 if (this.window) { 1303 this.window.focus(); 1304 } 1305 return {}; 1306 } 1307 1308 goForward() { 1309 // Wait a tick so that the response packet can be dispatched before the 1310 // subsequent navigation event packet. 1311 Services.tm.dispatchToMainThread( 1312 DevToolsUtils.makeInfallible(() => { 1313 // This won't work while the browser is shutting down and we don't really 1314 // care. 1315 if (Services.startup.shuttingDown) { 1316 return; 1317 } 1318 1319 this.webNavigation.goForward(); 1320 }, "WindowGlobalTargetActor.prototype.goForward's delayed body") 1321 ); 1322 1323 return {}; 1324 } 1325 1326 goBack() { 1327 // Wait a tick so that the response packet can be dispatched before the 1328 // subsequent navigation event packet. 1329 Services.tm.dispatchToMainThread( 1330 DevToolsUtils.makeInfallible(() => { 1331 // This won't work while the browser is shutting down and we don't really 1332 // care. 1333 if (Services.startup.shuttingDown) { 1334 return; 1335 } 1336 1337 this.webNavigation.goBack(); 1338 }, "WindowGlobalTargetActor.prototype.goBack's delayed body") 1339 ); 1340 1341 return {}; 1342 } 1343 1344 /** 1345 * Reload the page in this window global. 1346 * 1347 * @backward-compat { legacy } 1348 * reload is preserved for third party tools. See Bug 1717837. 1349 * DevTools should use Descriptor::reloadDescriptor instead. 1350 */ 1351 reload(request) { 1352 const force = request?.options?.force; 1353 // Wait a tick so that the response packet can be dispatched before the 1354 // subsequent navigation event packet. 1355 Services.tm.dispatchToMainThread( 1356 DevToolsUtils.makeInfallible(() => { 1357 // This won't work while the browser is shutting down and we don't really 1358 // care. 1359 if (Services.startup.shuttingDown) { 1360 return; 1361 } 1362 1363 this.webNavigation.reload( 1364 force 1365 ? Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE 1366 : Ci.nsIWebNavigation.LOAD_FLAGS_NONE 1367 ); 1368 }, "WindowGlobalTargetActor.prototype.reload's delayed body") 1369 ); 1370 return {}; 1371 } 1372 1373 /** 1374 * Navigate this window global to a new location 1375 */ 1376 navigateTo(request) { 1377 // Wait a tick so that the response packet can be dispatched before the 1378 // subsequent navigation event packet. 1379 Services.tm.dispatchToMainThread( 1380 DevToolsUtils.makeInfallible(() => { 1381 this.window.location = request.url; 1382 }, "WindowGlobalTargetActor.prototype.navigateTo's delayed body:" + request.url) 1383 ); 1384 return {}; 1385 } 1386 1387 /** 1388 * For browsing-context targets which can't use the watcher configuration 1389 * actor (eg webextension targets), the client directly calls `reconfigure`. 1390 * Once all targets support the watcher, this method can be removed. 1391 */ 1392 reconfigure(request) { 1393 const options = request.options || {}; 1394 return this.updateTargetConfiguration(options); 1395 } 1396 1397 /** 1398 * Apply target-specific options. 1399 * 1400 * This will be called by the watcher when the DevTools target-configuration 1401 * is updated, or when a target is created via JSWindowActors. 1402 */ 1403 updateTargetConfiguration(options = {}, calledFromDocumentCreation = false) { 1404 if (!this.docShell) { 1405 // The window global is already closed. 1406 return; 1407 } 1408 1409 // Also update configurations which applies to all target types 1410 super.updateTargetConfiguration(options, calledFromDocumentCreation); 1411 1412 let reload = false; 1413 if (typeof options.touchEventsOverride !== "undefined") { 1414 const enableTouchSimulator = options.touchEventsOverride === "enabled"; 1415 1416 // We want to reload the document if it's an "existing" top level target on which 1417 // the touch simulator will be toggled and the user has turned the 1418 // "reload on touch simulation" setting on. 1419 if ( 1420 enableTouchSimulator !== this.touchSimulator.enabled && 1421 options.reloadOnTouchSimulationToggle === true && 1422 this.isTopLevelTarget && 1423 !calledFromDocumentCreation 1424 ) { 1425 reload = true; 1426 } 1427 1428 if (enableTouchSimulator) { 1429 this.touchSimulator.start(); 1430 } else { 1431 this.touchSimulator.stop(); 1432 } 1433 } 1434 1435 if (typeof options.customFormatters !== "undefined") { 1436 this.customFormatters = options.customFormatters; 1437 } 1438 1439 if (typeof options.useSimpleHighlightersForReducedMotion == "boolean") { 1440 this._useSimpleHighlightersForReducedMotion = 1441 options.useSimpleHighlightersForReducedMotion; 1442 this.emit("use-simple-highlighters-updated"); 1443 } 1444 1445 if (!this.isTopLevelTarget) { 1446 // Following DevTools target options should only apply to the top target and be 1447 // propagated through the window global tree via the platform. 1448 return; 1449 } 1450 if (typeof options.restoreFocus == "boolean") { 1451 this._restoreFocus = options.restoreFocus; 1452 } 1453 if (typeof options.recordAllocations == "object") { 1454 const actor = this._memoryActor; 1455 if (options.recordAllocations == null) { 1456 actor.stopRecordingAllocations(); 1457 } else { 1458 actor.attach(); 1459 actor.startRecordingAllocations(options.recordAllocations); 1460 } 1461 } 1462 1463 if (reload) { 1464 this.webNavigation.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE); 1465 } 1466 } 1467 1468 get touchSimulator() { 1469 if (!this._touchSimulator) { 1470 this._touchSimulator = new TouchSimulator(this); 1471 } 1472 1473 return this._touchSimulator; 1474 } 1475 1476 /** 1477 * Opposite of the updateTargetConfiguration method, that resets document 1478 * state when closing the toolbox. 1479 */ 1480 _restoreTargetConfiguration() { 1481 if (this._restoreFocus && this.browsingContext?.isActive && this.window) { 1482 try { 1483 this.window.focus(); 1484 } catch (e) { 1485 // When closing devtools while navigating, focus() may throw NS_ERROR_XPC_SECURITY_MANAGER_VETO 1486 if (e.result != Cr.NS_ERROR_XPC_SECURITY_MANAGER_VETO) { 1487 throw e; 1488 } 1489 } 1490 } 1491 } 1492 1493 _changeTopLevelDocument(window) { 1494 // WebExtensions are still using one WindowGlobalTarget instance for many document. 1495 // When reloading the add-on, the current docShell/window we are attached to may be being destroyed 1496 // and throwing when accessing its properties. 1497 // Ignore the current window and only register the new and functional window. 1498 if (!this.docShell.isBeingDestroyed() && this.window) { 1499 // Fake a will-navigate on the previous document 1500 // to let a chance to unregister it 1501 this._willNavigate({ 1502 window: this.window, 1503 newURI: window.location.href, 1504 request: null, 1505 isFrameSwitching: true, 1506 navigationStart: Date.now(), 1507 }); 1508 1509 this._windowDestroyed(this.window, { 1510 isFrozen: true, 1511 isFrameSwitching: true, 1512 }); 1513 } 1514 1515 // Immediately change the window as this window, if in process of unload 1516 // may already be non working on the next cycle and start throwing 1517 this._setWindow(window); 1518 1519 DevToolsUtils.executeSoon(() => { 1520 // No need to do anything more if the actor is destroyed. 1521 // e.g. the client has been closed and the actors destroyed in the meantime. 1522 if (this.isDestroyed()) { 1523 return; 1524 } 1525 1526 // Then fake window-ready and navigate on the given document 1527 this._windowReady(window, { isFrameSwitching: true }); 1528 DevToolsUtils.executeSoon(() => { 1529 this._navigate(window, true); 1530 }); 1531 }); 1532 } 1533 1534 _setWindow(window) { 1535 // Here is the very important call where we switch the currently targeted 1536 // window global (it will indirectly update this.window and many other 1537 // attributes defined from docShell). 1538 this.docShell = window.docShell; 1539 this.emit("changed-toplevel-document"); 1540 this.emit("frameUpdate", { 1541 selected: this.outerWindowID, 1542 }); 1543 } 1544 1545 /** 1546 * Handle location changes, by clearing the previous debuggees and enabling 1547 * debugging, which may have been disabled temporarily by the 1548 * DebuggerProgressListener. 1549 */ 1550 _windowReady(window, { isFrameSwitching, isBFCache } = {}) { 1551 if (this.ignoreSubFrames) { 1552 return; 1553 } 1554 const isTopLevel = window == this.window; 1555 1556 // We just reset iframe list on WillNavigate, so we now list all existing 1557 // frames when we load a new document in the original window 1558 if (window == this._originalWindow && !isFrameSwitching) { 1559 this._updateChildDocShells(); 1560 } 1561 1562 // If this follows WindowGlobal lifecycle, a new Target actor will be spawn for the top level 1563 // target document. Only notify about in-process iframes. 1564 // Note that OOP iframes won't emit window-ready and will also have their dedicated target. 1565 // Also, we allow window-ready to be fired for iframe switching of top level documents, 1566 // otherwise the iframe dropdown no longer works with server side targets. 1567 if (this.followWindowGlobalLifeCycle && isTopLevel && !isFrameSwitching) { 1568 return; 1569 } 1570 1571 this.emit("window-ready", { 1572 window, 1573 isTopLevel, 1574 isBFCache, 1575 id: getWindowID(window), 1576 isFrameSwitching, 1577 }); 1578 } 1579 1580 _windowDestroyed( 1581 window, 1582 { id = null, isFrozen = false, isFrameSwitching = false } 1583 ) { 1584 if (this.ignoreSubFrames) { 1585 return; 1586 } 1587 const isTopLevel = window == this.window; 1588 1589 // If this follows WindowGlobal lifecycle, this target will be destroyed, alongside its top level document. 1590 // Only notify about in-process iframes. 1591 // Note that OOP iframes won't emit window-ready and will also have their dedicated target. 1592 // Also, we allow window-destroyed to be fired for iframe switching of top level documents, 1593 // otherwise the iframe dropdown no longer works with server side targets. 1594 if (this.followWindowGlobalLifeCycle && isTopLevel && !isFrameSwitching) { 1595 return; 1596 } 1597 1598 this.emit("window-destroyed", { 1599 window, 1600 isTopLevel, 1601 id: id || getWindowID(window), 1602 isFrozen, 1603 }); 1604 } 1605 1606 /** 1607 * Start notifying server and client about a new document being loaded in the 1608 * currently targeted window global. 1609 */ 1610 _willNavigate({ 1611 window, 1612 newURI, 1613 request, 1614 isFrameSwitching = false, 1615 navigationStart, 1616 }) { 1617 if (this.ignoreSubFrames) { 1618 return; 1619 } 1620 let isTopLevel = window == this.window; 1621 1622 let reset = false; 1623 if (window == this._originalWindow && !isFrameSwitching) { 1624 // If the top level document changes and we are targeting an iframe, we 1625 // need to reset to the upcoming new top level document. But for this 1626 // will-navigate event, we will dispatch on the old window. (The inspector 1627 // codebase expect to receive will-navigate for the currently displayed 1628 // document in order to cleanup the markup view) 1629 if (this.window != this._originalWindow) { 1630 reset = true; 1631 window = this.window; 1632 isTopLevel = true; 1633 } 1634 } 1635 1636 // will-navigate event needs to be dispatched synchronously, by calling the 1637 // listeners in the order or registration. This event fires once navigation 1638 // starts, (all pending user prompts are dealt with), but before the first 1639 // request starts. 1640 this.emit("will-navigate", { 1641 window, 1642 isTopLevel, 1643 newURI, 1644 request, 1645 navigationStart, 1646 isFrameSwitching, 1647 }); 1648 1649 // We don't do anything for inner frames here. 1650 // (we will only update thread actor on window-ready) 1651 if (!isTopLevel) { 1652 return; 1653 } 1654 1655 // When the actor acts as a WindowGlobalTarget, will-navigate won't fired. 1656 // Instead we will receive a new top level target with isTargetSwitching=true. 1657 if (!this.followWindowGlobalLifeCycle) { 1658 this.emit("tabNavigated", { 1659 url: newURI, 1660 state: "start", 1661 isFrameSwitching, 1662 }); 1663 } 1664 1665 if (reset) { 1666 this._setWindow(this._originalWindow); 1667 } 1668 } 1669 1670 /** 1671 * Notify server and client about a new document done loading in the current 1672 * targeted window global. 1673 */ 1674 _navigate(window, isFrameSwitching = false) { 1675 if (this.ignoreSubFrames) { 1676 return; 1677 } 1678 const isTopLevel = window == this.window; 1679 1680 // navigate event needs to be dispatched synchronously, 1681 // by calling the listeners in the order or registration. 1682 // This event is fired once the document is loaded, 1683 // after the load event, it's document ready-state is 'complete'. 1684 this.emit("navigate", { 1685 window, 1686 isTopLevel, 1687 }); 1688 1689 // We don't do anything for inner frames here. 1690 // (we will only update thread actor on window-ready) 1691 if (!isTopLevel) { 1692 return; 1693 } 1694 1695 // We may still significate when the document is done loading, via navigate. 1696 // But as we no longer fire the "will-navigate", may be it is better to find 1697 // other ways to get to our means. 1698 // Listening to "navigate" is misleading as the document may already be loaded 1699 // if we just opened the DevTools. So it is better to use "watch" pattern 1700 // and instead have the actor either emit immediately resources as they are 1701 // already available, or later on as the load progresses. 1702 if (this.followWindowGlobalLifeCycle) { 1703 return; 1704 } 1705 1706 this.emit("tabNavigated", { 1707 url: this.url, 1708 title: this.title, 1709 state: "stop", 1710 isFrameSwitching, 1711 }); 1712 } 1713 1714 removeActorByName(name) { 1715 if (name in this._extraActors) { 1716 const actor = this._extraActors[name]; 1717 if (this._targetScopedActorPool.has(actor)) { 1718 this._targetScopedActorPool.removeActor(actor); 1719 } 1720 delete this._extraActors[name]; 1721 } 1722 } 1723 } 1724 1725 exports.WindowGlobalTargetActor = WindowGlobalTargetActor; 1726 1727 class DebuggerProgressListener { 1728 /** 1729 * The DebuggerProgressListener class is an nsIWebProgressListener which 1730 * handles onStateChange events for the targeted window global. If the user 1731 * tries to navigate away from a paused page, the listener makes sure that the 1732 * debuggee is resumed before the navigation begins. 1733 * 1734 * @param WindowGlobalTargetActor targetActor 1735 * The window global target actor associated with this listener. 1736 */ 1737 constructor(targetActor) { 1738 this._targetActor = targetActor; 1739 this._onWindowCreated = this.onWindowCreated.bind(this); 1740 this._onWindowHidden = this.onWindowHidden.bind(this); 1741 1742 // Watch for windows destroyed (global observer that will need filtering) 1743 Services.obs.addObserver(this, "inner-window-destroyed"); 1744 1745 // XXX: for now we maintain the list of windows we know about in this instance 1746 // so that we can discriminate windows we care about when observing 1747 // inner-window-destroyed events. Bug 1016952 would remove the need for this. 1748 this._knownWindowIDs = new Map(); 1749 1750 this._watchedDocShells = new WeakSet(); 1751 } 1752 1753 QueryInterface = ChromeUtils.generateQI([ 1754 "nsIWebProgressListener", 1755 "nsISupportsWeakReference", 1756 ]); 1757 1758 destroy() { 1759 Services.obs.removeObserver(this, "inner-window-destroyed"); 1760 this._knownWindowIDs.clear(); 1761 this._knownWindowIDs = null; 1762 } 1763 1764 watch(docShell) { 1765 // Add the docshell to the watched set. We're actually adding the window, 1766 // because docShell objects are not wrappercached and would be rejected 1767 // by the WeakSet. 1768 const docShellWindow = docShell.domWindow; 1769 this._watchedDocShells.add(docShellWindow); 1770 1771 const webProgress = docShell 1772 .QueryInterface(Ci.nsIInterfaceRequestor) 1773 .getInterface(Ci.nsIWebProgress); 1774 webProgress.addProgressListener( 1775 this, 1776 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW | 1777 Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT 1778 ); 1779 1780 const handler = getDocShellChromeEventHandler(docShell); 1781 handler.addEventListener("DOMWindowCreated", this._onWindowCreated, true); 1782 handler.addEventListener("pageshow", this._onWindowCreated, true); 1783 handler.addEventListener("pagehide", this._onWindowHidden, true); 1784 1785 // Dispatch the _windowReady event on the targetActor for pre-existing windows 1786 const windows = this._targetActor.ignoreSubFrames 1787 ? [docShellWindow] 1788 : this._getWindowsInDocShell(docShell); 1789 for (const win of windows) { 1790 this._targetActor._windowReady(win); 1791 this._knownWindowIDs.set(getWindowID(win), win); 1792 } 1793 1794 // Immediately enable CSS error reports on new top level docshells, if this was already enabled. 1795 // This is specific to MBT and WebExtension targets (so the isRootActor check). 1796 if ( 1797 this._targetActor.isRootActor && 1798 this._targetActor.docShell.cssErrorReportingEnabled 1799 ) { 1800 docShell.cssErrorReportingEnabled = true; 1801 } 1802 } 1803 1804 unwatch(docShell) { 1805 // If the docshell is being destroyed, we won't be able to retrieve its related window object, 1806 // which is the key ingredient for all cleanup operations done in this method. 1807 if (docShell.isBeingDestroyed()) { 1808 return; 1809 } 1810 1811 const docShellWindow = docShell.domWindow; 1812 if (!this._watchedDocShells.has(docShellWindow)) { 1813 return; 1814 } 1815 this._watchedDocShells.delete(docShellWindow); 1816 1817 const webProgress = docShell 1818 .QueryInterface(Ci.nsIInterfaceRequestor) 1819 .getInterface(Ci.nsIWebProgress); 1820 // During process shutdown, the docshell may already be cleaned up and throw 1821 try { 1822 webProgress.removeProgressListener(this); 1823 } catch (e) { 1824 // ignore 1825 } 1826 1827 const handler = getDocShellChromeEventHandler(docShell); 1828 handler.removeEventListener( 1829 "DOMWindowCreated", 1830 this._onWindowCreated, 1831 true 1832 ); 1833 handler.removeEventListener("pageshow", this._onWindowCreated, true); 1834 handler.removeEventListener("pagehide", this._onWindowHidden, true); 1835 1836 const windows = this._targetActor.ignoreSubFrames 1837 ? [docShellWindow] 1838 : this._getWindowsInDocShell(docShell); 1839 for (const win of windows) { 1840 this._knownWindowIDs.delete(getWindowID(win)); 1841 } 1842 } 1843 1844 _getWindowsInDocShell(docShell) { 1845 return getChildDocShells(docShell).map(d => { 1846 return d.domWindow; 1847 }); 1848 } 1849 1850 onWindowCreated = DevToolsUtils.makeInfallible(function (evt) { 1851 if (this._targetActor.isDestroyed()) { 1852 return; 1853 } 1854 1855 // If we're in a frame swap (which occurs when toggling RDM, for example), then we can 1856 // ignore this event, as the window never really went anywhere for our purposes. 1857 if (evt.inFrameSwap) { 1858 return; 1859 } 1860 1861 const window = evt.target.defaultView; 1862 if (!window) { 1863 // Some old UIs might emit unrelated events called pageshow/pagehide on 1864 // elements which are not documents. Bail in this case. See Bug 1669666. 1865 return; 1866 } 1867 1868 const innerID = getWindowID(window); 1869 1870 // This handler is called for two events: "DOMWindowCreated" and "pageshow". 1871 // Bail out if we already processed this window. 1872 if (this._knownWindowIDs.has(innerID)) { 1873 return; 1874 } 1875 this._knownWindowIDs.set(innerID, window); 1876 1877 // For a regular page navigation, "DOMWindowCreated" is fired before 1878 // "pageshow". If the current event is "pageshow" but we have not processed 1879 // the window yet, it means this is a BF cache navigation. In theory, 1880 // `event.persisted` should be set for BF cache navigation events, but it is 1881 // not always available, so we fallback on checking if "pageshow" is the 1882 // first event received for a given window (see Bug 1378133). 1883 const isBFCache = evt.type == "pageshow"; 1884 1885 this._targetActor._windowReady(window, { isBFCache }); 1886 }, "DebuggerProgressListener.prototype.onWindowCreated"); 1887 1888 onWindowHidden = DevToolsUtils.makeInfallible(function (evt) { 1889 if (this._targetActor.isDestroyed()) { 1890 return; 1891 } 1892 1893 // If we're in a frame swap (which occurs when toggling RDM, for example), then we can 1894 // ignore this event, as the window isn't really going anywhere for our purposes. 1895 if (evt.inFrameSwap) { 1896 return; 1897 } 1898 1899 // Only act as if the window has been destroyed if the 'pagehide' event 1900 // was sent for a persisted window (persisted is set when the page is put 1901 // and frozen in the bfcache). If the page isn't persisted, the observer's 1902 // inner-window-destroyed event will handle it. 1903 if (!evt.persisted) { 1904 return; 1905 } 1906 1907 const window = evt.target.defaultView; 1908 if (!window) { 1909 // Some old UIs might emit unrelated events called pageshow/pagehide on 1910 // elements which are not documents. Bail in this case. See Bug 1669666. 1911 return; 1912 } 1913 1914 this._targetActor._windowDestroyed(window, { isFrozen: true }); 1915 this._knownWindowIDs.delete(getWindowID(window)); 1916 }, "DebuggerProgressListener.prototype.onWindowHidden"); 1917 1918 observe = DevToolsUtils.makeInfallible(function (subject) { 1919 if (this._targetActor.isDestroyed()) { 1920 return; 1921 } 1922 1923 // Because this observer will be called for all inner-window-destroyed in 1924 // the application, we need to filter out events for windows we are not 1925 // watching 1926 const innerID = subject.QueryInterface(Ci.nsISupportsPRUint64).data; 1927 const window = this._knownWindowIDs.get(innerID); 1928 if (window) { 1929 this._knownWindowIDs.delete(innerID); 1930 this._targetActor._windowDestroyed(window, { id: innerID }); 1931 } 1932 1933 // Bug 1598364: when debugging browser.xhtml from the Browser Toolbox 1934 // the DOMWindowCreated/pageshow/pagehide event listeners have to be 1935 // re-registered against the next document when we reload browser.html 1936 // (or navigate to another doc). 1937 // That's because we registered the listener on docShell.domWindow as 1938 // top level windows don't have a chromeEventHandler. 1939 if ( 1940 this._watchedDocShells.has(window) && 1941 // Avoid exception when the notified window is a cross origin object 1942 // (most likely an iframe running in a distinct origin) 1943 !Cu.isRemoteProxy(window) && 1944 window.docShell && 1945 !window.docShell.chromeEventHandler 1946 ) { 1947 // First cleanup all the existing listeners 1948 this.unwatch(window.docShell); 1949 // Re-register new ones. The docShell is already referencing the new document. 1950 this.watch(window.docShell); 1951 } 1952 }, "DebuggerProgressListener.prototype.observe"); 1953 1954 onStateChange = DevToolsUtils.makeInfallible(function ( 1955 progress, 1956 request, 1957 flag 1958 ) { 1959 if (this._targetActor.isDestroyed()) { 1960 return; 1961 } 1962 progress.QueryInterface(Ci.nsIDocShell); 1963 if (progress.isBeingDestroyed()) { 1964 return; 1965 } 1966 1967 const isStart = flag & Ci.nsIWebProgressListener.STATE_START; 1968 const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP; 1969 const isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; 1970 const isWindow = flag & Ci.nsIWebProgressListener.STATE_IS_WINDOW; 1971 1972 // Ideally, we would fetch navigationStart from window.performance.timing.navigationStart 1973 // but as WindowGlobal isn't instantiated yet we don't have access to it. 1974 // This is ultimately handed over to DocumentEventListener, which uses this. 1975 // See its comment about WILL_NAVIGATE_TIME_SHIFT for more details about the related workaround. 1976 const navigationStart = Date.now(); 1977 1978 // Catch any iframe location change 1979 if (isDocument && isStop) { 1980 // Watch document stop to ensure having the new iframe url. 1981 this._targetActor._notifyDocShellsUpdate([progress]); 1982 } 1983 1984 const window = progress.DOMWindow; 1985 if (isDocument && isStart) { 1986 // One of the earliest events that tells us a new URI 1987 // is being loaded in this window. 1988 const newURI = request instanceof Ci.nsIChannel ? request.URI.spec : null; 1989 this._targetActor._willNavigate({ 1990 window, 1991 newURI, 1992 request, 1993 isFrameSwitching: false, 1994 navigationStart, 1995 }); 1996 } 1997 if (isWindow && isStop) { 1998 // Don't dispatch "navigate" event just yet when there is a redirect to 1999 // about:neterror page. 2000 // Navigating to about:neterror will make `status` be something else than NS_OK. 2001 // But for some error like NS_BINDING_ABORTED we don't want to emit any `navigate` 2002 // event as the page load has been cancelled and the related page document is going 2003 // to be a dead wrapper. 2004 if ( 2005 request.status != Cr.NS_OK && 2006 request.status != Cr.NS_BINDING_ABORTED 2007 ) { 2008 // Instead, listen for DOMContentLoaded as about:neterror is loaded 2009 // with LOAD_BACKGROUND flags and never dispatches load event. 2010 // That may be the same reason why there is no onStateChange event 2011 // for about:neterror loads. 2012 const handler = getDocShellChromeEventHandler(progress); 2013 const onLoad = evt => { 2014 // Ignore events from iframes 2015 if (evt.target === window.document) { 2016 handler.removeEventListener("DOMContentLoaded", onLoad, true); 2017 this._targetActor._navigate(window); 2018 } 2019 }; 2020 handler.addEventListener("DOMContentLoaded", onLoad, true); 2021 } else { 2022 // Somewhat equivalent of load event. 2023 // (window.document.readyState == complete) 2024 this._targetActor._navigate(window); 2025 } 2026 } 2027 }, "DebuggerProgressListener.prototype.onStateChange"); 2028 }