browser_resources_thread_states.js (17527B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 // Test the ResourceCommand API around THREAD_STATE 7 8 const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js"); 9 10 const BREAKPOINT_TEST_URL = URL_ROOT_SSL + "breakpoint_document.html"; 11 const REMOTE_IFRAME_URL = 12 "https://example.org/document-builder.sjs?html=" + 13 encodeURIComponent("<script>debugger;</script>"); 14 15 add_task(async function () { 16 // Check hitting the "debugger;" statement before and after calling 17 // watchResource(THREAD_TYPES). Both should break. First will 18 // be a cached resource and second will be a live one. 19 await checkBreakpointBeforeWatchResources(); 20 await checkBreakpointAfterWatchResources(); 21 22 // Check setting a real breakpoint on a given line 23 await checkRealBreakpoint(); 24 25 // Check the "pause on exception" setting 26 await checkPauseOnException(); 27 28 // Check an edge case where spamming setBreakpoints calls causes issues 29 await checkSetBeforeWatch(); 30 31 // Check debugger statement for (remote) iframes 32 await checkDebuggerStatementInIframes(); 33 34 // Check the behavior of THREAD_STATE against a multiprocess usecase 35 await testMultiprocessThreadState(); 36 }); 37 38 async function checkBreakpointBeforeWatchResources() { 39 info( 40 "Check whether ResourceCommand gets existing breakpoint, being hit before calling watchResources" 41 ); 42 43 const tab = await addTab(BREAKPOINT_TEST_URL); 44 45 const { commands, resourceCommand, targetCommand } = 46 await initResourceCommand(tab); 47 48 // Ensure that the target front is initialized early from TargetCommand.onTargetAvailable 49 // By the time `initResourceCommand` resolves, it should already be initialized. 50 info( 51 "Verify that TargetFront's initialized is resolved after having calling attachAndInitThread" 52 ); 53 await targetCommand.targetFront.initialized; 54 55 // We have to ensure passing any thread configuration in order to have breakpoints being handled. 56 await commands.threadConfigurationCommand.updateConfiguration({ 57 skipBreakpoints: false, 58 }); 59 60 info("Run the 'debugger' statement"); 61 // Note that we do not wait for the resolution of spawn as it will be paused 62 ContentTask.spawn(tab.linkedBrowser, null, () => { 63 content.window.wrappedJSObject.runDebuggerStatement(); 64 }); 65 66 info("Call watchResources"); 67 const availableResources = []; 68 await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], { 69 onAvailable: resources => availableResources.push(...resources), 70 }); 71 72 is( 73 availableResources.length, 74 1, 75 "Got the THREAD_STATE's related to the debugger statement" 76 ); 77 const threadState = availableResources.pop(); 78 79 assertPausedResource(threadState, { 80 state: "paused", 81 why: { 82 type: "debuggerStatement", 83 }, 84 frame: { 85 type: "call", 86 asyncCause: null, 87 state: "on-stack", 88 // this: object actor's form referring to `this` variable 89 displayName: "runDebuggerStatement", 90 // arguments: [] 91 where: { 92 line: 17, 93 column: 6, 94 }, 95 }, 96 }); 97 98 const { threadFront } = targetCommand.targetFront; 99 await threadFront.resume(); 100 101 await waitFor( 102 () => availableResources.length == 1, 103 "Wait until we receive the resumed event" 104 ); 105 106 const resumed = availableResources.pop(); 107 108 assertResumedResource(resumed); 109 110 targetCommand.destroy(); 111 await commands.destroy(); 112 } 113 114 async function checkBreakpointAfterWatchResources() { 115 info( 116 "Check whether ResourceCommand gets breakpoint hit after calling watchResources" 117 ); 118 119 const tab = await addTab(BREAKPOINT_TEST_URL); 120 121 const { commands, resourceCommand, targetCommand } = 122 await initResourceCommand(tab); 123 124 info("Call watchResources"); 125 const availableResources = []; 126 await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], { 127 onAvailable: resources => availableResources.push(...resources), 128 }); 129 130 is( 131 availableResources.length, 132 0, 133 "Got no THREAD_STATE when calling watchResources" 134 ); 135 136 info("Run the 'debugger' statement"); 137 // Note that we do not wait for the resolution of spawn as it will be paused 138 ContentTask.spawn(tab.linkedBrowser, null, () => { 139 content.window.wrappedJSObject.runDebuggerStatement(); 140 }); 141 142 await waitFor( 143 () => availableResources.length == 1, 144 "Got the THREAD_STATE related to the debugger statement" 145 ); 146 const threadState = availableResources.pop(); 147 148 assertPausedResource(threadState, { 149 state: "paused", 150 why: { 151 type: "debuggerStatement", 152 }, 153 frame: { 154 type: "call", 155 asyncCause: null, 156 state: "on-stack", 157 // this: object actor's form referring to `this` variable 158 displayName: "runDebuggerStatement", 159 // arguments: [] 160 where: { 161 line: 17, 162 column: 6, 163 }, 164 }, 165 }); 166 167 // treadFront is created and attached while calling watchResources 168 const { threadFront } = targetCommand.targetFront; 169 170 await threadFront.resume(); 171 172 await waitFor( 173 () => availableResources.length == 1, 174 "Wait until we receive the resumed event" 175 ); 176 177 const resumed = availableResources.pop(); 178 179 assertResumedResource(resumed); 180 181 targetCommand.destroy(); 182 await commands.destroy(); 183 } 184 185 async function checkRealBreakpoint() { 186 info( 187 "Check whether ResourceCommand gets breakpoint set via the thread Front (instead of just debugger statements)" 188 ); 189 190 const tab = await addTab(BREAKPOINT_TEST_URL); 191 192 const { commands, resourceCommand, targetCommand } = 193 await initResourceCommand(tab); 194 195 info("Call watchResources"); 196 const availableResources = []; 197 await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], { 198 onAvailable: resources => availableResources.push(...resources), 199 }); 200 201 is( 202 availableResources.length, 203 0, 204 "Got no THREAD_STATE when calling watchResources" 205 ); 206 207 // treadFront is created and attached while calling watchResources 208 const { threadFront } = targetCommand.targetFront; 209 210 // We have to call `sources` request, otherwise the Thread Actor 211 // doesn't start watching for sources, and ignore the setBreakpoint call 212 // as it doesn't have any source registered. 213 await threadFront.getSources(); 214 215 await threadFront.setBreakpoint( 216 { sourceUrl: BREAKPOINT_TEST_URL, line: 14 }, 217 {} 218 ); 219 220 info("Run the test function where we set a breakpoint"); 221 // Note that we do not wait for the resolution of spawn as it will be paused 222 ContentTask.spawn(tab.linkedBrowser, null, () => { 223 content.window.wrappedJSObject.testFunction(); 224 }); 225 226 await waitFor( 227 () => availableResources.length == 1, 228 "Got the THREAD_STATE related to the debugger statement" 229 ); 230 const threadState = availableResources.pop(); 231 232 assertPausedResource(threadState, { 233 state: "paused", 234 why: { 235 type: "breakpoint", 236 }, 237 frame: { 238 type: "call", 239 asyncCause: null, 240 state: "on-stack", 241 // this: object actor's form referring to `this` variable 242 displayName: "testFunction", 243 // arguments: [] 244 where: { 245 line: 14, 246 column: 6, 247 }, 248 }, 249 }); 250 251 await threadFront.resume(); 252 253 await waitFor( 254 () => availableResources.length == 1, 255 "Wait until we receive the resumed event" 256 ); 257 258 const resumed = availableResources.pop(); 259 260 assertResumedResource(resumed); 261 262 targetCommand.destroy(); 263 await commands.destroy(); 264 } 265 266 async function checkPauseOnException() { 267 info( 268 "Check whether ResourceCommand gets breakpoint for exception (when explicitly requested)" 269 ); 270 271 const tab = await addTab( 272 "data:text/html,<meta charset=utf8><script>a.b.c.d</script>" 273 ); 274 275 const { commands, resourceCommand, targetCommand } = 276 await initResourceCommand(tab); 277 278 info("Call watchResources"); 279 const availableResources = []; 280 await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], { 281 onAvailable: resources => availableResources.push(...resources), 282 }); 283 284 is( 285 availableResources.length, 286 0, 287 "Got no THREAD_STATE when calling watchResources" 288 ); 289 290 await commands.threadConfigurationCommand.updateConfiguration({ 291 pauseOnExceptions: true, 292 }); 293 294 info("Reload the page, in order to trigger exception on load"); 295 const reloaded = reloadBrowser(); 296 297 await waitFor( 298 () => availableResources.length == 1, 299 "Got the THREAD_STATE related to the debugger statement" 300 ); 301 const threadState = availableResources.pop(); 302 303 assertPausedResource(threadState, { 304 state: "paused", 305 why: { 306 type: "exception", 307 }, 308 frame: { 309 type: "global", 310 asyncCause: null, 311 state: "on-stack", 312 // this: object actor's form referring to `this` variable 313 displayName: "(global)", 314 // arguments: [] 315 where: { 316 line: 1, 317 column: 27, 318 }, 319 }, 320 }); 321 322 const { threadFront } = targetCommand.targetFront; 323 await threadFront.resume(); 324 info("Wait for page to finish reloading after resume"); 325 await reloaded; 326 327 await waitFor( 328 () => availableResources.length == 1, 329 "Wait until we receive the resumed event" 330 ); 331 332 const resumed = availableResources.pop(); 333 334 assertResumedResource(resumed); 335 336 targetCommand.destroy(); 337 await commands.destroy(); 338 } 339 340 async function checkSetBeforeWatch() { 341 info( 342 "Verify bug 1683139 - D103068, where setting a breakpoint before watching for thread state, avoid receiving the paused state" 343 ); 344 345 const tab = await addTab(BREAKPOINT_TEST_URL); 346 347 const { commands, resourceCommand, targetCommand } = 348 await initResourceCommand(tab); 349 350 // Instantiate the thread front in order to be able to set a breakpoint before watching for thread state 351 info("Attach the top level thread actor"); 352 await targetCommand.targetFront.attachAndInitThread(targetCommand); 353 const { threadFront } = targetCommand.targetFront; 354 355 // We have to call `sources` request, otherwise the Thread Actor 356 // doesn't start watching for sources, and ignore the setBreakpoint call 357 // as it doesn't have any source registered. 358 await threadFront.getSources(); 359 360 // Set the breakpoint before trying to hit it 361 await threadFront.setBreakpoint( 362 { sourceUrl: BREAKPOINT_TEST_URL, line: 14, column: 6 }, 363 {} 364 ); 365 366 info("Run the test function where we set a breakpoint"); 367 // Note that we do not wait for the resolution of spawn as it will be paused 368 ContentTask.spawn(tab.linkedBrowser, null, () => { 369 content.window.wrappedJSObject.testFunction(); 370 }); 371 372 // bug 1683139 - D103068. Re-setting the breakpoint just before watching for thread state 373 // prevented to receive the paused state change. 374 await threadFront.setBreakpoint( 375 { sourceUrl: BREAKPOINT_TEST_URL, line: 14, column: 6 }, 376 {} 377 ); 378 379 info("Call watchResources"); 380 const availableResources = []; 381 await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], { 382 onAvailable: resources => availableResources.push(...resources), 383 }); 384 385 await waitFor(() => { 386 return availableResources.length == 1; 387 }, "Got the THREAD_STATE related to the debugger statement"); 388 const threadState = availableResources.pop(); 389 390 assertPausedResource(threadState, { 391 state: "paused", 392 why: { 393 type: "breakpoint", 394 }, 395 frame: { 396 type: "call", 397 asyncCause: null, 398 state: "on-stack", 399 // this: object actor's form referring to `this` variable 400 displayName: "testFunction", 401 // arguments: [] 402 where: { 403 line: 14, 404 column: 6, 405 }, 406 }, 407 }); 408 409 await threadFront.resume(); 410 411 await waitFor( 412 () => availableResources.length == 1, 413 "Wait until we receive the resumed event" 414 ); 415 416 const resumed = availableResources.pop(); 417 418 assertResumedResource(resumed); 419 420 targetCommand.destroy(); 421 await commands.destroy(); 422 } 423 424 async function checkDebuggerStatementInIframes() { 425 info("Check whether ResourceCommand gets breakpoint for (remote) iframes"); 426 427 const tab = await addTab(BREAKPOINT_TEST_URL); 428 429 const { commands, resourceCommand, targetCommand } = 430 await initResourceCommand(tab); 431 432 info("Call watchResources"); 433 const availableResources = []; 434 await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], { 435 onAvailable: resources => availableResources.push(...resources), 436 }); 437 438 is( 439 availableResources.length, 440 0, 441 "Got no THREAD_STATE when calling watchResources" 442 ); 443 444 info("Inject the iframe with an inline 'debugger' statement"); 445 // Note that we do not wait for the resolution of spawn as it will be paused 446 SpecialPowers.spawn( 447 gBrowser.selectedBrowser, 448 [REMOTE_IFRAME_URL], 449 async function (url) { 450 const iframe = content.document.createElement("iframe"); 451 iframe.src = url; 452 content.document.body.appendChild(iframe); 453 } 454 ); 455 456 await waitFor( 457 () => availableResources.length == 1, 458 "Got the THREAD_STATE related to the iframe's debugger statement" 459 ); 460 const threadState = availableResources.pop(); 461 462 assertPausedResource(threadState, { 463 state: "paused", 464 why: { 465 type: "debuggerStatement", 466 }, 467 frame: { 468 type: "global", 469 asyncCause: null, 470 state: "on-stack", 471 // this: object actor's form referring to `this` variable 472 displayName: "(global)", 473 // arguments: [] 474 where: { 475 line: 1, 476 column: 8, 477 }, 478 }, 479 }); 480 481 const iframeTarget = threadState.targetFront; 482 is( 483 iframeTarget.url, 484 REMOTE_IFRAME_URL, 485 "The pause is from the iframe's target" 486 ); 487 const { threadFront } = iframeTarget; 488 489 await threadFront.resume(); 490 491 await waitFor( 492 () => availableResources.length == 1, 493 "Wait until we receive the resumed event" 494 ); 495 496 const resumed = availableResources.pop(); 497 498 assertResumedResource(resumed); 499 500 targetCommand.destroy(); 501 await commands.destroy(); 502 } 503 504 async function testMultiprocessThreadState() { 505 // Ensure debugging the content processes and the tab 506 await pushPref("devtools.browsertoolbox.scope", "everything"); 507 508 const { commands, resourceCommand, targetCommand } = 509 await initMultiProcessResourceCommand(); 510 511 info("Call watchResources"); 512 const availableResources = []; 513 await resourceCommand.watchResources([resourceCommand.TYPES.SOURCE], { 514 onAvailable() {}, 515 }); 516 await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], { 517 onAvailable: resources => availableResources.push(...resources), 518 }); 519 520 is( 521 availableResources.length, 522 0, 523 "Got no THREAD_STATE when calling watchResources" 524 ); 525 526 const tab = await addTab(BREAKPOINT_TEST_URL); 527 528 info("Run the 'debugger' statement"); 529 // Note that we do not wait for the resolution of spawn as it will be paused 530 const onResumed = ContentTask.spawn(tab.linkedBrowser, null, () => { 531 content.window.wrappedJSObject.runDebuggerStatement(); 532 }); 533 534 await waitFor( 535 () => availableResources.length == 1, 536 "Got the THREAD_STATE related to the debugger statement" 537 ); 538 const threadState = availableResources.pop(); 539 ok(threadState.targetFront.targetType, "process"); 540 ok(threadState.targetFront.threadFront.state, "paused"); 541 542 assertPausedResource(threadState, { 543 state: "paused", 544 why: { 545 type: "debuggerStatement", 546 }, 547 frame: { 548 type: "call", 549 asyncCause: null, 550 state: "on-stack", 551 // this: object actor's form referring to `this` variable 552 displayName: "runDebuggerStatement", 553 // arguments: [] 554 where: { 555 line: 17, 556 column: 6, 557 }, 558 }, 559 }); 560 561 await threadState.targetFront.threadFront.resume(); 562 563 await waitFor( 564 () => availableResources.length == 1, 565 "Wait until we receive the resumed event" 566 ); 567 568 const resumed = availableResources.pop(); 569 570 assertResumedResource(resumed); 571 572 // This is an important check, which verify that no unexpected pause happens. 573 // We might spawn a Thread Actor for the WindowGlobal target, which might pause the thread a second time, 574 // whereas we only expect the ContentProcess target actor to pause on all JS files of the related content process. 575 info("Wait for the content page thread to resume its execution"); 576 await onResumed; 577 is(availableResources.length, 0, "There should be no other pause"); 578 579 targetCommand.destroy(); 580 await commands.destroy(); 581 } 582 583 async function assertPausedResource(resource, expected) { 584 is( 585 resource.resourceType, 586 ResourceCommand.TYPES.THREAD_STATE, 587 "Resource type is correct" 588 ); 589 is(resource.state, "paused", "state attribute is correct"); 590 is(resource.why.type, expected.why.type, "why.type attribute is correct"); 591 is( 592 resource.frame.type, 593 expected.frame.type, 594 "frame.type attribute is correct" 595 ); 596 is( 597 resource.frame.asyncCause, 598 expected.frame.asyncCause, 599 "frame.asyncCause attribute is correct" 600 ); 601 is( 602 resource.frame.state, 603 expected.frame.state, 604 "frame.state attribute is correct" 605 ); 606 is( 607 resource.frame.displayName, 608 expected.frame.displayName, 609 "frame.displayName attribute is correct" 610 ); 611 is( 612 resource.frame.where.line, 613 expected.frame.where.line, 614 "frame.where.line attribute is correct" 615 ); 616 is( 617 resource.frame.where.column, 618 expected.frame.where.column, 619 "frame.where.column attribute is correct" 620 ); 621 } 622 623 async function assertResumedResource(resource) { 624 is( 625 resource.resourceType, 626 ResourceCommand.TYPES.THREAD_STATE, 627 "Resource type is correct" 628 ); 629 is(resource.state, "resumed", "state attribute is correct"); 630 }