Files
servo/resources/debugger.js
atbrakhi d6097d1967 devtools: Implement debugger Eval event (#42306)
Replace the `EvaluateJS` function to use `debugger.js` `Eval` event and
the proper context. This takes the global context and calls
[executeInGlobal](https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Object.html#executeinglobal-code-options).

In future we also add the version that takes a frame and calls
[eval](https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Frame.html#eval-code-options).

Testing: No new tests added yet, Old tests are not impacted by this
change.
Fixes: Part of https://github.com/servo/servo/issues/36027

---------

Signed-off-by: atbrakhi <atbrakhi@igalia.com>
Co-authored-by: eri <eri@igalia.com>
2026-02-09 12:58:51 +00:00

168 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
if ("dbg" in this) {
throw new Error("Debugger script must not run more than once!");
}
const dbg = new Debugger;
const debuggeesToPipelineIds = new Map;
const debuggeesToWorkerIds = new Map;
const sourceIdsToScripts = new Map;
// Find script by scriptId within a script tree
function findScriptById(script, scriptId) {
if (script.sourceStart === scriptId) {
return script;
}
for (const child of script.getChildScripts()) {
const found = findScriptById(child, scriptId);
if (found) return found;
}
return null;
}
// Walk script tree and call callback for each script
function walkScriptTree(script, callback) {
callback(script);
for (const child of script.getChildScripts()) {
walkScriptTree(child, callback);
}
}
dbg.uncaughtExceptionHook = function(error) {
console.error(`[debugger] Uncaught exception at ${error.fileName}:${error.lineNumber}:${error.columnNumber}: ${error.name}: ${error.message}`);
};
dbg.onNewScript = function(script) {
// TODO: handle wasm (`script.source.introductionType == wasm`)
sourceIdsToScripts.set(script.source.id, script);
notifyNewSource({
pipelineId: debuggeesToPipelineIds.get(script.global),
workerId: debuggeesToWorkerIds.get(script.global),
spidermonkeyId: script.source.id,
url: script.source.url,
urlOverride: script.source.displayURL,
text: script.source.text,
introductionType: script.source.introductionType ?? null,
});
};
addEventListener("addDebuggee", event => {
const {global, pipelineId: {namespaceId, index}, workerId} = event;
const debuggerObject = dbg.addDebuggee(global);
debuggeesToPipelineIds.set(debuggerObject, { namespaceId, index });
debuggeesToWorkerIds.set(debuggerObject, workerId);
});
// Create a result value object from a debuggee value.
// Debuggee values: <https://firefox-source-docs.mozilla.org/js/Debugger/Conventions.html#debuggee-values>
// Type detection follows Firefox's createValueGrip pattern:
// <https://searchfox.org/mozilla-central/source/devtools/server/actors/object/utils.js#116>
function createValueResult(value) {
switch (typeof value) {
case "undefined":
return { valueType: "undefined" };
case "boolean":
return { valueType: "boolean", booleanValue: value };
case "number":
return { valueType: "number", numberValue: value };
case "string":
return { valueType: "string", stringValue: value };
case "object":
if (value === null) {
return { valueType: "null" };
}
// Debugger.Object - use the `class` accessor property
// <https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Object.html>
return { valueType: "object", objectClass: value.class };
default:
return { valueType: "string", stringValue: String(value) };
}
}
// <https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Object.html#executeinglobal-code-options>
addEventListener("eval", event => {
const {code, pipelineId: {namespaceId, index}, workerId} = event;
let object = debuggeesToPipelineIds.keys().next().value;
let completionValue = object.executeInGlobal(code);
// Completion values: <https://firefox-source-docs.mozilla.org/js/Debugger/Conventions.html#completion-values>
let resultValue;
if (completionValue === null) {
resultValue = { completionType: "terminated", valueType: "undefined" };
} else if ("throw" in completionValue) {
// Adopt the value to ensure proper Debugger ownership
// <https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.html#adoptdebuggeevalue-value>
// <https://searchfox.org/firefox-main/source/devtools/server/actors/webconsole/eval-with-debugger.js#312>
// we probably don't need adoptDebuggeeValue, as we only have one debugger instance for now
// let value = dbg.adoptDebuggeeValue(completionValue.throw);
resultValue = { completionType: "throw", ...createValueResult(completionValue.throw) };
} else if ("return" in completionValue) {
// let value = dbg.adoptDebuggeeValue(completionValue.return);
resultValue = { completionType: "return", ...createValueResult(completionValue.return) };
}
evalResult(event, resultValue);
});
addEventListener("getPossibleBreakpoints", event => {
const {spidermonkeyId} = event;
const script = sourceIdsToScripts.get(spidermonkeyId);
const result = [];
walkScriptTree(script, (currentScript) => {
for (const location of currentScript.getPossibleBreakpoints()) {
location["scriptId"] = currentScript.sourceStart;
result.push(location);
}
});
getPossibleBreakpointsResult(event, result);
});
addEventListener("setBreakpoint", event => {
const {spidermonkeyId, scriptId, offset} = event;
const script = sourceIdsToScripts.get(spidermonkeyId);
const target = findScriptById(script, scriptId);
if (target) {
target.setBreakpoint(offset, {
hit: () => {
// <https://firefox-source-docs.mozilla.org/js/Debugger/Conventions.html#resumption-values>
// TODO: notify script to pause
return { throw: "1" };
}
});
}
});
// <https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Frame.html>
addEventListener("pause", event => {
dbg.onEnterFrame = function(frame) {
dbg.onEnterFrame = undefined;
// TODO: Some properties throw if terminated is true
// TODO: Check if start line / column is correct or we need the proper breakpoint
const result = {
// TODO: arguments: frame.arguments,
column: frame.script.startColumn,
displayName: frame.script.displayName,
line: frame.script.startLine,
onStack: frame.onStack,
oldest: frame.older == null,
terminated: frame.terminated,
type_: frame.type,
url: frame.script.url,
};
getFrameResult(event, result);
};
});
// <https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Script.html#clearbreakpoint-handler-offset>
// There may be more than one breakpoint at the same offset with different handlers, but we dont handle that case for now.
addEventListener("clearBreakpoint", event => {
const {spidermonkeyId, scriptId, offset} = event;
const script = sourceIdsToScripts.get(spidermonkeyId);
const target = findScriptById(script, scriptId);
if (target) {
// <https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Script.html#clearallbreakpoints-offset>
// If the instance refers to a JSScript, remove all breakpoints set in this script at that offset.
target.clearAllBreakpoints(offset);
}
});