0ad/binaries/data/mods/mod/tools/dap/jsdebugger.js
trompetin17 20b7c3f9b8
Implement Debug Adapter Protocol (DAP) Interface in debugger.js
Adds initial support for the Debug Adapter Protocol (DAP) to
SpiderMonkey via debugger.js. This JavaScript-based implementation
enables external debuggers (e.g., VS Code) to interact with the JS
runtime using the DAP interface.

Implemented DAP requests and events:

- attach
- initialize (capabilities)
- stopped event
- threads
- scopes
- variables (globalThis pending)
- continue
- stepIn
- stepOut
- setBreakpoints
- Handling of debugger statements

This forms the foundation for interactive debugging of in-game scripts,
providing smoother integration with developer tools.
2025-07-11 11:06:05 -05:00

185 lines
4.2 KiB
JavaScript

import { logger } from 'tools/dap/logger.js';
export class JsDebugger {
constructor() {
this.debugger = new Debugger();
this.logger = logger.getLogger("SpiderDebugger");
this.events = [];
this.sourcesReferences = [];
this.currentFrame = null;
this.hooks = {
'onDebuggerAttached': [],
'onDebuggerDetached': [],
'onNewGlobalObject': [],
'onDebuggerStatement': [],
'onNewScript': [],
'onEnterFrame': [],
'onUncaughtException': [],
'onStopInFrame': [],
'onRsumeInFrame': [],
};
this.debugger.uncaughtExceptionHook = (e) => {
this._runHooks('onUncaughtException', e);
};
this.debugger.onNewGlobalObject = (global) => {
this._runHooks('onNewGlobalObject', global);
};
this.debugger.onDebuggerStatement = (frame) => {
this._runHooks('onDebuggerStatement', frame);
};
this.debugger.onNewScript = (script, global) => {
this._runHooks('onNewScript', { script, global });
};
this.debugger.onEnterFrame = (frame) => {
this._runHooks('onEnterFrame', frame);
};
this.debuggerAttached = false;
}
_runHooks(event, data) {
this.logger.trace(`Running hook for ${event}`);
for (const hookInfo of this.hooks[event])
{
this.logger.trace(`Running hook for ${hookInfo.source}-${event}`);
if (typeof hookInfo.callback !== 'function')
{
this.logger.warn(`Hook for ${event} is not a function: ${hook.source}`);
continue;
}
try {
hookInfo.callback(data);
} catch (e) {
this.logger.error(`Error in hook for ${hookInfo.source}-${event}: ${e.message}`);
this.logger.error(uneval(e.stack));
}
}
}
on(event, callback, source) {
if (!event || typeof event !== 'string')
{
this.logger.warn('Invalid event name');
return;
}
if (!this.hooks[event])
{
this.logger.warn(`No hooks registered for event: ${event}`);
return;
}
if (!source || typeof source !== 'string')
{
this.logger.warn(`Invalid source name for event ${event}`);
return;
}
if (typeof callback !== 'function')
{
this.logger.warn(`Callback for event ${source}:${event} is not a function`);
return;
}
this.hooks[event].push({ callback, source });
this.logger.debug(`Hook added for event: ${event}`);
}
get instance() {
return this.debugger;
}
setAttached(attached) {
this.debuggerAttached = attached;
if (attached)
{
this.logger.debug("Debugger attached");
this._runHooks('onDebuggerAttached', {});
}
else
{
this.logger.debug("Debugger detached");
this._runHooks('onDebuggerDetached', {});
}
}
pushEvent(eventName, eventData, source) {
if (!eventName || typeof eventName !== 'string')
{
this.logger.warn('Invalid event name');
return;
}
if (source && typeof source !== 'string')
{
this.logger.warn('Invalid source name');
return;
}
this.logger.debug(`Pushing event: ${source}-${eventName}`);
this.events.push({
'type': 'event',
'event': eventName,
'body': eventData
});
}
stopInframe(frame, onHandler) {
if (!frame || !(frame instanceof Debugger.Frame))
{
this.logger.error('Invalid frame provided to stopInframe');
return;
}
this.currentFrame = frame;
frame.currentLocation = frame.script.getOffsetLocation(frame.offset);
this.logger.debug(`Stop at ${frame.script.url}:${frame.currentLocation.lineNumber}:${frame.currentLocation.columnNumber}`);
this.logger.debug(`Frame type: ${frame.type}`);
if (onHandler && typeof onHandler === 'function')
onHandler();
this._runHooks('onStopInFrame', frame);
Engine.WaitForMessage();
this._runHooks('onRsumeInFrame', frame);
this.logger.debug("Client continue");
}
registerHookName(event, source) {
if (!event || typeof event !== 'string')
{
this.logger.warn('Invalid event name');
return;
}
if (!source || typeof source !== 'string')
{
this.logger.warn(`Invalid source name for ${event}`);
return;
}
if (this.hooks[event])
{
this.logger.warn(`Hooks already registered for event: ${event}`);
return;
}
this.hooks[event] = [];
this.logger.debug(`Hook registered for event: ${event} from source: ${source}`);
}
triggerHook(event, data) {
if (!event || typeof event !== 'string')
{
this.logger.warn('Invalid event name');
return;
}
this._runHooks(event, data);
}
}