Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* JS shell host for the DebugSession module in DebugSession.sys.mjs.
* See the SMDOC comment in DebugSession.cpp.
*
* This script evaluates to a function that takes the module namespace and
* returns the entry points called by the natives:
*
* create(debuggee) Create the debug session. There can only be one
* at a time.
* command(name, argJSON) Commands of the session. Not allowed while it is
* paused.
*/
(function (ns) {
"use strict";
const { DebugSession } = ns;
const ACTIONS = ["resume", "stepInto", "stepOut", "stepOver"];
// The state of the current session, or null.
let current = null;
/**
* Errors reported to the natives as {error}.
*/
class HostError extends Error {}
function serializeValue(value) {
if (value instanceof Debugger.Object) {
return { type: "object", class: value.class };
}
switch (typeof value) {
case "undefined":
return { type: "undefined" };
case "boolean":
case "string":
return { type: typeof value, value };
case "number":
if (!Number.isFinite(value) || Object.is(value, -0)) {
return { type: "number", value: String(value) };
}
return { type: "number", value };
case "bigint":
return { type: "bigint", value: String(value) };
case "symbol":
return { type: "symbol", description: value.description ?? null };
}
if (value === null) {
return { type: "null" };
}
return { type: "unknown" };
}
function checkBreakpoints(arg) {
if (!Array.isArray(arg)) {
throw new HostError("setBreakpoints expects an array");
}
const breakpoints = [];
for (const bp of arg) {
if (typeof bp !== "object" || bp === null) {
throw new HostError("breakpoint must be an object");
}
const { id, url, line, column } = bp;
if (typeof id !== "string" && typeof id !== "number") {
throw new HostError("breakpoint id must be a string or number");
}
if (typeof url !== "string") {
throw new HostError("breakpoint url must be a string");
}
if (!Number.isInteger(line) || line < 1) {
throw new HostError("breakpoint line must be a positive integer");
}
if (column !== undefined && (!Number.isInteger(column) || column < 1)) {
throw new HostError("breakpoint column must be a positive integer");
}
breakpoints.push({ id: String(id), url, line, column });
}
return breakpoints;
}
function checkActions(arg) {
if (!Array.isArray(arg)) {
throw new HostError("setPauseActions expects an array");
}
const actions = [];
for (const action of arg) {
if (typeof action === "string") {
if (!ACTIONS.includes(action)) {
throw new HostError("unknown pause action: " + action);
}
actions.push(action);
continue;
}
if (typeof action !== "object" || action === null) {
throw new HostError("pause action must be a string or an object");
}
const { evaluate: expression, frame = 0 } = action;
if (typeof expression !== "string") {
throw new HostError("evaluate action expects an expression string");
}
if (!Number.isInteger(frame) || frame < 0) {
throw new HostError("frame index must be a non-negative integer");
}
actions.push({ evaluate: expression, frame });
}
return actions;
}
function evaluate(entry, expression, frameIndex) {
let frame = entry.environment?.frame;
for (let i = 0; frame && i < frameIndex; i++) {
frame = frame.older;
}
if (!frame) {
return { error: "no frame with index " + frameIndex };
}
const completion = frame.eval(expression);
if (!completion) {
return { terminated: true };
}
if ("throw" in completion) {
return { throw: serializeValue(completion.throw) };
}
return { return: serializeValue(completion.return) };
}
// Run the pause actions of |entry| until a resume or step action, and return
// that action.
function runPauseActions(entry) {
while (entry.actions.length) {
const action = entry.actions.shift();
if (typeof action === "string") {
return action;
}
const { evaluate: expression, frame } = action;
const logEntry = { type: "evaluate", expression, frame, result: null };
entry.log.push(logEntry);
logEntry.result = evaluate(entry, expression, frame);
}
return "resume";
}
function create(debuggee) {
if (current) {
throw new HostError("a debug session already exists");
}
const entry = {
actions: [],
environment: null,
log: [],
pausedInfo: null,
session: null,
};
entry.session = new DebugSession(debuggee, {
createDebugger: () => new Debugger(),
waitWhilePaused() {
const logEntry = { type: "paused", ...entry.pausedInfo, action: null };
entry.log.push(logEntry);
const action = runPauseActions(entry);
logEntry.action = action;
entry.session[action]();
},
serializeValue,
serializeUnavailableVariable(kind) {
return { type: kind };
},
createError(kind, message) {
return new HostError(kind + ": " + message);
},
setDebuggerEnvironment(environment) {
entry.environment = environment;
},
onPaused(info) {
entry.pausedInfo = info;
},
onResumed() {
entry.pausedInfo = null;
entry.log.push({ type: "resumed" });
},
});
current = entry;
}
function command(name, arg) {
const entry = current;
if (!entry) {
throw new HostError("no debug session");
}
const { session } = entry;
// Evaluated debuggee code must not change the session during a pause.
if (session.paused) {
throw new HostError(name + " is not allowed while the session is paused");
}
switch (name) {
case "enable":
if (!session.enable()) {
session.disable();
throw new HostError("can't debug this global");
}
return undefined;
case "disable":
session.disable();
return undefined;
case "destroy":
current = null;
session.destroy();
return undefined;
case "setBreakpoints":
session.setBreakpoints(checkBreakpoints(arg));
return undefined;
case "setPauseOnExceptions":
if (typeof arg !== "string") {
throw new HostError("setPauseOnExceptions expects a mode string");
}
session.setPauseOnExceptions(arg);
return undefined;
case "setPauseActions":
entry.actions = checkActions(arg);
return undefined;
case "listScripts":
return session.listScripts();
case "getScriptSource":
if (typeof arg !== "string") {
throw new HostError("getScriptSource expects a URL string");
}
return session.getScriptSource(arg);
case "takeLog": {
const log = entry.log;
entry.log = [];
return log;
}
}
throw new HostError("unknown command: " + name);
}
function errorMessage(e) {
if (e instanceof Error) {
return e.message;
}
return "unknown error";
}
// The entry points only return JSON strings. Arguments arrive as JSON strings
// and errors are reported as {error} instead of throwing objects of this
// global.
return {
create(debuggee) {
try {
create(debuggee);
return JSON.stringify({ result: undefined });
} catch (e) {
return JSON.stringify({ error: errorMessage(e) });
}
},
command(name, argJSON) {
try {
const arg = argJSON === undefined ? undefined : JSON.parse(argJSON);
return JSON.stringify({ result: command(name, arg) });
} catch (e) {
return JSON.stringify({ error: errorMessage(e) });
}
},
};
});