tor-browser

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

helpers-browser-toolbox.js (7277B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 /* eslint-disable no-unused-vars, no-undef */
      4 
      5 "use strict";
      6 
      7 const { BrowserToolboxLauncher } = ChromeUtils.importESModule(
      8  "resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs"
      9 );
     10 const {
     11  DevToolsClient,
     12 } = require("resource://devtools/client/devtools-client.js");
     13 
     14 /**
     15 * Open up a browser toolbox and return a ToolboxTask object for interacting
     16 * with it. ToolboxTask has the following methods:
     17 *
     18 * importFunctions(object)
     19 *
     20 *   The object contains functions from this process which should be defined in
     21 *   the global evaluation scope of the toolbox. The toolbox cannot load testing
     22 *   files directly.
     23 *
     24 * destroy()
     25 *
     26 *   Destroy the browser toolbox and make sure it exits cleanly.
     27 *
     28 * @param {object}:
     29 *        - {Function} existingProcessClose: if truth-y, connect to an existing
     30 *          browser toolbox process rather than launching a new one and
     31 *          connecting to it.  The given function is expected to return an
     32 *          object containing an `exitCode`, like `{exitCode}`, and will be
     33 *          awaited in the returned `destroy()` function.  `exitCode` is
     34 *          asserted to be 0 (success).
     35 */
     36 async function initBrowserToolboxTask({ existingProcessClose } = {}) {
     37  await pushPref("devtools.chrome.enabled", true);
     38  await pushPref("devtools.debugger.remote-enabled", true);
     39  await pushPref("devtools.browsertoolbox.enable-test-server", true);
     40  await pushPref("devtools.debugger.prompt-connection", false);
     41 
     42  // This rejection seems to affect all tests using the browser toolbox.
     43  ChromeUtils.importESModule(
     44    "resource://testing-common/PromiseTestUtils.sys.mjs"
     45  ).PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
     46 
     47  let process;
     48  let dbgProcess;
     49  if (!existingProcessClose) {
     50    [process, dbgProcess] = await new Promise(resolve => {
     51      BrowserToolboxLauncher.init({
     52        onRun: (_process, _dbgProcess) => resolve([_process, _dbgProcess]),
     53        overwritePreferences: true,
     54      });
     55    });
     56    ok(true, "Browser toolbox started");
     57    is(
     58      BrowserToolboxLauncher.getBrowserToolboxSessionState(),
     59      true,
     60      "Has session state"
     61    );
     62  } else {
     63    ok(true, "Connecting to existing browser toolbox");
     64  }
     65 
     66  // The port of the DevToolsServer installed in the toolbox process is fixed.
     67  // See browser-toolbox/window.js
     68  let transport;
     69  while (true) {
     70    try {
     71      transport = await DevToolsClient.socketConnect({
     72        host: "localhost",
     73        port: 6001,
     74        webSocket: false,
     75      });
     76      break;
     77    } catch (e) {
     78      await waitForTime(100);
     79    }
     80  }
     81  ok(true, "Got transport");
     82 
     83  const client = new DevToolsClient(transport);
     84  await client.connect();
     85 
     86  const commands = await CommandsFactory.forMainProcess({ client });
     87  const target = await commands.descriptorFront.getTarget();
     88  const consoleFront = await target.getFront("console");
     89 
     90  ok(true, "Connected");
     91 
     92  await importFunctions({
     93    info: msg => dump(msg + "\n"),
     94    is: (a, b, description) => {
     95      let msg =
     96        "'" + JSON.stringify(a) + "' is equal to '" + JSON.stringify(b) + "'";
     97      if (description) {
     98        msg += " - " + description;
     99      }
    100      if (a !== b) {
    101        msg = "FAILURE: " + msg;
    102        dump(msg + "\n");
    103        throw new Error(msg);
    104      } else {
    105        msg = "SUCCESS: " + msg;
    106        dump(msg + "\n");
    107      }
    108    },
    109    ok: (a, description) => {
    110      let msg = "'" + JSON.stringify(a) + "' is true";
    111      if (description) {
    112        msg += " - " + description;
    113      }
    114      if (!a) {
    115        msg = "FAILURE: " + msg;
    116        dump(msg + "\n");
    117        throw new Error(msg);
    118      } else {
    119        msg = "SUCCESS: " + msg;
    120        dump(msg + "\n");
    121      }
    122    },
    123  });
    124 
    125  async function evaluateExpression(expression, options = {}) {
    126    const onEvaluationResult = consoleFront.once("evaluationResult");
    127    await consoleFront.evaluateJSAsync({ text: expression, ...options });
    128    return onEvaluationResult;
    129  }
    130 
    131  /**
    132   * Invoke the given function and argument(s) within the global evaluation scope
    133   * of the toolbox. The evaluation scope predefines the name "gToolbox" for the
    134   * toolbox itself.
    135   *
    136   * @param {value|Array<value>} arg
    137   *        If an Array is passed, we will consider it as the list of arguments
    138   *        to pass to `fn`. Otherwise we will consider it as the unique argument
    139   *        to pass to it.
    140   * @param {Function} fn
    141   *        Function to call in the global scope within the browser toolbox process.
    142   *        This function will be stringified and passed to the process via RDP.
    143   * @return {Promise<Value>}
    144   *        Return the primitive value returned by `fn`.
    145   */
    146  async function spawn(arg, fn) {
    147    // Use JSON.stringify to ensure that we can pass strings
    148    // as well as any JSON-able object.
    149    const argString = JSON.stringify(Array.isArray(arg) ? arg : [arg]);
    150    const rv = await evaluateExpression(`(${fn}).apply(null,${argString})`, {
    151      // Use the following argument in order to ensure waiting for the completion
    152      // of the promise returned by `fn` (in case this is an async method).
    153      mapped: { await: true },
    154    });
    155    if (rv.exceptionMessage) {
    156      throw new Error(`ToolboxTask.spawn failure: ${rv.exceptionMessage}`);
    157    } else if (rv.topLevelAwaitRejected) {
    158      throw new Error(`ToolboxTask.spawn await rejected`);
    159    }
    160    return rv.result;
    161  }
    162 
    163  async function importFunctions(functions) {
    164    for (const [key, fn] of Object.entries(functions)) {
    165      await evaluateExpression(`this.${key} = ${fn}`);
    166    }
    167  }
    168 
    169  async function importScript(script) {
    170    const response = await evaluateExpression(script);
    171    if (response.hasException) {
    172      ok(
    173        false,
    174        "ToolboxTask.spawn exception while importing script: " +
    175          response.exceptionMessage
    176      );
    177    }
    178  }
    179 
    180  let destroyed = false;
    181  async function destroy() {
    182    // No need to do anything if `destroy` was already called.
    183    if (destroyed) {
    184      return;
    185    }
    186 
    187    const closePromise = existingProcessClose
    188      ? existingProcessClose()
    189      : dbgProcess.wait();
    190    evaluateExpression("gToolbox.destroy()").catch(e => {
    191      // Ignore connection close as the toolbox destroy may destroy
    192      // everything quickly enough so that evaluate request is still pending
    193      if (!e.message.includes("Connection closed")) {
    194        throw e;
    195      }
    196    });
    197 
    198    const { exitCode } = await closePromise;
    199    ok(true, "Browser toolbox process closed");
    200 
    201    is(exitCode, 0, "The remote debugger process died cleanly");
    202 
    203    if (!existingProcessClose) {
    204      is(
    205        BrowserToolboxLauncher.getBrowserToolboxSessionState(),
    206        false,
    207        "No session state after closing"
    208      );
    209    }
    210 
    211    await commands.destroy();
    212    destroyed = true;
    213  }
    214 
    215  // When tests involving using this task fail, the spawned Browser Toolbox is not
    216  // destroyed and might impact the next tests (e.g. pausing the content process before
    217  // the debugger from the content toolbox does). So make sure to cleanup everything.
    218  registerCleanupFunction(destroy);
    219 
    220  return {
    221    importFunctions,
    222    importScript,
    223    spawn,
    224    destroy,
    225  };
    226 }