tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }