UNPKG

uae-dap

Version:

Debug Adapter Protocol for Amiga development with FS-UAE or WinUAE

738 lines 31 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UAEDebugSession = void 0; const debugadapter_1 = require("@vscode/debugadapter"); const logger_1 = require("@vscode/debugadapter/lib/logger"); const async_mutex_1 = require("async-mutex"); const path_1 = require("path"); const gdbClient_1 = require("./gdbClient"); const breakpointManager_1 = __importDefault(require("./breakpointManager")); const strings_1 = require("./utils/strings"); const emulator_1 = require("./emulator"); const variableManager_1 = __importStar(require("./variableManager")); const vasm_1 = require("./vasm"); const amigaHunkParser_1 = require("./amigaHunkParser"); const sourceMap_1 = __importDefault(require("./sourceMap")); const disassembly_1 = require("./disassembly"); const hardware_1 = require("./hardware"); const stackManager_1 = __importDefault(require("./stackManager")); const promise_retry_1 = __importDefault(require("promise-retry")); const help_1 = require("./help"); /** * Default values for custom launch/attach args */ const defaultArgs = { program: undefined, remoteProgram: undefined, stopOnEntry: false, trace: false, exceptionMask: 0b111100, serverName: "localhost", serverPort: 2345, emulatorBin: undefined, emulatorType: process.platform === "win32" ? "winuae" : "fs-uae", emulatorArgs: [], memoryFormats: { watch: { length: 104, wordLength: 2, }, hover: { length: 24, wordLength: 2, }, }, }; class UAEDebugSession extends debugadapter_1.LoggingDebugSession { constructor() { super(); this.trace = false; this.stopOnEntry = false; this.exceptionMask = defaultArgs.exceptionMask; // This can be replaced with a persistent implementation in VS Code this.dataBreakpointSizes = new Map(); // this debugger uses zero-based lines and columns this.setDebuggerLinesStartAt1(false); this.setDebuggerColumnsStartAt1(false); this.gdb = new gdbClient_1.GdbClient(); process.on("unhandledRejection", (reason, p) => { debugadapter_1.logger.error(reason + " Unhandled Rejection at Promise " + p); }); process.on("uncaughtException", (err) => { debugadapter_1.logger.error("Uncaught Exception thrown: " + this.errorString(err)); process.exit(1); }); const mutex = new async_mutex_1.Mutex(); this.gdb.on("stop", (haltStatus) => { mutex.runExclusive(async () => { return this.handleStop(haltStatus).catch((err) => { debugadapter_1.logger.error(this.errorString(err)); }); }); }); this.gdb.on("end", this.shutdown.bind(this)); this.gdb.on("output", (msg) => { this.sendEvent(new debugadapter_1.OutputEvent(msg + "\n", "console")); }); } initializeRequest(response) { // build and return the capabilities of this debug adapter: response.body = { ...response.body, supportsCompletionsRequest: true, supportsConditionalBreakpoints: true, supportsHitConditionalBreakpoints: true, supportsLogPoints: true, supportsConfigurationDoneRequest: true, supportsDataBreakpoints: true, supportsDisassembleRequest: true, supportsEvaluateForHovers: true, supportsExceptionInfoRequest: true, supportsExceptionOptions: true, supportsInstructionBreakpoints: true, supportsReadMemoryRequest: true, supportsRestartRequest: true, supportsRestartFrame: false, supportsSetVariable: true, supportsSingleThreadExecutionRequests: false, supportsStepBack: false, supportsSteppingGranularity: false, supportsValueFormattingOptions: true, supportsWriteMemoryRequest: true, exceptionBreakpointFilters: [ { filter: "all", label: "All Exceptions", default: true, }, ], }; this.sendResponse(response); } async launchRequest(response, customArgs) { await this.launchOrAttach(response, customArgs, true, !customArgs.noDebug); } async attachRequest(response, customArgs) { await this.launchOrAttach(response, customArgs, false); } async launchOrAttach(response, customArgs, startEmulator = true, debug = true) { // Merge in default args const args = { ...defaultArgs, ...customArgs, memoryFormats: { ...defaultArgs.memoryFormats, ...customArgs.memoryFormats, }, }; try { this.trace = args.trace; this.stopOnEntry = args.stopOnEntry; this.exceptionMask = args.exceptionMask; // Initialize logger: debugadapter_1.logger.init((e) => this.sendEvent(e)); debugadapter_1.logger.setup(args.trace ? logger_1.LogLevel.Verbose : logger_1.LogLevel.Warn); debugadapter_1.logger.log("[LAUNCH] " + JSON.stringify(args, null, 2)); // Start the emulator if (startEmulator) { this.emulator = emulator_1.Emulator.getInstance(args.emulatorType); // Program is required when debugging if (debug && !args.program) { throw new Error("Missing program argument in launch request"); } // Set default remoteProgram from program if (args.program && !args.remoteProgram) { args.remoteProgram = "SYS:" + (0, path_1.basename)(args.program); } // Determine HDD mount from program and remoteProgram let mountDir = undefined; if (args.program && args.remoteProgram) { // By default mount the dir containing the remote program as hard drive 0 (SYS:) mountDir = args.program .replace(/\\/, "/") .replace(args.remoteProgram.replace(/^.+:/, ""), ""); } const runOpts = { bin: args.emulatorBin, args: args.emulatorArgs, mountDir, onExit: () => { this.sendEvent(new debugadapter_1.TerminatedEvent()); }, }; if (debug) { await this.emulator.debug({ ...runOpts, serverPort: args.serverPort, remoteProgram: args.remoteProgram, }); } else { await this.emulator.run(runOpts); } } else { debugadapter_1.logger.log(`[LAUNCH] Not starting emulator`); } if (!debug) { debugadapter_1.logger.log(`[LAUNCH] Not debugging`); return; } this.sendEvent(new debugadapter_1.OutputEvent(help_1.helpSummary, "console")); // Connect to the remote debugger await (0, promise_retry_1.default)((retry, attempt) => { debugadapter_1.logger.log(`[LAUNCH] Connecting to remote debugger... [${attempt}]`); return this.gdb .connect(args.serverName, args.serverPort) .catch(retry); }, { retries: 20, factor: 1.1 }); this.gdb.setExceptionBreakpoint(args.exceptionMask); for (const threadId of [hardware_1.Threads.CPU, hardware_1.Threads.COPPER]) { this.sendEvent(new debugadapter_1.ThreadEvent("started", threadId)); } // Get info to Initialize source map const [hunks, offsets] = await Promise.all([ (0, amigaHunkParser_1.parseHunksFromFile)(args.program), this.gdb.getOffsets(), ]); const sourceMap = new sourceMap_1.default(hunks, offsets); // Initialize managers: this.variables = new variableManager_1.default(this.gdb, sourceMap, this.getSourceConstantResolver(customArgs), args.memoryFormats); this.disassembly = new disassembly_1.DisassemblyManager(this.gdb, this.variables, sourceMap); this.breakpoints = new breakpointManager_1.default(this.gdb, sourceMap, this.disassembly, this.dataBreakpointSizes); this.stack = new stackManager_1.default(this.gdb, sourceMap, this.disassembly); if (args.stopOnEntry) { debugadapter_1.logger.log("[LAUNCH] Stopping on entry"); await this.gdb.stepIn(hardware_1.Threads.CPU); this.sendStoppedEvent(hardware_1.Threads.CPU, "entry"); } // Tell client that we can now handle breakpoints etc. this.sendEvent(new debugadapter_1.InitializedEvent()); } catch (err) { this.sendEvent(new debugadapter_1.TerminatedEvent()); response.success = false; if (err instanceof Error) { response.message = err.message; } } this.sendResponse(response); } configurationDoneRequest(response) { this.handleAsyncRequest(response, async () => { if (!this.stopOnEntry) { debugadapter_1.logger.log("Continuing execution after config done"); await this.gdb.continueExecution(hardware_1.Threads.CPU); } }); } restartRequest(response) { this.handleAsyncRequest(response, async () => { await this.gdb.monitor("reset"); }); } shutdown() { debugadapter_1.logger.log(`Shutting down`); this.gdb.destroy(); this.emulator?.destroy(); } // Breakpoints: async setBreakPointsRequest(response, args) { this.handleAsyncRequest(response, async () => { const breakpoints = await this.breakpointManager().setSourceBreakpoints(args.source, args.breakpoints || []); response.body = { breakpoints }; response.success = true; }); } async setInstructionBreakpointsRequest(response, args) { this.handleAsyncRequest(response, async () => { const breakpoints = await this.breakpointManager().setInstructionBreakpoints(args.breakpoints); response.body = { breakpoints }; response.success = true; }); } async dataBreakpointInfoRequest(response, args) { this.handleAsyncRequest(response, async () => { if (!args.variablesReference || !args.name) { return; } const variables = this.variableManager(); const { type } = variables.getScopeReference(args.variablesReference); if (type === variableManager_1.ScopeType.Symbols || type === variableManager_1.ScopeType.Registers) { const variableName = args.name; const vars = await variables.getVariables(); const value = vars[variableName]; if (typeof value === "number") { const displayValue = variables.formatVariable(variableName, value); const isRegister = type === variableManager_1.ScopeType.Registers; const dataId = `${variableName}(${displayValue})`; response.body = { dataId, description: isRegister ? displayValue : dataId, accessTypes: ["read", "write", "readWrite"], canPersist: true, }; } } }); } async setDataBreakpointsRequest(response, args) { this.handleAsyncRequest(response, async () => { for (const i in args.breakpoints) { const bp = args.breakpoints[i]; const { name, displayValue } = this.breakpointManager().parseDataId(bp.dataId); const size = this.dataBreakpointSizes.get(bp.dataId); if (!size) { const sizeInput = await this.getDataBreakpointSize(displayValue, name); this.dataBreakpointSizes.set(bp.dataId, sizeInput); } } const breakpoints = await this.breakpointManager().setDataBreakpoints(args.breakpoints); response.body = { breakpoints }; response.success = true; }); } async setExceptionBreakPointsRequest(response, args) { this.handleAsyncRequest(response, async () => { if (this.exceptionMask) { // There is only one filter - "all exceptions" so just use it to toggle on/off if (args.filters.length > 0) { await this.gdb.setExceptionBreakpoint(this.exceptionMask); } else { await this.gdb.setExceptionBreakpoint(0); } } response.success = true; }); } // Running program info: async threadsRequest(response) { response.body = { threads: [ { id: hardware_1.Threads.CPU, name: "cpu" }, { id: hardware_1.Threads.COPPER, name: "copper" }, ], }; this.sendResponse(response); } async stackTraceRequest(response, { threadId }) { this.handleAsyncRequest(response, async () => { const stack = this.stackManager(); const positions = await stack.getPositions(threadId); const stackFrames = await stack.getStackTrace(threadId, positions); stackFrames.map(({ source }) => this.processSource(source)); if (threadId === hardware_1.Threads.CPU && positions[0]) { await this.onCpuFrame(positions[0].pc); const breakpoints = this.breakpointManager(); if (breakpoints.temporaryBreakpointAtAddress(positions[0].pc)) { await breakpoints.clearTemporaryBreakpoints(); } } response.body = { stackFrames, totalFrames: positions.length }; }); } scopesRequest(response, { frameId }) { this.handleAsyncRequest(response, async () => { response.body = { scopes: this.variableManager().getScopes(frameId), }; }); } async exceptionInfoRequest(response) { this.handleAsyncRequest(response, async () => { const haltStatus = await this.gdb.getHaltStatus(); if (haltStatus) { response.body = { exceptionId: haltStatus.signal.toString(), description: haltStatus.label, breakMode: "always", }; } }); } // Execution flow: async pauseRequest(response, { threadId }) { this.sendResponse(response); await this.gdb.pause(hardware_1.Threads.CPU); this.sendStoppedEvent(threadId, "pause"); } async continueRequest(response) { response.body = { allThreadsContinued: true }; this.sendResponse(response); await this.gdb.continueExecution(hardware_1.Threads.CPU); } async nextRequest(response, { threadId }) { this.sendResponse(response); const [frame] = await this.stackManager().getPositions(threadId); await this.gdb.stepToRange(threadId, frame.pc, frame.pc); this.sendStoppedEvent(threadId, "step"); } async stepInRequest(response, { threadId }) { this.sendResponse(response); if (threadId === hardware_1.Threads.COPPER) { const [frame] = await this.stackManager().getPositions(threadId); await this.gdb.stepToRange(threadId, frame.pc, frame.pc); } else { await this.gdb.stepIn(threadId); } this.sendStoppedEvent(threadId, "step"); } async stepOutRequest(response, { threadId }) { this.sendResponse(response); const positions = await this.stackManager().getPositions(threadId); if (positions[1]) { // Set a temp breakpoint after PC of prev stack frame const { pc } = positions[1]; await this.breakpointManager().addTemporaryBreakpoints(pc); await this.gdb.continueExecution(threadId); } else { // Step over instead const { pc } = positions[0]; await this.gdb.stepToRange(threadId, pc, pc); this.sendStoppedEvent(threadId, "step"); } } // Variables: async variablesRequest(response, args) { this.handleAsyncRequest(response, async () => { // Try to look up stored reference const variables = await this.variableManager().getVariablesByReference(args.variablesReference); response.body = { variables }; }); } async setVariableRequest(response, { variablesReference, name, value }) { this.handleAsyncRequest(response, async () => { const newValue = await this.variableManager().setVariable(variablesReference, name, value); response.body = { value: newValue, }; }); } async evaluateRequest(response, args) { this.handleAsyncRequest(response, async () => { args.expression = args.expression.trim(); // UAE debug console commands with '$' prefix if (args.expression.startsWith("$")) { const res = await this.gdb.monitor("console " + args.expression.substring(1).trim()); this.sendEvent(new debugadapter_1.OutputEvent(res, "console")); response.body = { result: "", variablesReference: 0 }; return; } // Command help if (args.expression.match(/^h\s/i)) { const cmd = args.expression.replace(/^h\s+/, ""); const help = help_1.commandHelp[cmd] || `No help available for command '${cmd}'`; this.sendEvent(new debugadapter_1.OutputEvent(help, "console")); response.body = { result: "", variablesReference: 0 }; return; } // Expression const body = await this.variableManager().evaluateExpression(args); if (body) { response.body = body; return; } // Default help summary response.body = { result: "", variablesReference: 0 }; this.sendEvent(new debugadapter_1.OutputEvent(help_1.helpSummary, "console")); }); } async completionsRequest(response, args) { this.handleAsyncRequest(response, async () => { const targets = await this.variableManager().getCompletions(args.text, args.frameId); response.body = { targets }; }); } // Memory: async readMemoryRequest(response, args) { this.handleAsyncRequest(response, async () => { const address = parseInt(args.memoryReference); let size = 0; let memory = ""; const maxChunkSize = 1000; let remaining = args.count; while (remaining > 0) { let chunkSize = maxChunkSize; if (remaining < chunkSize) { chunkSize = remaining; } memory += await this.gdb.readMemory(address + size, chunkSize); remaining -= chunkSize; size += chunkSize; } let unreadable = args.count - size; if (unreadable < 0) { unreadable = 0; } response.body = { address: address.toString(16), data: (0, strings_1.hexToBase64)(memory), }; }); } async writeMemoryRequest(response, args) { this.handleAsyncRequest(response, async () => { let address = parseInt(args.memoryReference); if (args.offset) { address += args.offset; } const hexString = (0, strings_1.base64ToHex)(args.data); const count = hexString.length; const maxChunkSize = 1000; let remaining = count; let size = 0; while (remaining > 0) { let chunkSize = maxChunkSize; if (remaining < chunkSize) { chunkSize = remaining; } await this.gdb.writeMemory(address, hexString.substring(size, chunkSize)); remaining -= chunkSize; size += chunkSize; } response.body = { bytesWritten: size, }; }); } // Disassembly: async disassembleRequest(response, args) { this.handleAsyncRequest(response, async () => { const instructions = await this.disassemblyManager().disassemble(args); instructions.map(({ location }) => this.processSource(location)); response.body = { instructions }; }); } sourceRequest(response, args) { this.handleAsyncRequest(response, async () => { const content = await this.disassembly?.getDisassembledFileContentsByRef(args.sourceReference); if (!content) { throw new Error("Source not found"); } response.body = { content }; }); } async customRequest(command, response, // eslint-disable-next-line @typescript-eslint/no-explicit-any args) { if (command === "disassembledFileContents") { const fileReq = args; const content = await this.disassemblyManager().getDisassembledFileContentsByPath(fileReq.path); response.body = { content }; return this.sendResponse(response); } if (command === "modifyVariableFormat") { const variableReq = args; this.variableManager().setVariableFormat(variableReq.variableInfo.variable.name, variableReq.variableDisplayFormat); this.sendEvent(new debugadapter_1.InvalidatedEvent(["variables"])); return this.sendResponse(response); } super.customRequest(command, response, args); } // Internals: async handleAsyncRequest(response, cb) { try { await cb(); } catch (err) { if (err instanceof Error) { // Display stack trace in trace mode response.message = this.trace ? err.stack ?? err.message : err.message; } response.success = false; } this.sendResponse(response); } sendStoppedEvent(threadId, reason, preserveFocusHint) { this.sendEvent({ event: "stopped", body: { reason, threadId, allThreadsStopped: true, preserveFocusHint, }, }); } errorString(err) { if (err instanceof Error) return this.trace ? err.stack || err.message : err.message; return String(err); } /** * Process a Source object before returning in to the client */ processSource(source) { // Ensure path is in correct format for client if (source?.path) { source.path = this.convertDebuggerPathToClient(source.path); } } /** * Event handler for stop/halt event */ async handleStop(e) { debugadapter_1.logger.log(`[STOP] ${e.label} thread: ${e.threadId}`); // Any other halt code other than TRAP must be an exception: if (e.signal !== gdbClient_1.HaltSignal.TRAP) { debugadapter_1.logger.log(`[STOP] Exception`); this.sendStoppedEvent(hardware_1.Threads.CPU, "exception"); return; } // Get stack position to find current PC address for thread const threadId = e.threadId ?? hardware_1.Threads.CPU; const { pc, stackFrameIndex } = await this.stackManager().getStackPosition(threadId, gdbClient_1.DEFAULT_FRAME_INDEX); const manager = this.breakpointManager(); // Check temporary breakpoints: // Are we waiting for a temporary breakpoint? if (manager.hasTemporaryBreakpoints()) { // Did we hit it or something else? if (manager.temporaryBreakpointAtAddress(pc)) { debugadapter_1.logger.log(`[STOP] Matched temporary breakpoint at address ${(0, strings_1.formatAddress)(pc)}`); await manager.clearTemporaryBreakpoints(); this.sendStoppedEvent(hardware_1.Threads.CPU, "step"); } else { debugadapter_1.logger.log(`[STOP] ignoring while waiting for temporary breakpoint`); await this.gdb.continueExecution(hardware_1.Threads.CPU); } return; } // Check instruction breakpoints: if (manager.instructionBreakpointAtAddress(pc)) { debugadapter_1.logger.log(`[STOP] Matched instruction breakpoint at address ${(0, strings_1.formatAddress)(pc)}`); this.sendStoppedEvent(threadId, "instruction breakpoint"); return; } // Check source / data breakpoints: let ref = manager.sourceBreakpointAtAddress(pc); let type = "breakpoint"; if (!ref) { ref = manager.dataBreakpointAtAddress(pc); type = "data breakpoint"; } if (!ref) { debugadapter_1.logger.log(`[STOP] No breakpoint found at address ${(0, strings_1.formatAddress)(pc)}`); this.sendStoppedEvent(threadId, "breakpoint"); return; } debugadapter_1.logger.log(`[STOP] Matched ${type} at address ${(0, strings_1.formatAddress)(pc)}`); // Decide whether to stop at this breakpoint or continue // Needs to check for conditional / log breakpoints etc. let shouldStop = true; const bp = ref.breakpoint; // Log point: const logMessage = bp.logMessage; if (logMessage) { // Interpolate variables const message = await (0, strings_1.replaceAsync)(logMessage, /\{((#\{((#\{[^}]*\})|[^}])*\})|[^}])*\}/g, // Up to two levels of nesting async (match) => { try { const v = await this.variableManager().evaluate(match.substring(1, match.length - 1), stackFrameIndex); return (0, strings_1.formatHexadecimal)(v, 0); } catch { return "#error"; } }); this.sendEvent(new debugadapter_1.OutputEvent(message + "\n", "console")); shouldStop = false; } // Conditional breakpoint: if (bp.condition) { const result = await this.variableManager().evaluate(bp.condition, stackFrameIndex); const stop = !!result; debugadapter_1.logger.log(`[STOP] Evaluated conditional breakpoint ${bp.condition} = ${stop}`); } // Hit count: if (bp.hitCondition) { const evaluatedCondition = await this.variableManager().evaluate(bp.hitCondition, stackFrameIndex); debugadapter_1.logger.log(`[STOP] Hit count: ${ref.hitCount}/${evaluatedCondition}`); if (++ref.hitCount === evaluatedCondition) { this.gdb.removeBreakpoint(pc); } else { shouldStop = false; } } if (shouldStop) { debugadapter_1.logger.log(`[STOP] stopping at ${type}`); this.sendStoppedEvent(threadId, type); } else { debugadapter_1.logger.log(`[STOP] continuing execution`); await this.gdb.continueExecution(threadId); } } // Implementation specific overrides: // VS Code extension will override these methods to add native behaviours. getSourceConstantResolver(args) { return new vasm_1.VasmSourceConstantResolver(args.vasm); } async onCpuFrame(_address) { // This will trigger an update to the disassembly view } async getDataBreakpointSize(_displayValue, _name) { // This will prompt for user input in VS Code // TODO: could have a better guess at size by looking at disassembly e.g. `dc.l` or address of next label return 2; } // Manager getters: // Ensures these are defined i.e. the program is started breakpointManager() { if (!this.breakpoints) { throw new Error("BreakpointManager not initialized"); } return this.breakpoints; } variableManager() { if (!this.variables) { throw new Error("VariableManager not initialized"); } return this.variables; } disassemblyManager() { if (!this.disassembly) { throw new Error("DisassemblyManager not initialized"); } return this.disassembly; } stackManager() { if (!this.stack) { throw new Error("StackManager not initialized"); } return this.stack; } } exports.UAEDebugSession = UAEDebugSession; //# sourceMappingURL=debugSession.js.map