mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-18 06:13:55 -07:00
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.
185 lines
4.2 KiB
JavaScript
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);
|
|
}
|
|
}
|