Source code
Revision control
Copy as Markdown
Other Tools
/* Any copyright is dedicated to the Public Domain.
/* eslint-disable no-unused-vars, no-undef */
"use strict";
const { BrowserToolboxLauncher } = ChromeUtils.importESModule(
"resource://devtools/client/framework/browser-toolbox/Launcher.sys.mjs"
);
const {
DevToolsClient,
} = require("resource://devtools/client/devtools-client.js");
/**
* Open up a browser toolbox and return a ToolboxTask object for interacting
* with it. ToolboxTask has the following methods:
*
* importFunctions(object)
*
* The object contains functions from this process which should be defined in
* the global evaluation scope of the toolbox. The toolbox cannot load testing
* files directly.
*
* destroy()
*
* Destroy the browser toolbox and make sure it exits cleanly.
*
* @param {Object}:
* - {Function} existingProcessClose: if truth-y, connect to an existing
* browser toolbox process rather than launching a new one and
* connecting to it. The given function is expected to return an
* object containing an `exitCode`, like `{exitCode}`, and will be
* awaited in the returned `destroy()` function. `exitCode` is
* asserted to be 0 (success).
*/
async function initBrowserToolboxTask({ existingProcessClose } = {}) {
if (AppConstants.ASAN) {
ok(
false,
);
}
await pushPref("devtools.chrome.enabled", true);
await pushPref("devtools.debugger.remote-enabled", true);
await pushPref("devtools.browsertoolbox.enable-test-server", true);
await pushPref("devtools.debugger.prompt-connection", false);
// This rejection seems to affect all tests using the browser toolbox.
ChromeUtils.importESModule(
).PromiseTestUtils.allowMatchingRejectionsGlobally(/File closed/);
let process;
let dbgProcess;
if (!existingProcessClose) {
[process, dbgProcess] = await new Promise(resolve => {
BrowserToolboxLauncher.init({
onRun: (_process, _dbgProcess) => resolve([_process, _dbgProcess]),
overwritePreferences: true,
});
});
ok(true, "Browser toolbox started");
is(
BrowserToolboxLauncher.getBrowserToolboxSessionState(),
true,
"Has session state"
);
} else {
ok(true, "Connecting to existing browser toolbox");
}
// The port of the DevToolsServer installed in the toolbox process is fixed.
// See browser-toolbox/window.js
let transport;
while (true) {
try {
transport = await DevToolsClient.socketConnect({
host: "localhost",
port: 6001,
webSocket: false,
});
break;
} catch (e) {
await waitForTime(100);
}
}
ok(true, "Got transport");
const client = new DevToolsClient(transport);
await client.connect();
const commands = await CommandsFactory.forMainProcess({ client });
const target = await commands.descriptorFront.getTarget();
const consoleFront = await target.getFront("console");
ok(true, "Connected");
await importFunctions({
info: msg => dump(msg + "\n"),
is: (a, b, description) => {
let msg =
"'" + JSON.stringify(a) + "' is equal to '" + JSON.stringify(b) + "'";
if (description) {
msg += " - " + description;
}
if (a !== b) {
msg = "FAILURE: " + msg;
dump(msg + "\n");
throw new Error(msg);
} else {
msg = "SUCCESS: " + msg;
dump(msg + "\n");
}
},
ok: (a, description) => {
let msg = "'" + JSON.stringify(a) + "' is true";
if (description) {
msg += " - " + description;
}
if (!a) {
msg = "FAILURE: " + msg;
dump(msg + "\n");
throw new Error(msg);
} else {
msg = "SUCCESS: " + msg;
dump(msg + "\n");
}
},
});
async function evaluateExpression(expression, options = {}) {
const onEvaluationResult = consoleFront.once("evaluationResult");
await consoleFront.evaluateJSAsync({ text: expression, ...options });
return onEvaluationResult;
}
/**
* Invoke the given function and argument(s) within the global evaluation scope
* of the toolbox. The evaluation scope predefines the name "gToolbox" for the
* toolbox itself.
*
* @param {value|Array<value>} arg
* If an Array is passed, we will consider it as the list of arguments
* to pass to `fn`. Otherwise we will consider it as the unique argument
* to pass to it.
* @param {Function} fn
* Function to call in the global scope within the browser toolbox process.
* This function will be stringified and passed to the process via RDP.
* @return {Promise<Value>}
* Return the primitive value returned by `fn`.
*/
async function spawn(arg, fn) {
// Use JSON.stringify to ensure that we can pass strings
// as well as any JSON-able object.
const argString = JSON.stringify(Array.isArray(arg) ? arg : [arg]);
const rv = await evaluateExpression(`(${fn}).apply(null,${argString})`, {
// Use the following argument in order to ensure waiting for the completion
// of the promise returned by `fn` (in case this is an async method).
mapped: { await: true },
});
if (rv.exceptionMessage) {
throw new Error(`ToolboxTask.spawn failure: ${rv.exceptionMessage}`);
} else if (rv.topLevelAwaitRejected) {
throw new Error(`ToolboxTask.spawn await rejected`);
}
return rv.result;
}
async function importFunctions(functions) {
for (const [key, fn] of Object.entries(functions)) {
await evaluateExpression(`this.${key} = ${fn}`);
}
}
async function importScript(script) {
const response = await evaluateExpression(script);
if (response.hasException) {
ok(
false,
"ToolboxTask.spawn exception while importing script: " +
response.exceptionMessage
);
}
}
let destroyed = false;
async function destroy() {
// No need to do anything if `destroy` was already called.
if (destroyed) {
return;
}
const closePromise = existingProcessClose
? existingProcessClose()
: dbgProcess.wait();
evaluateExpression("gToolbox.destroy()").catch(e => {
// Ignore connection close as the toolbox destroy may destroy
// everything quickly enough so that evaluate request is still pending
if (!e.message.includes("Connection closed")) {
throw e;
}
});
const { exitCode } = await closePromise;
ok(true, "Browser toolbox process closed");
is(exitCode, 0, "The remote debugger process died cleanly");
if (!existingProcessClose) {
is(
BrowserToolboxLauncher.getBrowserToolboxSessionState(),
false,
"No session state after closing"
);
}
await commands.destroy();
destroyed = true;
}
// When tests involving using this task fail, the spawned Browser Toolbox is not
// destroyed and might impact the next tests (e.g. pausing the content process before
// the debugger from the content toolbox does). So make sure to cleanup everything.
registerCleanupFunction(destroy);
return {
importFunctions,
importScript,
spawn,
destroy,
};
}