breakpoints working in debug adapter
authorBrendan Hansen <brendan.f.hansen@gmail.com>
Wed, 10 Aug 2022 18:01:48 +0000 (13:01 -0500)
committerBrendan Hansen <brendan.f.hansen@gmail.com>
Wed, 10 Aug 2022 18:01:48 +0000 (13:01 -0500)
lib/linux_x86_64/lib/libovmwasm.so
misc/vscode/debugAdapter.ts [new file with mode: 0644]
misc/vscode/onyx-0.0.3.vsix
misc/vscode/out/debugAdapter.js [new file with mode: 0644]
misc/vscode/out/ovmDebug.js [new file with mode: 0644]
misc/vscode/ovmDebug.ts [new file with mode: 0644]
misc/vscode/package-lock.json
misc/vscode/package.json
misc/vscode/tsconfig.json

index 45af884fa05fe89e1b54fe6e513926e131961c62..d11cf846e0c76a7cda66d2669c9728884db050cc 100755 (executable)
Binary files a/lib/linux_x86_64/lib/libovmwasm.so and b/lib/linux_x86_64/lib/libovmwasm.so differ
diff --git a/misc/vscode/debugAdapter.ts b/misc/vscode/debugAdapter.ts
new file mode 100644 (file)
index 0000000..f8a2c80
--- /dev/null
@@ -0,0 +1,42 @@
+// Part of this code was taken from the example vscode debugger found here,
+// https://github.com/microsoft/vscode-mock-debug/blob/5524e7a3dd9ec176a987ffe3aebd489f1f543799/src/debugAdapter.ts`
+
+import { OVMDebugSession } from './ovmDebug';
+
+import * as Net from 'node:net';
+/*
+ * When the debug adapter is run as an external process,
+ * normally the helper function DebugSession.run(...) takes care of everything:
+ *
+ *     MockDebugSession.run(MockDebugSession);
+ *
+ * but here the helper is not flexible enough to deal with a debug session constructors with a parameter.
+ * So for now we copied and modified the helper:
+ */
+
+// first parse command line arguments to see whether the debug adapter should run as a server
+let port = 0;
+const args = process.argv.slice(2);
+args.forEach(function (val, index, array) {
+       const portMatch = /^--server=(\d{4,5})$/.exec(val);
+       if (portMatch) {
+               port = parseInt(portMatch[1], 10);
+       }
+});
+
+if (port > 0) {
+       Net.createServer((socket) => {
+               const session = new OVMDebugSession();
+               session.setRunAsServer(true);
+               session.start(socket, socket);
+       }).listen(port);
+
+} else {
+
+       // start a single session that communicates via stdin/stdout
+       const session = new OVMDebugSession();
+       process.on('SIGTERM', () => {
+               session.shutdown();
+       });
+       session.start(process.stdin, process.stdout);
+}
\ No newline at end of file
index 6768cd37cd54653daa1aedc129809523e40fc0e8..8c1803e340bd088f7d51146cb3f7d845893d4b04 100644 (file)
Binary files a/misc/vscode/onyx-0.0.3.vsix and b/misc/vscode/onyx-0.0.3.vsix differ
diff --git a/misc/vscode/out/debugAdapter.js b/misc/vscode/out/debugAdapter.js
new file mode 100644 (file)
index 0000000..2b08cad
--- /dev/null
@@ -0,0 +1,39 @@
+"use strict";
+// Part of this code was taken from the example vscode debugger found here,
+// https://github.com/microsoft/vscode-mock-debug/blob/5524e7a3dd9ec176a987ffe3aebd489f1f543799/src/debugAdapter.ts`
+Object.defineProperty(exports, "__esModule", { value: true });
+const ovmDebug_1 = require("./ovmDebug");
+const Net = require("node:net");
+/*
+ * When the debug adapter is run as an external process,
+ * normally the helper function DebugSession.run(...) takes care of everything:
+ *
+ *     MockDebugSession.run(MockDebugSession);
+ *
+ * but here the helper is not flexible enough to deal with a debug session constructors with a parameter.
+ * So for now we copied and modified the helper:
+ */
+// first parse command line arguments to see whether the debug adapter should run as a server
+let port = 0;
+const args = process.argv.slice(2);
+args.forEach(function (val, index, array) {
+    const portMatch = /^--server=(\d{4,5})$/.exec(val);
+    if (portMatch) {
+        port = parseInt(portMatch[1], 10);
+    }
+});
+if (port > 0) {
+    Net.createServer((socket) => {
+        const session = new ovmDebug_1.OVMDebugSession();
+        session.setRunAsServer(true);
+        session.start(socket, socket);
+    }).listen(port);
+}
+else {
+    // start a single session that communicates via stdin/stdout
+    const session = new ovmDebug_1.OVMDebugSession();
+    process.on('SIGTERM', () => {
+        session.shutdown();
+    });
+    session.start(process.stdin, process.stdout);
+}
diff --git a/misc/vscode/out/ovmDebug.js b/misc/vscode/out/ovmDebug.js
new file mode 100644 (file)
index 0000000..22b30d4
--- /dev/null
@@ -0,0 +1,390 @@
+"use strict";
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.OVMDebugSession = void 0;
+const debugadapter_1 = require("@vscode/debugadapter");
+const EventEmitter = require("node:events");
+const await_notify_1 = require("await-notify");
+const net = require("node:net");
+class OVMDebugSession extends debugadapter_1.LoggingDebugSession {
+    constructor() {
+        super("ovm-debug-log.txt");
+        this._configurationDone = new await_notify_1.Subject();
+        this._clientConnected = new await_notify_1.Subject();
+        this.setDebuggerLinesStartAt1(true);
+        this.setDebuggerColumnsStartAt1(true);
+        this._loadedSources = new Map();
+        this.debugger = new OVMDebugger();
+        this.debugger.on("breakpointHit", (ev) => {
+            console.log("BREAKPOINT HIT");
+            this.sendEvent(new debugadapter_1.StoppedEvent("breakpoint", ev.threadId));
+        });
+        this.debugger.on("paused", (ev) => {
+            console.log("PAUSED");
+            this.sendEvent(new debugadapter_1.StoppedEvent("pause", ev.threadId));
+        });
+        this.debugger.on("terminated", () => {
+            this.sendEvent(new debugadapter_1.TerminatedEvent());
+        });
+    }
+    initializeRequest(response, args) {
+        console.log("INITIALIZE");
+        response.body = response.body || {};
+        // the adapter implements the configurationDone request.
+        response.body.supportsConfigurationDoneRequest = true;
+        // make VS Code use 'evaluate' when hovering over source
+        response.body.supportsEvaluateForHovers = false;
+        // make VS Code show a 'step back' button
+        response.body.supportsStepBack = false;
+        // make VS Code support data breakpoints
+        response.body.supportsDataBreakpoints = false;
+        // make VS Code support completion in REPL
+        response.body.supportsCompletionsRequest = false;
+        // response.body.completionTriggerCharacters = [ ".", "[" ];
+        // make VS Code send cancel request
+        response.body.supportsCancelRequest = true;
+        // make VS Code send the breakpointLocations request
+        response.body.supportsBreakpointLocationsRequest = false;
+        // make VS Code provide "Step in Target" functionality
+        response.body.supportsStepInTargetsRequest = false;
+        // the adapter defines two exceptions filters, one with support for conditions.
+        response.body.supportsExceptionFilterOptions = false;
+        /*response.body.exceptionBreakpointFilters = [
+            {
+                filter: 'namedException',
+                label: "Named Exception",
+                description: `Break on named exceptions. Enter the exception's name as the Condition.`,
+                default: false,
+                supportsCondition: true,
+                conditionDescription: `Enter the exception's name`
+            },
+            {
+                filter: 'otherExceptions',
+                label: "Other Exceptions",
+                description: 'This is a other exception',
+                default: true,
+                supportsCondition: false
+            }
+        ];*/
+        // make VS Code send exceptionInfo request
+        response.body.supportsExceptionInfoRequest = false;
+        // make VS Code send setVariable request
+        response.body.supportsSetVariable = false;
+        // make VS Code send setExpression request
+        response.body.supportsSetExpression = false;
+        // make VS Code send disassemble request
+        response.body.supportsDisassembleRequest = false;
+        response.body.supportsSteppingGranularity = false;
+        response.body.supportsInstructionBreakpoints = false;
+        // make VS Code able to read and write variable memory
+        response.body.supportsReadMemoryRequest = false;
+        response.body.supportsWriteMemoryRequest = false;
+        response.body.supportSuspendDebuggee = false;
+        response.body.supportTerminateDebuggee = true;
+        response.body.supportsFunctionBreakpoints = true;
+        this.sendResponse(response);
+        // since this debug adapter can accept configuration requests like 'setBreakpoint' at any time,
+        // we request them early by sending an 'initializeRequest' to the frontend.
+        // The frontend will end the configuration sequence by calling 'configurationDone' request.
+        this.sendEvent(new debugadapter_1.InitializedEvent());
+    }
+    configurationDoneRequest(response, args, request) {
+        console.log("CONFIGURATION DONE");
+        super.configurationDoneRequest(response, args);
+        this._configurationDone.notify();
+    }
+    disconnectRequest(response, args, request) {
+        console.log(`disconnectRequest suspend: ${args.suspendDebuggee}, terminate: ${args.terminateDebuggee}`);
+    }
+    cancelRequest(response, args, request) {
+        this.sendResponse(response);
+    }
+    setBreakPointsRequest(response, args, request) {
+        return __awaiter(this, void 0, void 0, function* () {
+            console.log("BREAKPOINTS", args, response);
+            yield this._clientConnected.wait(1000);
+            const path = args.source.path;
+            const clientLines = args.lines || [];
+            // TODO: In theory, breakpoints should be cleared here
+            const actualBreakpointsPromise = clientLines.map((line) => __awaiter(this, void 0, void 0, function* () {
+                const res = yield this.debugger.set_breakpoint(path, line);
+                const bp = new debugadapter_1.Breakpoint(res.verified, this.convertDebuggerLineToClient(res.line));
+                bp.id = res.id;
+                return bp;
+            }));
+            const actualBreakpoints = yield Promise.all(actualBreakpointsPromise);
+            response.body = {
+                breakpoints: actualBreakpoints
+            };
+            this.sendResponse(response);
+        });
+    }
+    stackTraceRequest(response, args, request) {
+        return __awaiter(this, void 0, void 0, function* () {
+            console.log("LOCATION");
+            let location = yield this.debugger.request_location(1);
+            let source = new debugadapter_1.Source(this.fileNameToShortName(location.filename), this.convertDebuggerPathToClient(location.filename), undefined, undefined, "ovm-debug-src");
+            console.log(source);
+            if (!this._loadedSources.has(source.name)) {
+                this._loadedSources.set(source.name, source);
+                this.sendEvent(new debugadapter_1.LoadedSourceEvent("new", source));
+            }
+            response.body = {
+                stackFrames: [
+                    new debugadapter_1.StackFrame(1, "test frame", source, location.line)
+                ]
+            };
+            this.sendResponse(response);
+        });
+    }
+    threadsRequest(response, request) {
+        console.log("THREADS");
+        response.body = {
+            threads: [
+                new debugadapter_1.Thread(1, "main thread"),
+            ]
+        };
+        this.sendResponse(response);
+    }
+    scopesRequest(response, args, request) {
+        console.log("SCOPES");
+        response.body = {
+            scopes: [
+                new debugadapter_1.Scope("Locals", 1, false),
+                new debugadapter_1.Scope("Globals", 2, true)
+            ]
+        };
+        this.sendResponse(response);
+    }
+    launchRequest(response, args, request) {
+        console.log("LAUNCH");
+        // console.error(`Unable to launch a new Onyx debugging session. Please use { "request": "attach" } instead.`);
+        this.attachRequest(response, { "socketPath": "/tmp/ovm-debug.0000", "stopOnEntry": true });
+    }
+    attachRequest(response, args, request) {
+        return __awaiter(this, void 0, void 0, function* () {
+            console.log("ATTACH");
+            yield this._configurationDone.wait(1000);
+            yield this.debugger.connect(args.socketPath);
+            this._clientConnected.notify();
+            this.sendResponse(response);
+            this.sendEvent(new debugadapter_1.ThreadEvent("started", 1));
+            if (!args.stopOnEntry) {
+                this.debugger.resume();
+            }
+        });
+    }
+    continueRequest(response, args, request) {
+        let thread_id = args.threadId;
+        if (!args.singleThread) {
+            thread_id = 0xffffffff;
+        }
+        response.body = {
+            allThreadsContinued: !!args.singleThread
+        };
+        this.debugger.resume(thread_id);
+        this.sendResponse(response);
+    }
+    fileNameToShortName(filename) {
+        return filename.substring(filename.lastIndexOf("/"));
+    }
+}
+exports.OVMDebugSession = OVMDebugSession;
+var OVMCommand;
+(function (OVMCommand) {
+    OVMCommand[OVMCommand["NOP"] = 0] = "NOP";
+    OVMCommand[OVMCommand["RES"] = 1] = "RES";
+    OVMCommand[OVMCommand["BRK"] = 2] = "BRK";
+    OVMCommand[OVMCommand["LOC"] = 3] = "LOC";
+})(OVMCommand || (OVMCommand = {}));
+var OVMEvent;
+(function (OVMEvent) {
+    OVMEvent[OVMEvent["NOP"] = 0] = "NOP";
+    OVMEvent[OVMEvent["BREAKPOINT_HIT"] = 1] = "BREAKPOINT_HIT";
+    OVMEvent[OVMEvent["PAUSED"] = 2] = "PAUSED";
+    OVMEvent[OVMEvent["RESPONSE"] = 4294967295] = "RESPONSE";
+})(OVMEvent || (OVMEvent = {}));
+class OVMDebugger extends EventEmitter {
+    constructor() {
+        super();
+        this._promiseResolution = new Map();
+    }
+    connect(path) {
+        this._next_cmd_id = 1;
+        this.pending_responses = {};
+        this.client = net.connect(path);
+        this.client.on("data", this.parseIncoming.bind(this));
+        this.client.on("end", () => {
+            console.log("terminated connection.");
+            this.sendEvent("terminated");
+        });
+        return new Promise((res, rej) => {
+            this.client.on("connect", res);
+        });
+    }
+    resume(thread_id = 0xffffffff) {
+        let data = new ArrayBuffer(12);
+        let view = new DataView(data);
+        let cmd_id = this.next_command_id;
+        view.setUint32(0, cmd_id, true);
+        view.setUint32(4, OVMCommand.RES, true);
+        view.setUint32(8, thread_id, true);
+        this.client.write(new Uint8Array(data));
+        this.pending_responses[cmd_id] = OVMCommand.RES;
+    }
+    set_breakpoint(filename, line) {
+        return __awaiter(this, void 0, void 0, function* () {
+            let data = new ArrayBuffer(16 + filename.length);
+            let view = new DataView(data);
+            let cmd_id = this.next_command_id;
+            view.setUint32(0, cmd_id, true);
+            view.setUint32(4, OVMCommand.BRK, true);
+            view.setUint32(8, filename.length, true);
+            for (let i = 0; i < filename.length; i++) {
+                view.setUint8(i + 12, filename.charCodeAt(i));
+            }
+            view.setUint32(12 + filename.length, line, true);
+            this.client.write(new Uint8Array(data));
+            this.pending_responses[cmd_id] = OVMCommand.BRK;
+            return this.preparePromise(cmd_id);
+        });
+    }
+    request_location(thread_id) {
+        let data = new ArrayBuffer(12);
+        let view = new DataView(data);
+        let cmd_id = this.next_command_id;
+        view.setUint32(0, cmd_id, true);
+        view.setUint32(4, OVMCommand.LOC, true);
+        view.setUint32(8, thread_id, true);
+        this.client.write(new Uint8Array(data));
+        this.pending_responses[cmd_id] = OVMCommand.LOC;
+        return this.preparePromise(cmd_id);
+    }
+    parseIncoming(data) {
+        let parser = new DataParser(data);
+        while (parser.offset != data.length) {
+            let event_id = parser.parseUint32();
+            switch (event_id) {
+                case OVMEvent.NOP: break;
+                case OVMEvent.BREAKPOINT_HIT: {
+                    let bp_id = parser.parseUint32();
+                    let thread_id = parser.parseUint32();
+                    this.sendEvent("breakpointHit", {
+                        breakpointId: bp_id,
+                        threadId: thread_id,
+                    });
+                    break;
+                }
+                case OVMEvent.PAUSED: {
+                    let thread_id = parser.parseUint32();
+                    this.sendEvent("paused", { threadId: thread_id });
+                    break;
+                }
+                case OVMEvent.RESPONSE: {
+                    this.handleResponse(parser);
+                    break;
+                }
+                default:
+                    console.log("Unknown event: ", event_id, data);
+            }
+        }
+    }
+    handleResponse(parser) {
+        let msg_id = parser.parseUint32();
+        let cmd_id = this.pending_responses[msg_id] || OVMCommand.NOP;
+        switch (cmd_id) {
+            case OVMCommand.NOP: break;
+            case OVMCommand.RES: {
+                let success = parser.parseBool();
+                break;
+            }
+            case OVMCommand.BRK: {
+                let success = parser.parseBool();
+                let bp_id = parser.parseInt32();
+                let line = parser.parseInt32();
+                this.resolvePromise(msg_id, {
+                    verified: success,
+                    id: bp_id,
+                    line: line
+                });
+                break;
+            }
+            case OVMCommand.LOC: {
+                console.log("Recv loc");
+                let success = parser.parseBool();
+                let filename = parser.parseString();
+                let line = parser.parseInt32();
+                if (!success)
+                    break;
+                this.resolvePromise(msg_id, { filename, line });
+                break;
+            }
+            default:
+                console.log("Unrecognized command. ", cmd_id, msg_id);
+        }
+    }
+    preparePromise(msg_id) {
+        return new Promise((resolve, reject) => {
+            this._promiseResolution.set(msg_id, resolve);
+        });
+    }
+    resolvePromise(msg_id, data) {
+        if (this._promiseResolution.has(msg_id)) {
+            let func = this._promiseResolution.get(msg_id);
+            this._promiseResolution.delete(msg_id);
+            func(data);
+        }
+    }
+    sendEvent(event, ...args) {
+        setTimeout(() => {
+            this.emit(event, ...args);
+        }, 0);
+    }
+    get next_command_id() {
+        let val = this._next_cmd_id;
+        this._next_cmd_id += 1;
+        return val;
+    }
+}
+//
+// Utility class for parsing system data types out of
+// a buffer. Currently, it assumes a little endian byte
+// order in the buffer, and that should probably be changed?
+//
+class DataParser {
+    constructor(data) {
+        this.data = data;
+        this.view = new DataView(data.buffer);
+        console.log("PARSING", this.data);
+        this.offset = 0;
+    }
+    parseInt32() {
+        this.offset += 4;
+        return this.view.getInt32(this.offset - 4, true);
+    }
+    parseUint32() {
+        this.offset += 4;
+        return this.view.getUint32(this.offset - 4, true);
+    }
+    parseString() {
+        let len = this.parseUint32();
+        let str = "";
+        for (let i = 0; i < len; i++) {
+            str += String.fromCharCode(this.view.getUint8(this.offset + i));
+        }
+        this.offset += len;
+        return str;
+    }
+    parseBool() {
+        this.offset += 1;
+        return this.view.getUint8(this.offset - 1) != 0;
+    }
+}
diff --git a/misc/vscode/ovmDebug.ts b/misc/vscode/ovmDebug.ts
new file mode 100644 (file)
index 0000000..d6519e4
--- /dev/null
@@ -0,0 +1,528 @@
+import {
+       Logger, logger,
+       LoggingDebugSession,
+       InitializedEvent, TerminatedEvent, StoppedEvent, BreakpointEvent, OutputEvent,
+       ProgressStartEvent, ProgressUpdateEvent, ProgressEndEvent, InvalidatedEvent,
+       Thread, StackFrame, Scope, Source, Handles, Breakpoint, MemoryEvent, ThreadEvent, LoadedSourceEvent
+} from '@vscode/debugadapter';
+import { DebugProtocol } from '@vscode/debugprotocol';
+import EventEmitter = require('node:events');
+
+import { Subject } from "await-notify";
+import * as net from "node:net";
+
+
+interface IOVMAttachRequestArguments extends DebugProtocol.AttachRequestArguments {
+    socketPath?: string;
+    stopOnEntry?: boolean;
+}
+
+export class OVMDebugSession extends LoggingDebugSession {
+
+       private debugger: OVMDebugger;
+
+       private _configurationDone = new Subject();
+       private _clientConnected = new Subject();
+
+       private _pending_breakpoints: DebugProtocol.SetBreakpointsArguments | null;
+       private _pending_breakpoint_response: DebugProtocol.SetBreakpointsResponse | null;
+
+       private _loadedSources: Map<string, Source>;
+
+    public constructor() {
+        super("ovm-debug-log.txt");
+
+        this.setDebuggerLinesStartAt1(true);
+        this.setDebuggerColumnsStartAt1(true);
+
+               this._loadedSources = new Map();
+                               
+        this.debugger = new OVMDebugger();
+
+               this.debugger.on("breakpointHit", (ev) => {
+                       console.log("BREAKPOINT HIT");
+                       this.sendEvent(new StoppedEvent("breakpoint", ev.threadId));
+               });
+
+               this.debugger.on("paused", (ev) => {
+                       console.log("PAUSED");
+                       this.sendEvent(new StoppedEvent("pause", ev.threadId));
+               });
+
+               this.debugger.on("terminated", () => {
+                       this.sendEvent(new TerminatedEvent());
+               });
+    }
+
+    protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void {
+               console.log("INITIALIZE");
+
+        response.body = response.body || {};
+
+        // the adapter implements the configurationDone request.
+               response.body.supportsConfigurationDoneRequest = true;
+
+               // make VS Code use 'evaluate' when hovering over source
+               response.body.supportsEvaluateForHovers = false;
+
+               // make VS Code show a 'step back' button
+               response.body.supportsStepBack = false;
+
+               // make VS Code support data breakpoints
+               response.body.supportsDataBreakpoints = false;
+
+               // make VS Code support completion in REPL
+               response.body.supportsCompletionsRequest = false;
+               // response.body.completionTriggerCharacters = [ ".", "[" ];
+
+               // make VS Code send cancel request
+               response.body.supportsCancelRequest = true;
+
+               // make VS Code send the breakpointLocations request
+               response.body.supportsBreakpointLocationsRequest = false;
+
+               // make VS Code provide "Step in Target" functionality
+               response.body.supportsStepInTargetsRequest = false;
+
+               // the adapter defines two exceptions filters, one with support for conditions.
+               response.body.supportsExceptionFilterOptions = false;
+               /*response.body.exceptionBreakpointFilters = [
+                       {
+                               filter: 'namedException',
+                               label: "Named Exception",
+                               description: `Break on named exceptions. Enter the exception's name as the Condition.`,
+                               default: false,
+                               supportsCondition: true,
+                               conditionDescription: `Enter the exception's name`
+                       },
+                       {
+                               filter: 'otherExceptions',
+                               label: "Other Exceptions",
+                               description: 'This is a other exception',
+                               default: true,
+                               supportsCondition: false
+                       }
+               ];*/
+
+               // make VS Code send exceptionInfo request
+               response.body.supportsExceptionInfoRequest = false;
+
+               // make VS Code send setVariable request
+               response.body.supportsSetVariable = false;
+
+               // make VS Code send setExpression request
+               response.body.supportsSetExpression = false;
+
+               // make VS Code send disassemble request
+               response.body.supportsDisassembleRequest = false;
+               response.body.supportsSteppingGranularity = false;
+               response.body.supportsInstructionBreakpoints = false;
+
+               // make VS Code able to read and write variable memory
+               response.body.supportsReadMemoryRequest = false;
+               response.body.supportsWriteMemoryRequest = false;
+
+               response.body.supportSuspendDebuggee = false;
+               response.body.supportTerminateDebuggee = true;
+               response.body.supportsFunctionBreakpoints = true;
+
+               this.sendResponse(response);
+
+               // since this debug adapter can accept configuration requests like 'setBreakpoint' at any time,
+               // we request them early by sending an 'initializeRequest' to the frontend.
+               // The frontend will end the configuration sequence by calling 'configurationDone' request.
+               this.sendEvent(new InitializedEvent());
+    }
+
+    protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments, request?: DebugProtocol.Request): void {
+               console.log("CONFIGURATION DONE");
+        super.configurationDoneRequest(response, args);
+
+               this._configurationDone.notify();
+    }
+
+       protected disconnectRequest(response: DebugProtocol.DisconnectResponse, args: DebugProtocol.DisconnectArguments, request?: DebugProtocol.Request): void {
+               console.log(`disconnectRequest suspend: ${args.suspendDebuggee}, terminate: ${args.terminateDebuggee}`);
+       }
+
+    protected cancelRequest(response: DebugProtocol.CancelResponse, args: DebugProtocol.CancelArguments, request?: DebugProtocol.Request): void {
+               this.sendResponse(response);
+    }
+
+       protected async setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments, request?: DebugProtocol.Request): Promise<void> {
+               console.log("BREAKPOINTS", args, response);
+               await this._clientConnected.wait(1000);
+
+               const path = args.source.path;
+               const clientLines = args.lines || [];
+
+               // TODO: In theory, breakpoints should be cleared here
+
+               const actualBreakpointsPromise = clientLines.map(async line => {
+                       const res = await this.debugger.set_breakpoint(path, line);
+                       const bp = new Breakpoint(res.verified, this.convertDebuggerLineToClient(res.line)) as DebugProtocol.Breakpoint;
+                       bp.id = res.id;
+                       return bp;
+               });
+
+               const actualBreakpoints = await Promise.all<DebugProtocol.Breakpoint>(actualBreakpointsPromise);
+
+               response.body = {
+                       breakpoints: actualBreakpoints
+               };
+               this.sendResponse(response);
+       }
+
+       protected async stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments, request?: DebugProtocol.Request): Promise<void> {
+               console.log("LOCATION");
+
+               let location = await this.debugger.request_location(1);
+               let source = new Source(
+                       this.fileNameToShortName(location.filename),
+                       this.convertDebuggerPathToClient(location.filename),
+                       undefined, undefined, "ovm-debug-src");
+
+               console.log(source);
+
+               if (!this._loadedSources.has(source.name)) {
+                       this._loadedSources.set(source.name, source);
+
+                       this.sendEvent(new LoadedSourceEvent("new", source));
+               }
+
+               response.body = {
+                       stackFrames: [
+                               new StackFrame(1, "test frame", source, location.line)
+                       ]       
+               };
+
+               this.sendResponse(response);
+       }
+
+       protected threadsRequest(response: DebugProtocol.ThreadsResponse, request?: DebugProtocol.Request): void {
+               console.log("THREADS");
+
+               response.body = {
+                       threads: [
+                               new Thread(1, "main thread"),
+                       ]
+               };
+
+               this.sendResponse(response);
+       }
+
+       protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments, request?: DebugProtocol.Request): void {
+               console.log("SCOPES");
+
+               response.body = {
+                       scopes: [
+                               new Scope("Locals", 1, false),
+                               new Scope("Globals", 2, true)
+                       ]
+               };
+               this.sendResponse(response);    
+       }
+
+    protected launchRequest(response: DebugProtocol.LaunchResponse, args: DebugProtocol.LaunchRequestArguments, request?: DebugProtocol.Request): void {
+               console.log("LAUNCH");
+        // console.error(`Unable to launch a new Onyx debugging session. Please use { "request": "attach" } instead.`);
+               this.attachRequest(response, {"socketPath": "/tmp/ovm-debug.0000", "stopOnEntry": true});
+    }
+
+    protected async attachRequest(response: DebugProtocol.AttachResponse, args: IOVMAttachRequestArguments, request?: DebugProtocol.Request): Promise<void> {
+               console.log("ATTACH");
+               await this._configurationDone.wait(1000);
+
+               await this.debugger.connect(args.socketPath);
+               this._clientConnected.notify();
+
+               this.sendResponse(response);
+               this.sendEvent(new ThreadEvent("started", 1));
+
+               if (!args.stopOnEntry) {
+                       this.debugger.resume();
+               }
+    }
+
+       protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments, request?: DebugProtocol.Request): void {
+               let thread_id = args.threadId;
+               if (!args.singleThread) {
+                       thread_id = 0xffffffff;
+               }
+
+               response.body = {
+                       allThreadsContinued: !!args.singleThread
+               };
+
+               this.debugger.resume(thread_id);
+               this.sendResponse(response);
+       }
+
+
+       private fileNameToShortName(filename: string): string {
+               return filename.substring(filename.lastIndexOf("/"));
+       }
+}
+
+interface IFileLocation {
+       filename: string;
+       line: number;
+}
+
+interface IBreakpointValidation {
+       verified: boolean;
+       id: number;
+       line: number;
+}
+
+enum OVMCommand {
+       NOP = 0,
+       RES = 1,
+       BRK = 2,
+       LOC = 3,
+}
+
+enum OVMEvent {
+       NOP = 0,
+       BREAKPOINT_HIT = 1,
+       PAUSED = 2,
+       RESPONSE = 0xffffffff
+}
+
+class OVMDebugger extends EventEmitter {
+
+       private client: net.Socket;
+       private pending_responses: any;
+
+       private _next_cmd_id: number;
+       private _promiseResolution: Map<number, (arg0: any) => void>;
+
+       constructor() {
+               super();
+               this._promiseResolution = new Map();
+       }
+
+       connect(path: string): Promise<void> {
+        this._next_cmd_id = 1;
+               this.pending_responses = {};
+
+               this.client = net.connect(path);
+
+               this.client.on("data", this.parseIncoming.bind(this));
+
+               this.client.on("end", () => {
+                       console.log("terminated connection.");
+                       this.sendEvent("terminated");
+               });
+
+               return new Promise((res, rej) => {
+                       this.client.on("connect", res);
+               });
+       }
+
+    resume(thread_id: number = 0xffffffff): void {
+        let data = new ArrayBuffer(12);
+        let view = new DataView(data);
+
+        let cmd_id = this.next_command_id;
+
+        view.setUint32(0, cmd_id, true);
+        view.setUint32(4, OVMCommand.RES, true);
+        view.setUint32(8, thread_id, true);
+
+        this.client.write(new Uint8Array(data));
+
+        this.pending_responses[cmd_id] = OVMCommand.RES;
+    }
+
+    async set_breakpoint(filename: string, line: number): Promise<IBreakpointValidation> {
+        let data = new ArrayBuffer(16+filename.length);
+        let view = new DataView(data);
+
+               let cmd_id = this.next_command_id;
+
+        view.setUint32(0, cmd_id, true);
+        view.setUint32(4, OVMCommand.BRK, true);
+
+        view.setUint32(8, filename.length, true);
+        for (let i=0; i<filename.length; i++) {
+            view.setUint8(i+12, filename.charCodeAt(i));
+        }
+        view.setUint32(12+filename.length, line, true);
+
+        this.client.write(new Uint8Array(data));
+
+        this.pending_responses[cmd_id] = OVMCommand.BRK;
+
+               return this.preparePromise(cmd_id);
+    }
+
+       request_location(thread_id: number): Promise<IFileLocation> {
+        let data = new ArrayBuffer(12);
+        let view = new DataView(data);
+
+        let cmd_id = this.next_command_id;
+
+        view.setUint32(0, cmd_id, true);
+        view.setUint32(4, OVMCommand.LOC, true);
+        view.setUint32(8, thread_id, true);
+
+        this.client.write(new Uint8Array(data));
+
+        this.pending_responses[cmd_id] = OVMCommand.LOC;
+
+               return this.preparePromise(cmd_id);
+       }
+
+       private parseIncoming(data: Buffer): void {
+               let parser = new DataParser(data);
+
+               while (parser.offset != data.length) {
+                       let event_id = parser.parseUint32();
+
+                       switch (event_id) {
+                               case OVMEvent.NOP: break;
+
+                               case OVMEvent.BREAKPOINT_HIT: {
+                                       let bp_id = parser.parseUint32();
+                                       let thread_id = parser.parseUint32();
+
+                                       this.sendEvent("breakpointHit", {
+                                               breakpointId: bp_id,
+                                               threadId: thread_id,
+                                       });
+                                       break;
+                               }
+
+                               case OVMEvent.PAUSED: {
+                                       let thread_id = parser.parseUint32();
+
+                                       this.sendEvent("paused", { threadId: thread_id });
+                                       break;
+                               }
+
+                               case OVMEvent.RESPONSE: {
+                                       this.handleResponse(parser);
+                                       break;
+                               }
+
+                               default:
+                                       console.log("Unknown event: ", event_id, data);
+                       }
+               }
+       }
+
+       private handleResponse(parser: DataParser) {
+               let msg_id = parser.parseUint32();
+               let cmd_id = this.pending_responses[msg_id] || OVMCommand.NOP;
+
+               switch (cmd_id) {
+                       case OVMCommand.NOP: break;
+                       case OVMCommand.RES: {
+                               let success = parser.parseBool();
+                               break;
+                       }
+
+                       case OVMCommand.BRK: {
+                               let success = parser.parseBool();
+                               let bp_id   = parser.parseInt32();
+                               let line    = parser.parseInt32();
+
+                               this.resolvePromise(msg_id, {
+                                       verified: success,
+                                       id: bp_id,
+                                       line: line
+                               });
+                               break;
+                       }
+
+                       case OVMCommand.LOC: {
+                               console.log("Recv loc");
+                               let success  = parser.parseBool();
+                               let filename = parser.parseString();
+                               let line     = parser.parseInt32();
+
+                               if (!success) break;
+
+                               this.resolvePromise(msg_id, {filename, line});
+                               break;
+                       }
+
+                       default:
+                               console.log("Unrecognized command. ", cmd_id, msg_id);
+               }
+       }
+
+       private preparePromise<T>(msg_id: number): Promise<T> {
+               return new Promise((resolve, reject) => {
+                       this._promiseResolution.set(msg_id, resolve);
+               });
+       }
+
+       private resolvePromise(msg_id: number, data: any): void {
+               if (this._promiseResolution.has(msg_id)) {
+                       let func = this._promiseResolution.get(msg_id);
+                       this._promiseResolution.delete(msg_id);
+                       func(data);
+               }
+       }
+
+       private sendEvent(event: string, ... args: any[]): void {
+               setTimeout(() => {
+                       this.emit(event, ...args);
+               }, 0);
+       }
+
+    private get next_command_id(): number {
+        let val = this._next_cmd_id;
+        this._next_cmd_id += 1;
+        return val;
+    }
+
+}
+
+
+//
+// Utility class for parsing system data types out of
+// a buffer. Currently, it assumes a little endian byte
+// order in the buffer, and that should probably be changed?
+//
+class DataParser {
+       private data: Buffer;
+       private view: DataView;
+       public offset: number;
+
+    constructor(data: Buffer) {
+        this.data = data;
+        this.view = new DataView(data.buffer);
+               console.log("PARSING", this.data);
+        this.offset = 0;
+    }
+
+    parseInt32() {
+        this.offset += 4;
+        return this.view.getInt32(this.offset - 4, true);
+    }
+
+    parseUint32() {
+        this.offset += 4;
+        return this.view.getUint32(this.offset - 4, true);
+    }
+
+    parseString() { 
+        let len = this.parseUint32();
+        let str = "";
+        for (let i=0; i<len; i++) {
+            str += String.fromCharCode(this.view.getUint8(this.offset + i));
+        }
+        this.offset += len;
+
+        return str;
+    }
+
+    parseBool() {
+        this.offset += 1;
+        return this.view.getUint8(this.offset - 1) != 0;
+    }
+}
+
index a5f3016c95cfcd0d4a40ce729e9741c315fe6961..a8740ae71392d57138f6dafb93dc9bc31f68fbfa 100644 (file)
@@ -11,7 +11,8 @@
                 "vscode-textmate-languageservice": "0.2.1"
             },
             "devDependencies": {
-                "vscode": "^1.1.37"
+                "vscode": "^1.1.37",
+                "vscode-debugadapter": "^1.51.0"
             },
             "engines": {
                 "vscode": "^1.51.0"
                 "node": ">=8.9.3"
             }
         },
+        "node_modules/vscode-debugadapter": {
+            "version": "1.51.0",
+            "resolved": "https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.51.0.tgz",
+            "integrity": "sha512-mObaXD5/FH/z6aL2GDuyCLbnwLsYRCAJWgFid01vKW9Y5Si8OvINK+Tn+Yl/lRUbetjNuZW3j1euMEz6z8Yzqg==",
+            "deprecated": "This package has been renamed to @vscode/debugadapter, please update to the new name",
+            "dev": true,
+            "dependencies": {
+                "mkdirp": "^1.0.4",
+                "vscode-debugprotocol": "1.51.0"
+            }
+        },
+        "node_modules/vscode-debugadapter/node_modules/mkdirp": {
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+            "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+            "dev": true,
+            "bin": {
+                "mkdirp": "bin/cmd.js"
+            },
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/vscode-debugprotocol": {
+            "version": "1.51.0",
+            "resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.51.0.tgz",
+            "integrity": "sha512-dzKWTMMyebIMPF1VYMuuQj7gGFq7guR8AFya0mKacu+ayptJfaRuM0mdHCqiOth4FnRP8mPhEroFPx6Ift8wHA==",
+            "deprecated": "This package has been renamed to @vscode/debugprotocol, please update to the new name",
+            "dev": true
+        },
         "node_modules/vscode-test": {
             "version": "0.4.3",
             "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-0.4.3.tgz",
                 "vscode-test": "^0.4.1"
             }
         },
+        "vscode-debugadapter": {
+            "version": "1.51.0",
+            "resolved": "https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.51.0.tgz",
+            "integrity": "sha512-mObaXD5/FH/z6aL2GDuyCLbnwLsYRCAJWgFid01vKW9Y5Si8OvINK+Tn+Yl/lRUbetjNuZW3j1euMEz6z8Yzqg==",
+            "dev": true,
+            "requires": {
+                "mkdirp": "^1.0.4",
+                "vscode-debugprotocol": "1.51.0"
+            },
+            "dependencies": {
+                "mkdirp": {
+                    "version": "1.0.4",
+                    "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+                    "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+                    "dev": true
+                }
+            }
+        },
+        "vscode-debugprotocol": {
+            "version": "1.51.0",
+            "resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.51.0.tgz",
+            "integrity": "sha512-dzKWTMMyebIMPF1VYMuuQj7gGFq7guR8AFya0mKacu+ayptJfaRuM0mdHCqiOth4FnRP8mPhEroFPx6Ift8wHA==",
+            "dev": true
+        },
         "vscode-test": {
             "version": "0.4.3",
             "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-0.4.3.tgz",
index 9cc6d87892d2bc7f7efc6295a5c501936d3fa736..c841d322d2b11446cc205a44e410a67afa986ad8 100644 (file)
@@ -4,6 +4,7 @@
     "description": "Onyx syntax highlighting.",
     "version": "0.0.3",
     "publisher": "brendanfh",
+    "license": "BSD-2-Clause",
     "engines": {
         "vscode": "^1.51.0"
     },
                     "loop": true
                 }
             }
+        ],
+        "breakpoints": [
+            {
+                "language": "onyx"
+            }
+        ],
+        "debuggers": [
+            {
+                "type": "onyx",
+                "languages": [
+                    "onyx"
+                ],
+                "label": "Onyx Debug",
+                "program": "./out/debugAdapter.js",
+                "runtime": "node",
+                "configurationAttributes": {
+                    "attach": {
+                        "properties": {
+                            "socketPath": {
+                                "type": "string",
+                                "description": "Path to UNIX socket for attaching to debugger",
+                                "default": "/tmp/ovm-debug.0000"
+                            },
+                            "stopOnEntry": {
+                                "type": "boolean",
+                                "description": "Automatically stop after launch.",
+                                "default": true
+                            }
+                        }
+                    }
+                },
+                "initialConfigurations": [
+                    {
+                        "type": "onyx",
+                        "request": "attach",
+                        "stopOnEntry": true
+                    }
+                ]
+            }
         ]
     },
     "files": [
         "./textmate-configuration.json"
     ],
     "dependencies": {
+        "@types/node": "^18.6.4",
+        "@vscode/debugadapter": "^1.57.0",
+        "await-notify": "^1.0.1",
         "vscode-textmate-languageservice": "0.2.1"
     },
     "devDependencies": {
-        "vscode": "^1.1.37"
+        "vscode": "^1.1.37",
+        "vscode-debugadapter": "^1.51.0"
     }
 }
index 57a115e0d484be1abe85633c0347b5b3e0fa703f..01ce745f5f5bfedf0064252aba58485ae8461d6c 100644 (file)
@@ -6,7 +6,10 @@
                "moduleResolution": "node",
                "lib": ["es6", "es2016"],
                "sourceMap": false,
-               "rootDir": "."
+               "rootDir": ".",
+               "typeRoots": [
+                       "node_modules/@types"
+               ]
        },
        "exclude": ["node_modules", ".vscode-test"]
 }
\ No newline at end of file