thread.js (74476B)
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 const { Actor } = require("resource://devtools/shared/protocol/Actor.js"); 12 const { Pool } = require("resource://devtools/shared/protocol/Pool.js"); 13 const { threadSpec } = require("resource://devtools/shared/specs/thread.js"); 14 15 const { 16 createValueGrip, 17 } = require("resource://devtools/server/actors/object/utils.js"); 18 const { 19 ObjectActorPool, 20 } = require("resource://devtools/server/actors/object/ObjectActorPool.js"); 21 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 22 const Debugger = require("Debugger"); 23 const { assert, dumpn, reportException } = DevToolsUtils; 24 const { 25 getAvailableEventBreakpoints, 26 eventBreakpointForNotification, 27 eventsRequireNotifications, 28 firstStatementBreakpointId, 29 makeEventBreakpointMessage, 30 } = require("resource://devtools/server/actors/utils/event-breakpoints.js"); 31 const { 32 WatchpointMap, 33 } = require("resource://devtools/server/actors/utils/watchpoint-map.js"); 34 35 const Targets = require("devtools/server/actors/targets/index"); 36 37 loader.lazyRequireGetter( 38 this, 39 "logEvent", 40 "resource://devtools/server/actors/utils/logEvent.js", 41 true 42 ); 43 loader.lazyRequireGetter( 44 this, 45 "EnvironmentActor", 46 "resource://devtools/server/actors/environment.js", 47 true 48 ); 49 loader.lazyRequireGetter( 50 this, 51 "BreakpointActorMap", 52 "resource://devtools/server/actors/utils/breakpoint-actor-map.js", 53 true 54 ); 55 loader.lazyRequireGetter( 56 this, 57 "EventLoop", 58 "resource://devtools/server/actors/utils/event-loop.js", 59 true 60 ); 61 loader.lazyRequireGetter( 62 this, 63 ["FrameActor", "getSavedFrameParent", "isValidSavedFrame"], 64 "resource://devtools/server/actors/frame.js", 65 true 66 ); 67 loader.lazyRequireGetter( 68 this, 69 "HighlighterEnvironment", 70 "resource://devtools/server/actors/highlighters.js", 71 true 72 ); 73 loader.lazyRequireGetter( 74 this, 75 "PausedDebuggerOverlay", 76 "resource://devtools/server/actors/highlighters/paused-debugger.js", 77 true 78 ); 79 80 const PROMISE_REACTIONS = new WeakMap(); 81 function cacheReactionsForFrame(frame) { 82 if (frame.asyncPromise) { 83 const reactions = frame.asyncPromise.getPromiseReactions(); 84 const existingReactions = PROMISE_REACTIONS.get(frame.asyncPromise); 85 if ( 86 reactions.length && 87 (!existingReactions || reactions.length > existingReactions.length) 88 ) { 89 PROMISE_REACTIONS.set(frame.asyncPromise, reactions); 90 } 91 } 92 } 93 94 function createStepForReactionTracking(onStep) { 95 return function () { 96 cacheReactionsForFrame(this); 97 return onStep ? onStep.apply(this, arguments) : undefined; 98 }; 99 } 100 101 const getAsyncParentFrame = frame => { 102 if (!frame.asyncPromise) { 103 return null; 104 } 105 106 // We support returning Frame actors for frames that are suspended 107 // at an 'await', and here we want to walk upward to look for the first 108 // frame that will be resumed when the current frame's promise resolves. 109 let reactions = 110 PROMISE_REACTIONS.get(frame.asyncPromise) || 111 frame.asyncPromise.getPromiseReactions(); 112 113 // eslint-disable-next-line no-constant-condition 114 while (true) { 115 // We loop here because we may have code like: 116 // 117 // async function inner(){ debugger; } 118 // 119 // async function outer() { 120 // await Promise.resolve().then(() => inner()); 121 // } 122 // 123 // where we can see that when `inner` resolves, we will resume from 124 // `outer`, even though there is a layer of promises between, and 125 // that layer could be any number of promises deep. 126 if (!(reactions[0] instanceof Debugger.Object)) { 127 break; 128 } 129 130 reactions = reactions[0].getPromiseReactions(); 131 } 132 133 if (reactions[0] instanceof Debugger.Frame) { 134 return reactions[0]; 135 } 136 return null; 137 }; 138 const RESTARTED_FRAMES = new WeakSet(); 139 140 // Thread actor possible states: 141 const STATES = { 142 // Before ThreadActor.attach is called: 143 DETACHED: "detached", 144 // After the actor is destroyed: 145 EXITED: "exited", 146 147 // States possible in between DETACHED AND EXITED: 148 // Default state, when the thread isn't paused, 149 RUNNING: "running", 150 // When paused on any type of breakpoint, or, when the client requested an interrupt. 151 PAUSED: "paused", 152 }; 153 exports.STATES = STATES; 154 155 // Possible values for the `why.type` attribute in "paused" event 156 const PAUSE_REASONS = { 157 ALREADY_PAUSED: "alreadyPaused", 158 INTERRUPTED: "interrupted", // Associated with why.onNext attribute 159 MUTATION_BREAKPOINT: "mutationBreakpoint", // Associated with why.mutationType and why.message attributes 160 DEBUGGER_STATEMENT: "debuggerStatement", 161 EXCEPTION: "exception", 162 XHR: "XHR", 163 EVENT_BREAKPOINT: "eventBreakpoint", 164 RESUME_LIMIT: "resumeLimit", 165 }; 166 exports.PAUSE_REASONS = PAUSE_REASONS; 167 168 class ThreadActor extends Actor { 169 /** 170 * Creates a ThreadActor. 171 * 172 * ThreadActors manage execution/inspection of debuggees. 173 * 174 * @param {TargetActor} targetActor 175 * This `ThreadActor`'s parent actor. i.e. one of the many Target actors. 176 */ 177 constructor(targetActor) { 178 super(targetActor.conn, threadSpec); 179 180 // This attribute is used by various other actors to find the target actor 181 this.targetActor = targetActor; 182 183 this._state = STATES.DETACHED; 184 this._options = { 185 skipBreakpoints: false, 186 }; 187 this._gripDepth = 0; 188 this._targetActorClosed = false; 189 this._observingNetwork = false; 190 this._shouldShowPauseOverlay = true; 191 this._frameActors = []; 192 this._xhrBreakpoints = []; 193 194 this._dbg = null; 195 this._threadLifetimePool = null; 196 this._activeEventPause = null; 197 this._pauseOverlay = null; 198 this._priorPause = null; 199 200 this._activeEventBreakpoints = new Set(); 201 this._frameActorMap = new WeakMap(); 202 this._debuggerSourcesSeen = new WeakSet(); 203 204 // A Set of URLs string to watch for when new sources are found by 205 // the debugger instance. 206 this._onLoadBreakpointURLs = new Set(); 207 208 // A WeakMap from Debugger.Frame to an exception value which will be ignored 209 // when deciding to pause if the value is thrown by the frame. When we are 210 // pausing on exceptions then we only want to pause when the youngest frame 211 // throws a particular exception, instead of for all older frames as well. 212 this._handledFrameExceptions = new WeakMap(); 213 214 this._watchpointsMap = new WatchpointMap(this); 215 216 this.breakpointActorMap = new BreakpointActorMap(this); 217 218 this._nestedEventLoop = new EventLoop({ 219 thread: this, 220 }); 221 222 this.onNewSourceEvent = this.onNewSourceEvent.bind(this); 223 224 this.createCompletionGrip = this.createCompletionGrip.bind(this); 225 this.onDebuggerStatement = this.onDebuggerStatement.bind(this); 226 this.onNewScript = this.onNewScript.bind(this); 227 this._onOpeningRequest = this._onOpeningRequest.bind(this); 228 this._onNewDebuggee = this._onNewDebuggee.bind(this); 229 this._onExceptionUnwind = this._onExceptionUnwind.bind(this); 230 this._eventBreakpointListener = this._eventBreakpointListener.bind(this); 231 this._onWindowReady = this._onWindowReady.bind(this); 232 this._onWillNavigate = this._onWillNavigate.bind(this); 233 this._onNavigate = this._onNavigate.bind(this); 234 235 this.targetActor.on("window-ready", this._onWindowReady); 236 this.targetActor.on("will-navigate", this._onWillNavigate); 237 this.targetActor.on("navigate", this._onNavigate); 238 239 this._firstStatementBreakpoint = null; 240 this._debuggerNotificationObserver = new DebuggerNotificationObserver(); 241 } 242 243 // Used by the ObjectActor to keep track of the depth of grip() calls. 244 _gripDepth = null; 245 246 get dbg() { 247 if (!this._dbg) { 248 this._dbg = this.targetActor.dbg; 249 // Keep the debugger disabled until a client attaches. 250 if (this._state === STATES.DETACHED) { 251 this._dbg.disable(); 252 } else { 253 this._dbg.enable(); 254 } 255 } 256 return this._dbg; 257 } 258 259 // Current state of the thread actor: 260 // - detached: state, before ThreadActor.attach is called, 261 // - exited: state, after the actor is destroyed, 262 // States possible in between these two states: 263 // - running: default state, when the thread isn't paused, 264 // - paused: state, when paused on any type of breakpoint, or, when the client requested an interrupt. 265 get state() { 266 return this._state; 267 } 268 269 // XXX: soon to be equivalent to !isDestroyed once the thread actor is initialized on target creation. 270 get attached() { 271 return this.state == STATES.RUNNING || this.state == STATES.PAUSED; 272 } 273 274 get pauseLifetimePool() { 275 return this._pausePool; 276 } 277 278 get threadLifetimePool() { 279 if (!this._threadLifetimePool) { 280 this._threadLifetimePool = new ObjectActorPool(this, "thread", true); 281 this._threadLifetimePool.objectActors = new WeakMap(); 282 } 283 return this._threadLifetimePool; 284 } 285 286 getThreadLifetimeObject(raw) { 287 return this.threadLifetimePool.objectActors.get(raw); 288 } 289 290 promoteObjectToThreadLifetime(objectActor) { 291 this.threadLifetimePool.manage(objectActor); 292 this.threadLifetimePool.objectActors.set(objectActor.obj, objectActor); 293 } 294 295 get sourcesManager() { 296 return this.targetActor.sourcesManager; 297 } 298 299 get breakpoints() { 300 return this.targetActor.breakpoints; 301 } 302 303 get youngestFrame() { 304 if (this.state != STATES.PAUSED) { 305 return null; 306 } 307 return this.dbg.getNewestFrame(); 308 } 309 310 get shouldSkipAnyBreakpoint() { 311 return ( 312 // Disable all types of breakpoints if: 313 // - the user explicitly requested it via the option 314 this._options.skipBreakpoints || 315 // - or when we are evaluating some javascript via the console actor and disableBreaks 316 // has been set to true (which happens for most evaluating except the console input) 317 this.insideClientEvaluation?.disableBreaks 318 ); 319 } 320 321 isPaused() { 322 return this._state === STATES.PAUSED; 323 } 324 325 lastPausedPacket() { 326 return this._priorPause; 327 } 328 329 /** 330 * Remove all debuggees and clear out the thread's sources. 331 */ 332 clearDebuggees() { 333 if (this._dbg) { 334 this.dbg.removeAllDebuggees(); 335 } 336 } 337 338 /** 339 * Destroy the debugger and put the actor in the exited state. 340 * 341 * As part of destroy, we: clean up listeners, debuggees and 342 * clear actor pools associated with the lifetime of this actor. 343 */ 344 destroy() { 345 dumpn("in ThreadActor.prototype.destroy"); 346 if (this._state == STATES.PAUSED) { 347 this.doResume(); 348 } 349 350 this.removeAllWatchpoints(); 351 this._xhrBreakpoints = []; 352 this._updateNetworkObserver(); 353 354 this._activeEventBreakpoints = new Set(); 355 this._debuggerNotificationObserver.removeListener( 356 this._eventBreakpointListener 357 ); 358 359 for (const global of this.dbg.getDebuggees()) { 360 try { 361 this._debuggerNotificationObserver.disconnect( 362 global.unsafeDereference() 363 ); 364 } catch (e) {} 365 } 366 367 this.targetActor.off("window-ready", this._onWindowReady); 368 this.targetActor.off("will-navigate", this._onWillNavigate); 369 this.targetActor.off("navigate", this._onNavigate); 370 371 this.sourcesManager.off("newSource", this.onNewSourceEvent); 372 this.clearDebuggees(); 373 this._threadLifetimePool.destroy(); 374 this._threadLifetimePool = null; 375 this._dbg = null; 376 this._state = STATES.EXITED; 377 378 super.destroy(); 379 } 380 381 /** 382 * Tells if the thread actor has been initialized/attached on target creation 383 * by the server codebase. (And not late, from the frontend, by the TargetMixinFront class) 384 */ 385 isAttached() { 386 return !!this.alreadyAttached; 387 } 388 389 // Request handlers 390 attach(options) { 391 // Note that the client avoids trying to call attach if already attached. 392 // But just in case, avoid any possible duplicate call to attach. 393 if (this.alreadyAttached) { 394 return; 395 } 396 397 if (this.state === STATES.EXITED) { 398 throw { 399 error: "exited", 400 message: "threadActor has exited", 401 }; 402 } 403 404 if (this.state !== STATES.DETACHED) { 405 throw { 406 error: "wrongState", 407 message: "Current state is " + this.state, 408 }; 409 } 410 411 this.dbg.onDebuggerStatement = this.onDebuggerStatement; 412 this.dbg.onNewScript = this.onNewScript; 413 this.dbg.onNewDebuggee = this._onNewDebuggee; 414 415 this.sourcesManager.on("newSource", this.onNewSourceEvent); 416 417 this.reconfigure(options); 418 419 // Switch state from DETACHED to RUNNING 420 this._state = STATES.RUNNING; 421 422 this.alreadyAttached = true; 423 this.dbg.enable(); 424 425 if (Services.obs) { 426 // Set a wrappedJSObject property so |this| can be sent via the observer service 427 // for the xpcshell harness. 428 this.wrappedJSObject = this; 429 Services.obs.notifyObservers(this, "devtools-thread-ready"); 430 } 431 } 432 433 toggleEventLogging(logEventBreakpoints) { 434 this._options.logEventBreakpoints = logEventBreakpoints; 435 return this._options.logEventBreakpoints; 436 } 437 438 get pauseOverlay() { 439 if (this._pauseOverlay) { 440 return this._pauseOverlay; 441 } 442 443 const env = new HighlighterEnvironment(); 444 env.initFromTargetActor(this.targetActor); 445 const highlighter = new PausedDebuggerOverlay(env, { 446 resume: () => this.resume(null), 447 stepOver: () => this.resume({ type: "next" }), 448 }); 449 this._pauseOverlay = highlighter; 450 return highlighter; 451 } 452 453 _canShowOverlay() { 454 // Only attempt to show on overlay on WindowGlobal targets, which displays a document. 455 // Workers and content processes can't display any overlay. 456 if (this.targetActor.targetType != Targets.TYPES.FRAME) { 457 return false; 458 } 459 460 const { window } = this.targetActor; 461 462 // The CanvasFrameAnonymousContentHelper class we're using to create the paused overlay 463 // need to have access to a documentElement. 464 // We might have access to a non-chrome window getter that is a Sandox (e.g. in the 465 // case of ContentProcessTargetActor). 466 if (!window?.document?.documentElement) { 467 return false; 468 } 469 470 // Ignore privileged document (top level window, special about:* pages, …). 471 if (window.isChromeWindow) { 472 return false; 473 } 474 475 return true; 476 } 477 478 async showOverlay() { 479 if ( 480 !this._shouldShowPauseOverlay || 481 !this.isPaused() || 482 !this._canShowOverlay() 483 ) { 484 return; 485 } 486 487 const reason = this._priorPause.why.type; 488 await this.pauseOverlay.isReady; 489 490 // we might not be paused anymore. 491 if (!this.isPaused()) { 492 return; 493 } 494 495 this.pauseOverlay.show(reason); 496 } 497 498 hideOverlay() { 499 if (this._canShowOverlay() && this._pauseOverlay) { 500 this.pauseOverlay.hide(); 501 } 502 } 503 504 /** 505 * Tell the thread to automatically add a breakpoint on the first line of 506 * a given file, when it is first loaded. 507 * 508 * This is currently only used by the xpcshell test harness, and unless 509 * we decide to expand the scope of this feature, we should keep it that way. 510 */ 511 setBreakpointOnLoad(urls) { 512 this._onLoadBreakpointURLs = new Set(urls); 513 } 514 515 _findXHRBreakpointIndex(p, m) { 516 return this._xhrBreakpoints.findIndex( 517 ({ path, method }) => path === p && method === m 518 ); 519 } 520 521 // We clear the priorPause field when a breakpoint is added or removed 522 // at the same location because we are no longer worried about pausing twice 523 // at that location (e.g. debugger statement, stepping). 524 _maybeClearPriorPause(location) { 525 if (!this._priorPause) { 526 return; 527 } 528 529 const { where } = this._priorPause.frame; 530 if (where.line === location.line && where.column === location.column) { 531 this._priorPause = null; 532 } 533 } 534 535 async setBreakpoint(location, options) { 536 // Automatically initialize the thread actor if it wasn't yet done. 537 // Note that ideally, it should rather be done via reconfigure/thread configuration. 538 if (this._state === STATES.DETACHED) { 539 this.attach({}); 540 this.addAllSources(); 541 } 542 543 let actor = this.breakpointActorMap.get(location); 544 // Avoid resetting the exact same breakpoint twice 545 if (actor && JSON.stringify(actor.options) == JSON.stringify(options)) { 546 return; 547 } 548 if (!actor) { 549 actor = this.breakpointActorMap.getOrCreateBreakpointActor(location); 550 } 551 actor.setOptions(options); 552 this._maybeClearPriorPause(location); 553 554 if (location.sourceUrl) { 555 // There can be multiple source actors for a URL if there are multiple 556 // inline sources on an HTML page. 557 const sourceActors = this.sourcesManager.getSourceActorsByURL( 558 location.sourceUrl 559 ); 560 for (const sourceActor of sourceActors) { 561 await sourceActor.applyBreakpoint(actor); 562 } 563 } else { 564 const sourceActor = this.sourcesManager.getSourceActorById( 565 location.sourceId 566 ); 567 if (sourceActor) { 568 await sourceActor.applyBreakpoint(actor); 569 } 570 } 571 } 572 573 removeBreakpoint(location) { 574 const actor = this.breakpointActorMap.getOrCreateBreakpointActor(location); 575 this._maybeClearPriorPause(location); 576 actor.delete(); 577 } 578 579 removeAllXHRBreakpoints() { 580 this._xhrBreakpoints = []; 581 return this._updateNetworkObserver(); 582 } 583 584 removeXHRBreakpoint(path, method) { 585 const index = this._findXHRBreakpointIndex(path, method); 586 587 if (index >= 0) { 588 this._xhrBreakpoints.splice(index, 1); 589 } 590 return this._updateNetworkObserver(); 591 } 592 593 setXHRBreakpoint(path, method) { 594 // request.path is a string, 595 // If requested url contains the path, then we pause. 596 const index = this._findXHRBreakpointIndex(path, method); 597 598 if (index === -1) { 599 this._xhrBreakpoints.push({ path, method }); 600 } 601 return this._updateNetworkObserver(); 602 } 603 604 getAvailableEventBreakpoints() { 605 return getAvailableEventBreakpoints(this.targetActor.targetGlobal); 606 } 607 getActiveEventBreakpoints() { 608 return Array.from(this._activeEventBreakpoints); 609 } 610 611 /** 612 * Add event breakpoints to the list of active event breakpoints 613 * 614 * @param {Array<string>} ids: events to add (e.g. ["event.mouse.click","event.mouse.mousedown"]) 615 */ 616 addEventBreakpoints(ids) { 617 this.setActiveEventBreakpoints( 618 this.getActiveEventBreakpoints().concat(ids) 619 ); 620 } 621 622 /** 623 * Remove event breakpoints from the list of active event breakpoints 624 * 625 * @param {Array<string>} ids: events to remove (e.g. ["event.mouse.click","event.mouse.mousedown"]) 626 */ 627 removeEventBreakpoints(ids) { 628 this.setActiveEventBreakpoints( 629 this.getActiveEventBreakpoints().filter(eventBp => !ids.includes(eventBp)) 630 ); 631 } 632 633 /** 634 * Set the the list of active event breakpoints 635 * 636 * @param {Array<string>} ids: events to add breakpoint for (e.g. ["event.mouse.click","event.mouse.mousedown"]) 637 */ 638 setActiveEventBreakpoints(ids) { 639 this._activeEventBreakpoints = new Set(ids); 640 641 if (eventsRequireNotifications(ids)) { 642 this._debuggerNotificationObserver.addListener( 643 this._eventBreakpointListener 644 ); 645 } else { 646 this._debuggerNotificationObserver.removeListener( 647 this._eventBreakpointListener 648 ); 649 } 650 651 if (this._activeEventBreakpoints.has(firstStatementBreakpointId())) { 652 this._ensureFirstStatementBreakpointInitialized(); 653 654 this._firstStatementBreakpoint.hit = frame => 655 this._pauseAndRespondEventBreakpoint( 656 frame, 657 firstStatementBreakpointId() 658 ); 659 } else if (this._firstStatementBreakpoint) { 660 // Disabling the breakpoint disables the feature as much as we need it 661 // to. We do not bother removing breakpoints from the scripts themselves 662 // here because the breakpoints will be a no-op if `hit` is `null`, and 663 // if we wanted to remove them, we'd need a way to iterate through them 664 // all, which would require us to hold strong references to them, which 665 // just isn't needed. Plus, if the user disables and then re-enables the 666 // feature again later, the breakpoints will still be there to work. 667 this._firstStatementBreakpoint.hit = null; 668 } 669 } 670 671 _ensureFirstStatementBreakpointInitialized() { 672 if (this._firstStatementBreakpoint) { 673 return; 674 } 675 676 this._firstStatementBreakpoint = { hit: null }; 677 for (const script of this.dbg.findScripts()) { 678 this._maybeTrackFirstStatementBreakpoint(script); 679 } 680 } 681 682 _maybeTrackFirstStatementBreakpointForNewGlobal(global) { 683 if (this._firstStatementBreakpoint) { 684 for (const script of this.dbg.findScripts({ global })) { 685 this._maybeTrackFirstStatementBreakpoint(script); 686 } 687 } 688 } 689 690 _maybeTrackFirstStatementBreakpoint(script) { 691 if ( 692 // If the feature is not enabled yet, there is nothing to do. 693 !this._firstStatementBreakpoint || 694 // WASM files don't have a first statement. 695 script.format !== "js" || 696 // All "top-level" scripts are non-functions, whether that's because 697 // the script is a module, a global script, or an eval or what. 698 script.isFunction 699 ) { 700 return; 701 } 702 703 const bps = script.getPossibleBreakpoints(); 704 705 // Scripts aren't guaranteed to have a step start if for instance the 706 // file contains only function declarations, so in that case we try to 707 // fall back to whatever we can find. 708 let meta = bps.find(bp => bp.isStepStart) || bps[0]; 709 if (!meta) { 710 // We've tried to avoid using `getAllColumnOffsets()` because the set of 711 // locations included in this list is very under-defined, but for this 712 // usecase it's not the end of the world. Maybe one day we could have an 713 // "onEnterFrame" that was scoped to a specific script to avoid this. 714 meta = script.getAllColumnOffsets()[0]; 715 } 716 717 if (!meta) { 718 // Not certain that this is actually possible, but including for sanity 719 // so that we don't throw unexpectedly. 720 return; 721 } 722 script.setBreakpoint(meta.offset, this._firstStatementBreakpoint); 723 } 724 725 _onNewDebuggee(global) { 726 this._maybeTrackFirstStatementBreakpointForNewGlobal(global); 727 try { 728 this._debuggerNotificationObserver.connect(global.unsafeDereference()); 729 } catch (e) {} 730 } 731 732 _updateNetworkObserver() { 733 // Workers don't have access to `Services` and even if they did, network 734 // requests are all dispatched to the main thread, so there would be 735 // nothing here to listen for. We'll need to revisit implementing 736 // XHR breakpoints for workers. 737 if (isWorker) { 738 return false; 739 } 740 741 if (this._xhrBreakpoints.length && !this._observingNetwork) { 742 this._observingNetwork = true; 743 Services.obs.addObserver( 744 this._onOpeningRequest, 745 "http-on-opening-request" 746 ); 747 } else if (this._xhrBreakpoints.length === 0 && this._observingNetwork) { 748 this._observingNetwork = false; 749 Services.obs.removeObserver( 750 this._onOpeningRequest, 751 "http-on-opening-request" 752 ); 753 } 754 755 return true; 756 } 757 758 _onOpeningRequest(subject) { 759 if (this.shouldSkipAnyBreakpoint) { 760 return; 761 } 762 763 const channel = subject.QueryInterface(Ci.nsIHttpChannel); 764 const url = channel.URI.asciiSpec; 765 const requestMethod = channel.requestMethod; 766 767 let causeType = Ci.nsIContentPolicy.TYPE_OTHER; 768 if (channel.loadInfo) { 769 causeType = channel.loadInfo.externalContentPolicyType; 770 } 771 772 const isXHR = 773 causeType === Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST || 774 causeType === Ci.nsIContentPolicy.TYPE_FETCH; 775 776 if (!isXHR) { 777 // We currently break only if the request is either fetch or xhr 778 return; 779 } 780 781 let shouldPause = false; 782 for (const { path, method } of this._xhrBreakpoints) { 783 if (method !== "ANY" && method !== requestMethod) { 784 continue; 785 } 786 if (url.includes(path)) { 787 shouldPause = true; 788 break; 789 } 790 } 791 792 if (shouldPause) { 793 const frame = this.dbg.getNewestFrame(); 794 795 // If there is no frame, this request was dispatched by logic that isn't 796 // primarily JS, so pausing the event loop wouldn't make sense. 797 // This covers background requests like loading the initial page document, 798 // or loading favicons. This also includes requests dispatched indirectly 799 // from workers. We'll need to handle them separately in the future. 800 if (frame) { 801 this._pauseAndRespond(frame, { type: PAUSE_REASONS.XHR }); 802 } 803 } 804 } 805 806 reconfigure(options = {}) { 807 if (this.state == STATES.EXITED) { 808 throw { 809 error: "wrongState", 810 }; 811 } 812 this._options = { ...this._options, ...options }; 813 814 if ("observeAsmJS" in options) { 815 this.dbg.allowUnobservedAsmJS = !options.observeAsmJS; 816 } 817 if ("observeWasm" in options) { 818 this.dbg.allowUnobservedWasm = !options.observeWasm; 819 } 820 if ("pauseOverlay" in options) { 821 this._shouldShowPauseOverlay = !!options.pauseOverlay; 822 if (this.isPaused()) { 823 if (!this._shouldShowPauseOverlay) { 824 this.hideOverlay(); 825 } else { 826 this.showOverlay(); 827 } 828 } 829 } 830 831 if ( 832 "pauseWorkersUntilAttach" in options && 833 this.targetActor.pauseWorkersUntilAttach 834 ) { 835 this.targetActor.pauseWorkersUntilAttach(options.pauseWorkersUntilAttach); 836 } 837 838 if (options.breakpoints) { 839 for (const breakpoint of Object.values(options.breakpoints)) { 840 this.setBreakpoint(breakpoint.location, breakpoint.options); 841 } 842 } 843 844 if (options.eventBreakpoints) { 845 this.setActiveEventBreakpoints(options.eventBreakpoints); 846 } 847 848 // Only consider this options if an explicit boolean value is passed. 849 if (typeof this._options.shouldPauseOnDebuggerStatement == "boolean") { 850 this.setPauseOnDebuggerStatement( 851 this._options.shouldPauseOnDebuggerStatement 852 ); 853 } 854 this.setPauseOnExceptions(this._options.pauseOnExceptions); 855 } 856 857 _eventBreakpointListener(notification) { 858 if (this._state === STATES.PAUSED || this._state === STATES.DETACHED) { 859 return; 860 } 861 862 const eventBreakpoint = eventBreakpointForNotification( 863 this.dbg, 864 notification 865 ); 866 867 if (!this._activeEventBreakpoints.has(eventBreakpoint)) { 868 return; 869 } 870 871 if (notification.phase === "pre" && !this._activeEventPause) { 872 this._activeEventPause = this._captureDebuggerHooks(); 873 874 this.dbg.onEnterFrame = 875 this._makeEventBreakpointEnterFrame(eventBreakpoint); 876 } else if (notification.phase === "post" && this._activeEventPause) { 877 this._restoreDebuggerHooks(this._activeEventPause); 878 this._activeEventPause = null; 879 } else if (!notification.phase && !this._activeEventPause) { 880 const frame = this.dbg.getNewestFrame(); 881 if (frame) { 882 if (this.sourcesManager.isFrameBlackBoxed(frame)) { 883 return; 884 } 885 886 this._pauseAndRespondEventBreakpoint(frame, eventBreakpoint); 887 } 888 } 889 } 890 891 _makeEventBreakpointEnterFrame(eventBreakpoint) { 892 return frame => { 893 if (this.sourcesManager.isFrameBlackBoxed(frame)) { 894 return undefined; 895 } 896 897 this._restoreDebuggerHooks(this._activeEventPause); 898 this._activeEventPause = null; 899 900 return this._pauseAndRespondEventBreakpoint(frame, eventBreakpoint); 901 }; 902 } 903 904 _pauseAndRespondEventBreakpoint(frame, eventBreakpoint) { 905 if (this.shouldSkipAnyBreakpoint) { 906 return undefined; 907 } 908 909 if (this._options.logEventBreakpoints) { 910 return logEvent({ threadActor: this, frame }); 911 } 912 913 return this._pauseAndRespond(frame, { 914 type: PAUSE_REASONS.EVENT_BREAKPOINT, 915 breakpoint: eventBreakpoint, 916 message: makeEventBreakpointMessage(eventBreakpoint), 917 }); 918 } 919 920 _captureDebuggerHooks() { 921 return { 922 onEnterFrame: this.dbg.onEnterFrame, 923 onStep: this.dbg.onStep, 924 onPop: this.dbg.onPop, 925 }; 926 } 927 928 _restoreDebuggerHooks(hooks) { 929 this.dbg.onEnterFrame = hooks.onEnterFrame; 930 this.dbg.onStep = hooks.onStep; 931 this.dbg.onPop = hooks.onPop; 932 } 933 934 /** 935 * Pause the debuggee, by entering a nested event loop, and return a 'paused' 936 * packet to the client. 937 * 938 * @param Debugger.Frame frame 939 * The newest debuggee frame in the stack. 940 * @param object reason 941 * An object with a 'type' property containing the reason for the pause. 942 * @param function onPacket 943 * Hook to modify the packet before it is sent. Feel free to return a 944 * promise. 945 */ 946 _pauseAndRespond(frame, reason, onPacket = k => k) { 947 try { 948 const packet = this._paused(frame); 949 if (!packet) { 950 return undefined; 951 } 952 953 const { sourceActor, line, column } = 954 this.sourcesManager.getFrameLocation(frame); 955 956 packet.why = reason; 957 958 if (!sourceActor) { 959 // If the frame location is in a source that not pass the 'isHiddenSource' 960 // check and thus has no actor, we do not bother pausing. 961 return undefined; 962 } 963 964 packet.frame.where = { 965 actor: sourceActor.actorID, 966 line, 967 column, 968 }; 969 const pkt = onPacket(packet); 970 971 this._priorPause = pkt; 972 this.emit("paused", pkt); 973 this.showOverlay(); 974 } catch (error) { 975 reportException("DBG-SERVER", error); 976 this.conn.send({ 977 error: "unknownError", 978 message: error.message + "\n" + error.stack, 979 }); 980 return undefined; 981 } 982 983 try { 984 this._nestedEventLoop.enter(); 985 } catch (e) { 986 reportException("TA__pauseAndRespond", e); 987 } 988 989 if (this._requestedFrameRestart) { 990 return null; 991 } 992 993 // If the parent actor has been closed, terminate the debuggee script 994 // instead of continuing. Executing JS after the content window is gone is 995 // a bad idea. 996 return this._targetActorClosed ? null : undefined; 997 } 998 999 _makeOnEnterFrame() { 1000 return frame => { 1001 if (this._requestedFrameRestart) { 1002 return null; 1003 } 1004 1005 // Continue forward until we get to a valid step target. 1006 const { onStep, onPop } = this._makeSteppingHooks({ 1007 steppingType: "next", 1008 }); 1009 1010 if (this.sourcesManager.isFrameBlackBoxed(frame)) { 1011 return undefined; 1012 } 1013 1014 frame.onStep = onStep; 1015 frame.onPop = onPop; 1016 return undefined; 1017 }; 1018 } 1019 1020 _makeOnPop({ pauseAndRespond, steppingType }) { 1021 const thread = this; 1022 return function (completion) { 1023 if (thread._requestedFrameRestart === this) { 1024 return thread.restartFrame(this); 1025 } 1026 1027 // onPop is called when we temporarily leave an async/generator 1028 if (steppingType != "finish" && (completion.await || completion.yield)) { 1029 thread.suspendedFrame = this; 1030 thread.dbg.onEnterFrame = undefined; 1031 return undefined; 1032 } 1033 1034 // Note that we're popping this frame; we need to watch for 1035 // subsequent step events on its caller. 1036 this.reportedPop = true; 1037 1038 // Cache the frame so that the onPop and onStep hooks are cleared 1039 // on the next pause. 1040 thread.suspendedFrame = this; 1041 1042 if ( 1043 steppingType != "finish" && 1044 !thread.sourcesManager.isFrameBlackBoxed(this) 1045 ) { 1046 const pauseAndRespValue = pauseAndRespond(this, packet => 1047 thread.createCompletionGrip(packet, completion) 1048 ); 1049 1050 // If the requested frame to restart differs from this frame, we don't 1051 // need to restart it at this point. 1052 if (thread._requestedFrameRestart === this) { 1053 return thread.restartFrame(this); 1054 } 1055 1056 return pauseAndRespValue; 1057 } 1058 1059 thread._attachSteppingHooks(this, "next", completion); 1060 return undefined; 1061 }; 1062 } 1063 1064 restartFrame(frame) { 1065 this._requestedFrameRestart = null; 1066 this._priorPause = null; 1067 1068 if ( 1069 frame.type !== "call" || 1070 frame.script.isGeneratorFunction || 1071 frame.script.isAsyncFunction 1072 ) { 1073 return undefined; 1074 } 1075 RESTARTED_FRAMES.add(frame); 1076 1077 const completion = frame.callee.apply(frame.this, frame.arguments); 1078 1079 return completion; 1080 } 1081 1082 hasMoved(frame, newType) { 1083 const newLocation = this.sourcesManager.getFrameLocation(frame); 1084 1085 if (!this._priorPause) { 1086 return true; 1087 } 1088 1089 // Recursion/Loops makes it okay to resume and land at 1090 // the same breakpoint or debugger statement. 1091 // It is not okay to transition from a breakpoint to debugger statement 1092 // or a step to a debugger statement. 1093 const { type } = this._priorPause.why; 1094 1095 // Conditional breakpoint are doing something weird as they are using "breakpoint" type 1096 // unless they throw in which case they will be "breakpointConditionThrown". 1097 if ( 1098 type == newType || 1099 (type == "breakpointConditionThrown" && newType == "breakpoint") 1100 ) { 1101 return true; 1102 } 1103 1104 const { line, column } = this._priorPause.frame.where; 1105 return line !== newLocation.line || column !== newLocation.column; 1106 } 1107 1108 _makeOnStep({ pauseAndRespond, startFrame, completion }) { 1109 const thread = this; 1110 return function () { 1111 if (thread._validFrameStepOffset(this, startFrame, this.offset)) { 1112 return pauseAndRespond(this, packet => 1113 thread.createCompletionGrip(packet, completion) 1114 ); 1115 } 1116 1117 return undefined; 1118 }; 1119 } 1120 1121 _validFrameStepOffset(frame, startFrame, offset) { 1122 const meta = frame.script.getOffsetMetadata(offset); 1123 1124 // Continue if: 1125 // 1. the location is not a valid breakpoint position 1126 // 2. the source is blackboxed 1127 // 3. we have not moved since the last pause 1128 if ( 1129 !meta.isBreakpoint || 1130 this.sourcesManager.isFrameBlackBoxed(frame) || 1131 !this.hasMoved(frame) 1132 ) { 1133 return false; 1134 } 1135 1136 // Pause if: 1137 // 1. the frame has changed 1138 // 2. the location is a step position. 1139 return frame !== startFrame || meta.isStepStart; 1140 } 1141 1142 atBreakpointLocation(frame) { 1143 const location = this.sourcesManager.getFrameLocation(frame); 1144 return !!this.breakpointActorMap.get(location); 1145 } 1146 1147 createCompletionGrip(packet, completion) { 1148 if (!completion) { 1149 return packet; 1150 } 1151 1152 packet.why.frameFinished = {}; 1153 1154 if (completion.hasOwnProperty("return")) { 1155 packet.why.frameFinished.return = this.createValueGrip(completion.return); 1156 } else if (completion.hasOwnProperty("yield")) { 1157 packet.why.frameFinished.return = this.createValueGrip(completion.yield); 1158 } else if (completion.hasOwnProperty("throw")) { 1159 packet.why.frameFinished.throw = this.createValueGrip(completion.throw); 1160 } 1161 1162 return packet; 1163 } 1164 1165 /** 1166 * Define the JS hook functions for stepping. 1167 */ 1168 _makeSteppingHooks({ steppingType, startFrame, completion }) { 1169 // Bind these methods and state because some of the hooks are called 1170 // with 'this' set to the current frame. Rather than repeating the 1171 // binding in each _makeOnX method, just do it once here and pass it 1172 // in to each function. 1173 const steppingHookState = { 1174 pauseAndRespond: (frame, onPacket = k => k) => 1175 this._pauseAndRespond( 1176 frame, 1177 { type: PAUSE_REASONS.RESUME_LIMIT }, 1178 onPacket 1179 ), 1180 startFrame: startFrame || this.youngestFrame, 1181 steppingType, 1182 completion, 1183 }; 1184 1185 return { 1186 onEnterFrame: this._makeOnEnterFrame(steppingHookState), 1187 onPop: this._makeOnPop(steppingHookState), 1188 onStep: this._makeOnStep(steppingHookState), 1189 }; 1190 } 1191 1192 /** 1193 * Handle attaching the various stepping hooks we need to attach when we 1194 * receive a resume request with a resumeLimit property. 1195 * 1196 * @param Object { resumeLimit } 1197 * The values received over the RDP. 1198 * @returns A promise that resolves to true once the hooks are attached, or is 1199 * rejected with an error packet. 1200 */ 1201 async _handleResumeLimit({ resumeLimit, frameActorID }) { 1202 const steppingType = resumeLimit.type; 1203 if ( 1204 !["break", "step", "next", "finish", "restart"].includes(steppingType) 1205 ) { 1206 return Promise.reject({ 1207 error: "badParameterType", 1208 message: "Unknown resumeLimit type", 1209 }); 1210 } 1211 1212 let frame = this.youngestFrame; 1213 1214 if (frameActorID) { 1215 frame = this._framesPool.getActorByID(frameActorID).frame; 1216 if (!frame) { 1217 throw new Error("Frame should exist in the frames pool."); 1218 } 1219 } 1220 1221 if (steppingType === "restart") { 1222 if ( 1223 frame.type !== "call" || 1224 frame.script.isGeneratorFunction || 1225 frame.script.isAsyncFunction 1226 ) { 1227 return undefined; 1228 } 1229 this._requestedFrameRestart = frame; 1230 } 1231 1232 return this._attachSteppingHooks(frame, steppingType, undefined); 1233 } 1234 1235 _attachSteppingHooks(frame, steppingType, completion) { 1236 // If we are stepping out of the onPop handler, we want to use "next" mode 1237 // so that the parent frame's handlers behave consistently. 1238 if (steppingType === "finish" && frame.reportedPop) { 1239 steppingType = "next"; 1240 } 1241 1242 // If there are no more frames on the stack, use "step" mode so that we will 1243 // pause on the next script to execute. 1244 const stepFrame = this._getNextStepFrame(frame); 1245 if (!stepFrame) { 1246 steppingType = "step"; 1247 } 1248 1249 const { onEnterFrame, onPop, onStep } = this._makeSteppingHooks({ 1250 steppingType, 1251 completion, 1252 startFrame: frame, 1253 }); 1254 1255 if (steppingType === "step" || steppingType === "restart") { 1256 this.dbg.onEnterFrame = onEnterFrame; 1257 } 1258 1259 if (stepFrame) { 1260 switch (steppingType) { 1261 case "step": 1262 case "break": 1263 case "next": 1264 if (stepFrame.script) { 1265 if (!this.sourcesManager.isFrameBlackBoxed(stepFrame)) { 1266 stepFrame.onStep = onStep; 1267 } 1268 } 1269 // eslint-disable-next-line no-fallthrough 1270 case "finish": 1271 stepFrame.onStep = createStepForReactionTracking(stepFrame.onStep); 1272 // eslint-disable-next-line no-fallthrough 1273 case "restart": 1274 stepFrame.onPop = onPop; 1275 break; 1276 } 1277 } 1278 1279 return true; 1280 } 1281 1282 /** 1283 * Clear the onStep and onPop hooks for all frames on the stack. 1284 */ 1285 _clearSteppingHooks() { 1286 if (this.suspendedFrame) { 1287 this.suspendedFrame.onStep = undefined; 1288 this.suspendedFrame.onPop = undefined; 1289 this.suspendedFrame = undefined; 1290 } 1291 1292 let frame = this.youngestFrame; 1293 if (frame?.onStack) { 1294 while (frame) { 1295 frame.onStep = undefined; 1296 frame.onPop = undefined; 1297 frame = frame.older; 1298 } 1299 } 1300 } 1301 1302 /** 1303 * Handle a protocol request to resume execution of the debuggee. 1304 */ 1305 async resume(resumeLimit, frameActorID) { 1306 if (this._state !== STATES.PAUSED) { 1307 return { 1308 error: "wrongState", 1309 message: 1310 "Can't resume when debuggee isn't paused. Current state is '" + 1311 this._state + 1312 "'", 1313 state: this._state, 1314 }; 1315 } 1316 1317 // In case of multiple nested event loops (due to multiple debuggers open in 1318 // different tabs or multiple devtools clients connected to the same tab) 1319 // only allow resumption in a LIFO order. 1320 if (!this._nestedEventLoop.isTheLastPausedThreadActor()) { 1321 return { 1322 error: "wrongOrder", 1323 message: "trying to resume in the wrong order.", 1324 }; 1325 } 1326 1327 try { 1328 if (resumeLimit) { 1329 await this._handleResumeLimit({ resumeLimit, frameActorID }); 1330 } else { 1331 this._clearSteppingHooks(); 1332 } 1333 1334 this.doResume({ resumeLimit }); 1335 return {}; 1336 } catch (error) { 1337 return error instanceof Error 1338 ? { 1339 error: "unknownError", 1340 message: DevToolsUtils.safeErrorString(error), 1341 } 1342 : // It is a known error, and the promise was rejected with an error 1343 // packet. 1344 error; 1345 } 1346 } 1347 1348 /** 1349 * Only resume and notify necessary observers. This should be used in cases 1350 * when we do not want to notify the front end of a resume, for example when 1351 * we are shutting down. 1352 */ 1353 doResume() { 1354 this._state = STATES.RUNNING; 1355 1356 // Drop the actors in the pause actor pool. 1357 this._pausePool.destroy(); 1358 this._pausePool = null; 1359 1360 this._pauseActor = null; 1361 this._nestedEventLoop.exit(); 1362 1363 // Tell anyone who cares of the resume (as of now, that's the xpcshell harness and 1364 // devtools-startup.js when handling the --wait-for-jsdebugger flag) 1365 this.emit("resumed"); 1366 this.hideOverlay(); 1367 } 1368 1369 /** 1370 * Set the debugging hook to pause on exceptions if configured to do so. 1371 * 1372 * Note that this is also called when evaluating conditional breakpoints. 1373 * 1374 * @param {boolean} doPause 1375 * Should watch for pause or not. `_onExceptionUnwind` function will 1376 * then be notified about new caught or uncaught exception being fired. 1377 */ 1378 setPauseOnExceptions(doPause) { 1379 if (doPause) { 1380 this.dbg.onExceptionUnwind = this._onExceptionUnwind; 1381 } else { 1382 this.dbg.onExceptionUnwind = undefined; 1383 } 1384 } 1385 1386 /** 1387 * Set the debugging hook to pause on debugger statement if configured to do so. 1388 * 1389 * Note that the thread actor will pause on exception by default. 1390 * This method has to be called with a falsy value to disable it. 1391 * 1392 * @param {boolean} doPause 1393 * Controls whether we should or should not pause on debugger statement. 1394 */ 1395 setPauseOnDebuggerStatement(doPause) { 1396 this.dbg.onDebuggerStatement = doPause 1397 ? this.onDebuggerStatement 1398 : undefined; 1399 } 1400 1401 isPauseOnExceptionsEnabled() { 1402 return this.dbg.onExceptionUnwind == this._onExceptionUnwind; 1403 } 1404 1405 /** 1406 * Helper method that returns the next frame when stepping. 1407 */ 1408 _getNextStepFrame(frame) { 1409 const endOfFrame = frame.reportedPop; 1410 const stepFrame = endOfFrame 1411 ? frame.older || getAsyncParentFrame(frame) 1412 : frame; 1413 if (!stepFrame || !stepFrame.script) { 1414 return null; 1415 } 1416 1417 // Skips a frame that has been restarted. 1418 if (RESTARTED_FRAMES.has(stepFrame)) { 1419 return this._getNextStepFrame(stepFrame.older); 1420 } 1421 1422 return stepFrame; 1423 } 1424 1425 frames(start, count) { 1426 if (this.state !== STATES.PAUSED) { 1427 return { 1428 error: "wrongState", 1429 message: 1430 "Stack frames are only available while the debuggee is paused.", 1431 }; 1432 } 1433 1434 // Find the starting frame... 1435 let frame = this.youngestFrame; 1436 1437 const walkToParentFrame = () => { 1438 if (!frame) { 1439 return; 1440 } 1441 1442 const currentFrame = frame; 1443 frame = null; 1444 1445 if (!(currentFrame instanceof Debugger.Frame)) { 1446 frame = getSavedFrameParent(this, currentFrame); 1447 } else if (currentFrame.older) { 1448 frame = currentFrame.older; 1449 } else if ( 1450 this._options.shouldIncludeSavedFrames && 1451 currentFrame.olderSavedFrame 1452 ) { 1453 frame = currentFrame.olderSavedFrame; 1454 if (frame && !isValidSavedFrame(this, frame)) { 1455 frame = null; 1456 } 1457 } else if ( 1458 this._options.shouldIncludeAsyncLiveFrames && 1459 currentFrame.asyncPromise 1460 ) { 1461 const asyncFrame = getAsyncParentFrame(currentFrame); 1462 if (asyncFrame) { 1463 frame = asyncFrame; 1464 } 1465 } 1466 }; 1467 1468 let i = 0; 1469 while (frame && i < start) { 1470 walkToParentFrame(); 1471 i++; 1472 } 1473 1474 // Return count frames, or all remaining frames if count is not defined. 1475 const frames = []; 1476 for (; frame && (!count || i < start + count); i++, walkToParentFrame()) { 1477 // SavedFrame instances don't have direct Debugger.Source object. If 1478 // there is an active Debugger.Source that represents the SaveFrame's 1479 // source, it will have already been created in the server. 1480 if (frame instanceof Debugger.Frame) { 1481 this.sourcesManager.createSourceActor(frame.script.source); 1482 } 1483 1484 if (RESTARTED_FRAMES.has(frame)) { 1485 continue; 1486 } 1487 1488 const frameActor = this._createFrameActor(frame, i); 1489 frames.push(frameActor); 1490 } 1491 1492 return { frames }; 1493 } 1494 1495 addAllSources() { 1496 // This method aims at instantiating Source Actors for all already existing 1497 // sources (via `_addSource()`). 1498 // This is called on each new target instantiation: 1499 // * when a new document or debugging context is instantiated. This 1500 // method should be a no-op as there should be no pre-existing sources. 1501 // * when devtools open. This time there might be pre-existing sources. 1502 // 1503 // We are using Debugger API `findSources()` for instantating source actors 1504 // of all still-active sources. But we want to also "resurrect" sources 1505 // which ran before DevTools were opened and were garbaged collected. 1506 // `findSources()` won't return them. 1507 // Debugger API `findSourceURLs()` will return the source URLs of all the 1508 // sources, GC-ed and still active ones. 1509 // 1510 // We are using `urlMap` to identify the GC-ed sources. 1511 // 1512 // We have two special edgecases: 1513 // 1514 // # HTML sources and inline <script> tags 1515 // 1516 // HTML sources will be specific to a given URL, but may relate to multiple 1517 // inline <script> tag. Each script will be related to a given Debugger API 1518 // source and a given DevTools Source Actor. 1519 // We collect all active sources in `urlMap`'s `sources` array so that we 1520 // only resurrect the GC-ed inline <script> and not the one which are still 1521 // active. 1522 // 1523 // # asm.js / wasm 1524 // 1525 // DevTools toggles Debugger API `allowUnobservedAsmJS` and 1526 // `allowUnobservedWasm` to false on opening. This changes how asm.js and 1527 // Wasm sources are compiled. But only to sources created after DevTools 1528 // are opened. This typically requires to reload the page. 1529 // 1530 // Before DevTools are opened, the asm.js functions are compiled into wasm 1531 // instances, and they are visible as "wasm" sources in `findSources()`. 1532 // The wasm instance doesn't keep the top-level normal JS script and the 1533 // corresponding JS source alive. If only the "wasm" source is found for 1534 // certain URL, the source needs to be re-compiled. 1535 // 1536 // Here, we should be careful to re-compile these sources the way they were 1537 // compiled before DevTools opening. Otherwise the re-compilation will 1538 // create Debugger.Script instances backed by normal JS functions for those 1539 // asm.js functions, which results in an inconsistency between what's 1540 // running in the debuggee and what's shown in DevTools. 1541 // 1542 // We are using `urlMap`'s `hasWasm` to flag them and instruct 1543 // `resurrectSource()` to re-compile the sources as if DevTools was off and 1544 // without debugging ability. 1545 const urlMap = {}; 1546 for (const url of this.dbg.findSourceURLs()) { 1547 if (url !== "self-hosted") { 1548 if (!urlMap[url]) { 1549 urlMap[url] = { count: 0, sources: [], hasWasm: false }; 1550 } 1551 urlMap[url].count++; 1552 } 1553 } 1554 1555 const sources = this.dbg.findSources(); 1556 1557 for (const source of sources) { 1558 this._addSource(source); 1559 1560 if (source.introductionType === "wasm") { 1561 const origURL = source.url.replace(/^wasm:/, ""); 1562 if (urlMap[origURL]) { 1563 urlMap[origURL].hasWasm = true; 1564 } 1565 } 1566 1567 // The following check should match the filtering done by `findSourceURLs`: 1568 // https://searchfox.org/mozilla-central/rev/ac7a567f036e1954542763f4722fbfce041fb752/js/src/debugger/Debugger.cpp#2406-2409 1569 // Otherwise we may populate `urlMap` incorrectly and resurrect sources that weren't GCed, 1570 // and spawn duplicated SourceActors/Debugger.Source for the same actual source. 1571 // `findSourceURLs` uses !introductionScript check as that allows to identify <script>'s 1572 // loaded from the HTML page. This boolean will be defined only when the <script> tag 1573 // is added by Javascript code at runtime. 1574 // https://searchfox.org/mozilla-central/rev/3d03a3ca09f03f06ef46a511446537563f62a0c6/devtools/docs/user/debugger-api/debugger.source/index.rst#113 1575 if (!source.introductionScript && urlMap[source.url]) { 1576 urlMap[source.url].count--; 1577 urlMap[source.url].sources.push(source); 1578 } 1579 } 1580 1581 // Resurrect any URLs for which not all sources are accounted for. 1582 for (const [url, data] of Object.entries(urlMap)) { 1583 if (data.count > 0) { 1584 this._resurrectSource(url, data.sources, data.hasWasm); 1585 } 1586 } 1587 } 1588 1589 sources() { 1590 this.addAllSources(); 1591 1592 // No need to flush the new source packets here, as we are sending the 1593 // list of sources out immediately and we don't need to invoke the 1594 // overhead of an RDP packet for every source right now. Let the default 1595 // timeout flush the buffered packets. 1596 1597 const forms = []; 1598 for (const source of this.sourcesManager.iter()) { 1599 forms.push(source.form()); 1600 } 1601 return forms; 1602 } 1603 1604 /** 1605 * Disassociate all breakpoint actors from their scripts and clear the 1606 * breakpoint handlers. This method can be used when the thread actor intends 1607 * to keep the breakpoint store, but needs to clear any actual breakpoints, 1608 * e.g. due to a page navigation. This way the breakpoint actors' script 1609 * caches won't hold on to the Debugger.Script objects leaking memory. 1610 */ 1611 disableAllBreakpoints() { 1612 for (const bpActor of this.breakpointActorMap.findActors()) { 1613 bpActor.removeScripts(); 1614 } 1615 } 1616 1617 removeAllBreakpoints() { 1618 this.breakpointActorMap.removeAllBreakpoints(); 1619 } 1620 1621 removeAllWatchpoints() { 1622 for (const actor of this.threadLifetimePool.poolChildren()) { 1623 if (actor.typeName == "obj") { 1624 actor.removeWatchpoints(); 1625 } 1626 } 1627 } 1628 1629 addWatchpoint(objActor, data) { 1630 this._watchpointsMap.add(objActor, data); 1631 } 1632 1633 removeWatchpoint(objActor, property) { 1634 this._watchpointsMap.remove(objActor, property); 1635 } 1636 1637 getWatchpoint(obj, property) { 1638 return this._watchpointsMap.get(obj, property); 1639 } 1640 1641 /** 1642 * Handle a protocol request to pause the debuggee. 1643 */ 1644 interrupt(when) { 1645 if (this.state == STATES.EXITED) { 1646 return { type: "exited" }; 1647 } else if (this.state == STATES.PAUSED) { 1648 // TODO: return the actual reason for the existing pause. 1649 this.emit("paused", { 1650 why: { type: PAUSE_REASONS.ALREADY_PAUSED }, 1651 }); 1652 return {}; 1653 } else if (this.state != STATES.RUNNING) { 1654 return { 1655 error: "wrongState", 1656 message: "Received interrupt request in " + this.state + " state.", 1657 }; 1658 } 1659 try { 1660 // If execution should pause just before the next JavaScript bytecode is 1661 // executed, just set an onEnterFrame handler. 1662 if (when == "onNext") { 1663 const onEnterFrame = frame => { 1664 this._pauseAndRespond(frame, { 1665 type: PAUSE_REASONS.INTERRUPTED, 1666 onNext: true, 1667 }); 1668 }; 1669 this.dbg.onEnterFrame = onEnterFrame; 1670 return {}; 1671 } 1672 1673 // If execution should pause immediately, just put ourselves in the paused 1674 // state. 1675 const packet = this._paused(); 1676 if (!packet) { 1677 return { error: "notInterrupted" }; 1678 } 1679 packet.why = { type: PAUSE_REASONS.INTERRUPTED, onNext: false }; 1680 1681 // Send the response to the interrupt request now (rather than 1682 // returning it), because we're going to start a nested event loop 1683 // here. 1684 this.conn.send({ from: this.actorID, type: "interrupt" }); 1685 this.emit("paused", packet); 1686 1687 // Start a nested event loop. 1688 this._nestedEventLoop.enter(); 1689 1690 // We already sent a response to this request, don't send one 1691 // now. 1692 return null; 1693 } catch (e) { 1694 reportException("DBG-SERVER", e); 1695 return { error: "notInterrupted", message: e.toString() }; 1696 } 1697 } 1698 1699 _paused(frame) { 1700 // We don't handle nested pauses correctly. Don't try - if we're 1701 // paused, just continue running whatever code triggered the pause. 1702 // We don't want to actually have nested pauses (although we 1703 // have nested event loops). If code runs in the debuggee during 1704 // a pause, it should cause the actor to resume (dropping 1705 // pause-lifetime actors etc) and then repause when complete. 1706 1707 if (this.state === STATES.PAUSED) { 1708 return undefined; 1709 } 1710 1711 this._state = STATES.PAUSED; 1712 1713 // Clear stepping hooks. 1714 this.dbg.onEnterFrame = undefined; 1715 this._requestedFrameRestart = null; 1716 this._clearSteppingHooks(); 1717 1718 // Create the actor pool that will hold the pause actor and its 1719 // children. 1720 assert(!this._pausePool, "No pause pool should exist yet"); 1721 this._pausePool = new ObjectActorPool(this, "pause", true); 1722 1723 // Give children of the pause pool a quick link back to the 1724 // thread... 1725 this._pausePool.threadActor = this; 1726 1727 // Create the pause actor itself... 1728 assert(!this._pauseActor, "No pause actor should exist yet"); 1729 this._pauseActor = new PauseActor(this._pausePool); 1730 this._pausePool.manage(this._pauseActor); 1731 1732 // Update the list of frames. 1733 this._updateFrames(); 1734 1735 // Send off the paused packet and spin an event loop. 1736 const packet = { 1737 actor: this._pauseActor.actorID, 1738 }; 1739 1740 if (frame) { 1741 packet.frame = this._createFrameActor(frame); 1742 } 1743 1744 return packet; 1745 } 1746 1747 /** 1748 * Expire frame actors for frames that are no longer on the current stack. 1749 */ 1750 _updateFrames() { 1751 // Create the actor pool that will hold the still-living frames. 1752 const framesPool = new Pool(this.conn, "frames"); 1753 const frameList = []; 1754 1755 for (const frameActor of this._frameActors) { 1756 if (frameActor.frame.onStack) { 1757 framesPool.manage(frameActor); 1758 frameList.push(frameActor); 1759 } 1760 } 1761 1762 // Remove the old frame actor pool, this will expire 1763 // any actors that weren't added to the new pool. 1764 if (this._framesPool) { 1765 this._framesPool.destroy(); 1766 } 1767 1768 this._frameActors = frameList; 1769 this._framesPool = framesPool; 1770 } 1771 1772 _createFrameActor(frame, depth) { 1773 let actor = this._frameActorMap.get(frame); 1774 if (!actor || actor.isDestroyed()) { 1775 actor = new FrameActor(frame, this, depth); 1776 this._frameActors.push(actor); 1777 this._framesPool.manage(actor); 1778 1779 this._frameActorMap.set(frame, actor); 1780 } 1781 return actor; 1782 } 1783 1784 /** 1785 * Create and return an environment actor that corresponds to the provided 1786 * Debugger.Environment. 1787 * 1788 * @param Debugger.Environment environment 1789 * The lexical environment we want to extract. 1790 * @param object pool 1791 * The pool where the newly-created actor will be placed. 1792 * @return The EnvironmentActor for environment or undefined for host 1793 * functions or functions scoped to a non-debuggee global. 1794 */ 1795 createEnvironmentActor(environment, pool) { 1796 if (!environment) { 1797 return undefined; 1798 } 1799 1800 if (environment.actor) { 1801 return environment.actor; 1802 } 1803 1804 const actor = new EnvironmentActor(environment, this); 1805 pool.manage(actor); 1806 environment.actor = actor; 1807 1808 return actor; 1809 } 1810 1811 /** 1812 * Create a grip for the given debuggee value. 1813 * Depdending on if the thread is paused, the object actor may have a different lifetime: 1814 * - when thread is paused, the object actor will be kept alive until the thread is resumed 1815 * (which also happens when we step) 1816 * - when thread is not paused, the object actor will be kept alive until the related target 1817 * is destroyed (thread stops or devtools closes) 1818 * 1819 * @param value Debugger.Object|any 1820 * A Debugger.Object for all JS objects, or any primitive JS type. 1821 * @return The value's grip 1822 * Primitive JS type, Object actor Form JSON object, or a JSON object to describe the value. 1823 */ 1824 createValueGrip(value) { 1825 // When the thread is paused, all objects are stored in a transient pool 1826 // which will be cleared on resume (which also happens when we step). 1827 const pool = this._pausePool || this.threadLifetimePool; 1828 1829 return createValueGrip(this, value, pool); 1830 } 1831 1832 _onWindowReady({ isTopLevel, isBFCache }) { 1833 // Note that this code relates to the disabling of Debugger API from will-navigate listener. 1834 // And should only be triggered when the target actor doesn't follow WindowGlobal lifecycle. 1835 // i.e. when the Thread Actor manages more than one top level WindowGlobal. 1836 if (isTopLevel && this.state != STATES.DETACHED) { 1837 this.sourcesManager.reset(); 1838 this.clearDebuggees(); 1839 this.dbg.enable(); 1840 } 1841 1842 // Refresh the debuggee list when a new window object appears (top window or 1843 // iframe). 1844 if (this.attached) { 1845 this.dbg.addDebuggees(); 1846 } 1847 1848 // BFCache navigations reuse old sources, so send existing sources to the 1849 // client instead of waiting for onNewScript debugger notifications. 1850 if (isBFCache) { 1851 this.addAllSources(); 1852 } 1853 } 1854 1855 _onWillNavigate({ isTopLevel }) { 1856 if (!isTopLevel) { 1857 return; 1858 } 1859 1860 // Proceed normally only if the debuggee is not paused. 1861 if (this.state == STATES.PAUSED) { 1862 // If we were paused while navigating to a new page, 1863 // we resume previous page execution, so that the document can be sucessfully unloaded. 1864 // And we disable the Debugger API, so that we do not hit any breakpoint or trigger any 1865 // thread actor feature. We will re-enable it just before the next page starts loading, 1866 // from window-ready listener. That's for when the target doesn't follow WindowGlobal 1867 // lifecycle. 1868 // When the target follows the WindowGlobal lifecycle, we will stiff resume and disable 1869 // this thread actor. It will soon be destroyed. And a new target will pick up 1870 // the next WindowGlobal and spawn a new Debugger API, via ThreadActor.attach(). 1871 this.doResume(); 1872 this.dbg.disable(); 1873 } 1874 1875 this.removeAllWatchpoints(); 1876 this.disableAllBreakpoints(); 1877 this.dbg.onEnterFrame = undefined; 1878 } 1879 1880 _onNavigate() { 1881 if (this.state == STATES.RUNNING) { 1882 this.dbg.enable(); 1883 } 1884 } 1885 1886 // JS Debugger API hooks. 1887 pauseForMutationBreakpoint( 1888 mutationType, 1889 targetNode, 1890 ancestorNode, 1891 action = "" // "add" or "remove" 1892 ) { 1893 if ( 1894 !["subtreeModified", "nodeRemoved", "attributeModified"].includes( 1895 mutationType 1896 ) 1897 ) { 1898 throw new Error("Unexpected mutation breakpoint type"); 1899 } 1900 1901 if (this.shouldSkipAnyBreakpoint) { 1902 return undefined; 1903 } 1904 1905 const frame = this.dbg.getNewestFrame(); 1906 if (!frame) { 1907 return undefined; 1908 } 1909 1910 if (this.sourcesManager.isFrameBlackBoxed(frame)) { 1911 return undefined; 1912 } 1913 1914 const global = (targetNode.ownerDocument || targetNode).defaultView; 1915 assert(global && this.dbg.hasDebuggee(global)); 1916 1917 const targetObj = this.dbg 1918 .makeGlobalObjectReference(global) 1919 .makeDebuggeeValue(targetNode); 1920 1921 let ancestorObj = null; 1922 if (ancestorNode) { 1923 ancestorObj = this.dbg 1924 .makeGlobalObjectReference(global) 1925 .makeDebuggeeValue(ancestorNode); 1926 } 1927 1928 return this._pauseAndRespond( 1929 frame, 1930 { 1931 type: PAUSE_REASONS.MUTATION_BREAKPOINT, 1932 mutationType, 1933 message: `DOM Mutation: '${mutationType}'`, 1934 }, 1935 pkt => { 1936 // We have to create the object actors late, from here because `_pausePool` is `null` beforehand, 1937 // and the actors created by createValueGrip would otherwise be registered in the thread lifetime pool 1938 pkt.why.nodeGrip = this.createValueGrip(targetObj); 1939 pkt.why.ancestorGrip = ancestorObj 1940 ? this.createValueGrip(ancestorObj) 1941 : null; 1942 pkt.why.action = action; 1943 return pkt; 1944 } 1945 ); 1946 } 1947 1948 /** 1949 * A function that the engine calls when a debugger statement has been 1950 * executed in the specified frame. 1951 * 1952 * @param frame Debugger.Frame 1953 * The stack frame that contained the debugger statement. 1954 */ 1955 onDebuggerStatement(frame) { 1956 // Don't pause if: 1957 // 1. breakpoints are disabled 1958 // 2. we have not moved since the last pause 1959 // 3. the source is blackboxed 1960 // 4. there is a breakpoint at the same location 1961 if ( 1962 this.shouldSkipAnyBreakpoint || 1963 !this.hasMoved(frame, "debuggerStatement") || 1964 this.sourcesManager.isFrameBlackBoxed(frame) || 1965 this.atBreakpointLocation(frame) 1966 ) { 1967 return undefined; 1968 } 1969 1970 return this._pauseAndRespond(frame, { 1971 type: PAUSE_REASONS.DEBUGGER_STATEMENT, 1972 }); 1973 } 1974 1975 skipBreakpoints(skip) { 1976 this._options.skipBreakpoints = skip; 1977 return { skip }; 1978 } 1979 1980 // Bug 1686485 is meant to remove usages of this request 1981 // in favor direct call to `reconfigure` 1982 pauseOnExceptions(pauseOnExceptions, ignoreCaughtExceptions) { 1983 this.reconfigure({ 1984 pauseOnExceptions, 1985 ignoreCaughtExceptions, 1986 }); 1987 return {}; 1988 } 1989 1990 /** 1991 * A function that the engine calls when an exception has been thrown and has 1992 * propagated to the specified frame. 1993 * 1994 * @param youngestFrame Debugger.Frame 1995 * The youngest remaining stack frame. 1996 * @param value object 1997 * The exception that was thrown. 1998 */ 1999 _onExceptionUnwind(youngestFrame, value) { 2000 // Ignore any reported exception if we are already paused 2001 if (this.isPaused()) { 2002 return undefined; 2003 } 2004 2005 // Ignore shouldSkipAnyBreakpoint if we are explicitly requested to do so. 2006 // Typically, when we are evaluating conditional breakpoints, we want to report any exception. 2007 if ( 2008 this.shouldSkipAnyBreakpoint && 2009 !this.insideClientEvaluation?.reportExceptionsWhenBreaksAreDisabled 2010 ) { 2011 return undefined; 2012 } 2013 2014 let willBeCaught = false; 2015 for (let frame = youngestFrame; frame != null; frame = frame.older) { 2016 if (frame.script.isInCatchScope(frame.offset)) { 2017 willBeCaught = true; 2018 break; 2019 } 2020 } 2021 2022 if (willBeCaught && this._options.ignoreCaughtExceptions) { 2023 return undefined; 2024 } 2025 2026 if ( 2027 this._handledFrameExceptions.has(youngestFrame) && 2028 this._handledFrameExceptions.get(youngestFrame) === value 2029 ) { 2030 return undefined; 2031 } 2032 2033 // NS_ERROR_NO_INTERFACE exceptions are a special case in browser code, 2034 // since they're almost always thrown by QueryInterface functions, and 2035 // handled cleanly by native code. 2036 if (!isWorker && value == Cr.NS_ERROR_NO_INTERFACE) { 2037 return undefined; 2038 } 2039 2040 // Don't pause on exceptions thrown while inside an evaluation being done on 2041 // behalf of the client. 2042 if (this.insideClientEvaluation) { 2043 return undefined; 2044 } 2045 2046 if (this.sourcesManager.isFrameBlackBoxed(youngestFrame)) { 2047 return undefined; 2048 } 2049 2050 // Now that we've decided to pause, ignore this exception if it's thrown by 2051 // any older frames. 2052 for (let frame = youngestFrame.older; frame != null; frame = frame.older) { 2053 this._handledFrameExceptions.set(frame, value); 2054 } 2055 2056 try { 2057 const packet = this._paused(youngestFrame); 2058 if (!packet) { 2059 return undefined; 2060 } 2061 2062 packet.why = { 2063 type: PAUSE_REASONS.EXCEPTION, 2064 exception: this.createValueGrip(value), 2065 }; 2066 this.emit("paused", packet); 2067 2068 this._nestedEventLoop.enter(); 2069 } catch (e) { 2070 reportException("TA_onExceptionUnwind", e); 2071 } 2072 2073 return undefined; 2074 } 2075 2076 /** 2077 * A function that the engine calls when a new script has been loaded. 2078 * 2079 * @param script Debugger.Script 2080 * The source script that has been loaded into a debuggee compartment. 2081 */ 2082 onNewScript(script) { 2083 this._addSource(script.source); 2084 2085 this._maybeTrackFirstStatementBreakpoint(script); 2086 } 2087 2088 /** 2089 * A function called when there's a new source from a thread actor's sources. 2090 * Emits `newSource` on the thread actor. 2091 * 2092 * @param {SourceActor} source 2093 */ 2094 onNewSourceEvent(source) { 2095 // When this target is supported by the Watcher Actor, 2096 // and we listen to SOURCE, we avoid emitting the newSource RDP event 2097 // as it would be duplicated with the Resource/watchResources API. 2098 // Could probably be removed once bug 1680280 is fixed. 2099 if (!this._shouldEmitNewSource) { 2100 return; 2101 } 2102 2103 // Bug 1516197: New sources are likely detected due to either user 2104 // interaction on the page, or devtools requests sent to the server. 2105 // We use executeSoon because we don't want to block those operations 2106 // by sending packets in the middle of them. 2107 DevToolsUtils.executeSoon(() => { 2108 if (this.isDestroyed()) { 2109 return; 2110 } 2111 this.emit("newSource", { 2112 source: source.form(), 2113 }); 2114 }); 2115 } 2116 2117 // API used by the Watcher Actor to disable the newSource events 2118 // Could probably be removed once bug 1680280 is fixed. 2119 _shouldEmitNewSource = true; 2120 disableNewSourceEvents() { 2121 this._shouldEmitNewSource = false; 2122 } 2123 2124 /** 2125 * Filtering function to filter out sources for which we don't want to notify/create 2126 * source actors 2127 * 2128 * @param {Debugger.Source} source 2129 * The source to accept or ignore 2130 * @param Boolean 2131 * True, if we want to create a source actor. 2132 */ 2133 _acceptSource(source) { 2134 // We have some spurious source created by ExtensionContent.sys.mjs when debugging tabs. 2135 // These sources are internal stuff injected by WebExt codebase to implement content 2136 // scripts. We can't easily ignore them from Debugger API, so ignore them 2137 // when debugging a tab (i.e. browser-element). As we still want to debug them 2138 // from the browser toolbox. 2139 if ( 2140 this.targetActor.sessionContext.type == "browser-element" && 2141 source.url.endsWith("ExtensionContent.sys.mjs") 2142 ) { 2143 return false; 2144 } 2145 2146 return true; 2147 } 2148 2149 /** 2150 * Add the provided source to the server cache. 2151 * 2152 * @param aSource Debugger.Source 2153 * The source that will be stored. 2154 */ 2155 _addSource(source) { 2156 if (!this._acceptSource(source)) { 2157 return; 2158 } 2159 2160 // Preloaded WebExtension content scripts may be cached internally by 2161 // ExtensionContent.jsm and ThreadActor would ignore them on a page reload 2162 // because it finds them in the _debuggerSourcesSeen WeakSet, 2163 // and so we also need to be sure that there is still a source actor for the source. 2164 let sourceActor; 2165 if ( 2166 this._debuggerSourcesSeen.has(source) && 2167 this.sourcesManager.hasSourceActor(source) 2168 ) { 2169 sourceActor = this.sourcesManager.getSourceActor(source); 2170 sourceActor.resetDebuggeeScripts(); 2171 } else { 2172 sourceActor = this.sourcesManager.createSourceActor(source); 2173 } 2174 2175 const sourceUrl = sourceActor.url; 2176 if (this._onLoadBreakpointURLs.has(sourceUrl)) { 2177 // Immediately set a breakpoint on first line 2178 // (note that this is only used by `./mach xpcshell-test --jsdebugger`) 2179 this.setBreakpoint({ sourceUrl, line: 1 }, {}); 2180 // But also query asynchronously the first really breakable line 2181 // as the first may not be valid and won't break. 2182 (async () => { 2183 const [firstLine] = await sourceActor.getBreakableLines(); 2184 if (firstLine != 1) { 2185 this.setBreakpoint({ sourceUrl, line: firstLine }, {}); 2186 } 2187 })(); 2188 } 2189 2190 const bpActors = this.breakpointActorMap 2191 .findActors() 2192 .filter( 2193 actor => 2194 actor.location.sourceUrl && actor.location.sourceUrl == sourceUrl 2195 ); 2196 2197 for (const actor of bpActors) { 2198 sourceActor.applyBreakpoint(actor); 2199 } 2200 2201 this._debuggerSourcesSeen.add(source); 2202 } 2203 2204 /** 2205 * Create a new source by refetching the specified URL and instantiating all 2206 * sources that were found in the result. 2207 * 2208 * @param url The URL string to fetch. 2209 * @param existingInlineSources The inline sources for the URL the debugger knows about 2210 * already, and that we shouldn't re-create (only used when 2211 * url content type is text/html). 2212 * @param forceEnableAsmJS A boolean to force enable the asm.js feature. 2213 * See the comment inside addAllSources for more 2214 * details. 2215 */ 2216 async _resurrectSource(url, existingInlineSources, forceEnableAsmJS) { 2217 let { content, contentType, sourceMapURL } = 2218 await this.sourcesManager.urlContents( 2219 url, 2220 /* partial */ false, 2221 /* canUseCache */ true 2222 ); 2223 2224 // Newlines in all sources should be normalized. Do this with HTML content 2225 // to simplify the comparisons below. 2226 content = content.replace(/\r\n?|\u2028|\u2029/g, "\n"); 2227 2228 if (contentType == "text/html") { 2229 // HTML files can contain any number of inline sources. We have to find 2230 // all the inline sources and their start line without running any of the 2231 // scripts on the page. The approach used here is approximate. 2232 if (!this.targetActor.window) { 2233 return; 2234 } 2235 2236 // Find the offsets in the HTML at which inline scripts might start. 2237 const scriptTagMatches = content.matchAll(/<script[^>]*>/gi); 2238 const scriptStartOffsets = [...scriptTagMatches].map( 2239 rv => rv.index + rv[0].length 2240 ); 2241 2242 // Find the script tags in this HTML page by parsing a new document from 2243 // the contentand looking for its script elements. 2244 const document = new DOMParser().parseFromString(content, "text/html"); 2245 2246 // For each inline source found, see if there is a start offset for what 2247 // appears to be a script tag, whose contents match the inline source. 2248 [...document.scripts].forEach(script => { 2249 const text = script.innerText; 2250 2251 // We only want to handle inline scripts 2252 if (script.src) { 2253 return; 2254 } 2255 2256 // Don't create source for empty script tag 2257 if (!text.trim()) { 2258 return; 2259 } 2260 2261 const scriptStartOffsetIndex = scriptStartOffsets.findIndex( 2262 offset => content.substring(offset, offset + text.length) == text 2263 ); 2264 // Bail if we couldn't find the start offset for the script 2265 if (scriptStartOffsetIndex == -1) { 2266 return; 2267 } 2268 2269 const scriptStartOffset = scriptStartOffsets[scriptStartOffsetIndex]; 2270 // Remove the offset from the array to mitigate any issue we might with scripts 2271 // sharing the same text content. 2272 scriptStartOffsets.splice(scriptStartOffsetIndex, 1); 2273 2274 const allLineBreaks = [ 2275 ...content.substring(0, scriptStartOffset).matchAll("\n"), 2276 ]; 2277 const startLine = 1 + allLineBreaks.length; 2278 // NOTE: Debugger.Source.prototype.startColumn is 1-based. 2279 // Create 1-based column here for the following comparison, 2280 // and also the createSource call below. 2281 const startColumn = 2282 1 + 2283 scriptStartOffset - 2284 (allLineBreaks.length ? allLineBreaks.at(-1).index - 1 : 0); 2285 2286 // Don't create a source if we already found one for this script 2287 if ( 2288 existingInlineSources.find( 2289 source => 2290 source.startLine == startLine && source.startColumn == startColumn 2291 ) 2292 ) { 2293 return; 2294 } 2295 2296 try { 2297 const global = this.dbg.getDebuggees()[0]; 2298 // NOTE: Debugger.Object.prototype.createSource takes 1-based column. 2299 this._addSource( 2300 global.createSource({ 2301 text, 2302 url, 2303 startLine, 2304 startColumn, 2305 isScriptElement: true, 2306 forceEnableAsmJS, 2307 }) 2308 ); 2309 } catch (e) { 2310 // Ignore parse errors. 2311 } 2312 }); 2313 2314 // If no scripts were found, we might have an inaccurate content type and 2315 // the file is actually JavaScript. Fall through and add the entire file 2316 // as the source. 2317 if (document.scripts.length) { 2318 return; 2319 } 2320 } 2321 2322 // Other files should only contain javascript, so add the file contents as 2323 // the source itself. 2324 try { 2325 const global = this.dbg.getDebuggees()[0]; 2326 this._addSource( 2327 global.createSource({ 2328 text: content, 2329 url, 2330 startLine: 1, 2331 sourceMapURL, 2332 forceEnableAsmJS, 2333 }) 2334 ); 2335 } catch (e) { 2336 // Ignore parse errors. 2337 } 2338 } 2339 2340 dumpThread() { 2341 return { 2342 pauseOnExceptions: this._options.pauseOnExceptions, 2343 ignoreCaughtExceptions: this._options.ignoreCaughtExceptions, 2344 logEventBreakpoints: this._options.logEventBreakpoints, 2345 skipBreakpoints: this.shouldSkipAnyBreakpoint, 2346 breakpoints: this.breakpointActorMap.listKeys(), 2347 }; 2348 } 2349 2350 // NOTE: dumpPools is defined in the Thread actor to avoid 2351 // adding it to multiple target specs and actors. 2352 dumpPools() { 2353 return this.conn.dumpPools(); 2354 } 2355 2356 logLocation(prefix, frame) { 2357 const loc = this.sourcesManager.getFrameLocation(frame); 2358 dump(`${prefix} (${loc.line}, ${loc.column})\n`); 2359 } 2360 } 2361 2362 exports.ThreadActor = ThreadActor; 2363 2364 /** 2365 * Creates a PauseActor. 2366 * 2367 * PauseActors exist for the lifetime of a given debuggee pause. Used to 2368 * scope pause-lifetime grips. 2369 */ 2370 class PauseActor { 2371 /** 2372 * @param {Pool} pool: The actor pool created for this pause. 2373 */ 2374 constructor(pool) { 2375 this.pool = pool; 2376 } 2377 typeName = "pause"; 2378 } 2379 2380 // Utility functions. 2381 2382 /** 2383 * Unwrap a global that is wrapped in a |Debugger.Object|, or if the global has 2384 * become a dead object, return |undefined|. 2385 * 2386 * @param Debugger.Object wrappedGlobal 2387 * The |Debugger.Object| which wraps a global. 2388 * 2389 * @returns {object | undefined} 2390 * Returns the unwrapped global object or |undefined| if unwrapping 2391 * failed. 2392 */ 2393 exports.unwrapDebuggerObjectGlobal = wrappedGlobal => { 2394 try { 2395 // Because of bug 991399 we sometimes get nuked window references here. We 2396 // just bail out in that case. 2397 // 2398 // Note that addon sandboxes have a DOMWindow as their prototype. So make 2399 // sure that we can touch the prototype too (whatever it is), in case _it_ 2400 // is it a nuked window reference. We force stringification to make sure 2401 // that any dead object proxies make themselves known. 2402 const global = wrappedGlobal.unsafeDereference(); 2403 Object.getPrototypeOf(global) + ""; 2404 return global; 2405 } catch (e) { 2406 return undefined; 2407 } 2408 };