From: Brendan Hansen Date: Wed, 10 Aug 2022 18:01:48 +0000 (-0500) Subject: breakpoints working in debug adapter X-Git-Url: https://git.brendanfh.com/?a=commitdiff_plain;h=4d8f59d0478b4b9af370eeb26351c24511ac649b;p=onyx.git breakpoints working in debug adapter --- diff --git a/lib/linux_x86_64/lib/libovmwasm.so b/lib/linux_x86_64/lib/libovmwasm.so index 45af884f..d11cf846 100755 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 index 00000000..f8a2c80a --- /dev/null +++ b/misc/vscode/debugAdapter.ts @@ -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 diff --git a/misc/vscode/onyx-0.0.3.vsix b/misc/vscode/onyx-0.0.3.vsix index 6768cd37..8c1803e3 100644 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 index 00000000..2b08cad8 --- /dev/null +++ b/misc/vscode/out/debugAdapter.js @@ -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 index 00000000..22b30d46 --- /dev/null +++ b/misc/vscode/out/ovmDebug.js @@ -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 index 00000000..d6519e4e --- /dev/null +++ b/misc/vscode/ovmDebug.ts @@ -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; + + 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 { + 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(actualBreakpointsPromise); + + response.body = { + breakpoints: actualBreakpoints + }; + this.sendResponse(response); + } + + protected async stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments, request?: DebugProtocol.Request): Promise { + 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 { + 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 void>; + + constructor() { + super(); + this._promiseResolution = new Map(); + } + + connect(path: string): Promise { + 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 { + 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 { + 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(msg_id: number): Promise { + 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=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", @@ -1210,6 +1241,30 @@ "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", diff --git a/misc/vscode/package.json b/misc/vscode/package.json index 9cc6d878..c841d322 100644 --- a/misc/vscode/package.json +++ b/misc/vscode/package.json @@ -4,6 +4,7 @@ "description": "Onyx syntax highlighting.", "version": "0.0.3", "publisher": "brendanfh", + "license": "BSD-2-Clause", "engines": { "vscode": "^1.51.0" }, @@ -50,15 +51,58 @@ "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" } } diff --git a/misc/vscode/tsconfig.json b/misc/vscode/tsconfig.json index 57a115e0..01ce745f 100644 --- a/misc/vscode/tsconfig.json +++ b/misc/vscode/tsconfig.json @@ -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