# How does vscode debugger extension work? ###### tags: `VSCode` 'trace code` ## [Debug adapter protocol](https://microsoft.github.io/debug-adapter-protocol/) ![](https://i.imgur.com/2iMPJt9.png) ### [coretex-debug](https://github.com/Marus/cortex-debug) 這個 project 的 wiki 裡有很好的解說。 ![](https://i.imgur.com/GRS61aa.png) Extension 必須用 Javascript 寫(總之就是要跑在 nodejs 上),而 debug adapter 可以用任意語言來實現。 ### Extension 有哪些方式來跟 debug adapter 溝通? * Socket * NamedPipe * Supported from [v1.49](https://code.visualstudio.com/updates/v1_49) * Stdin/stdout * 就是 create 一個 subprocess * Inline * 必須用 Javascript 寫,直接呼叫 API ### Extension 需要做什麼? * Launch debugger(optional) * e.g. gdb-server * Communicate with debug adapter ### Debug adapter * 把 DAP 的 protocol 轉成真正 debugger 的 protocol ### Example 官方 [library](https://github.com/microsoft/vscode-debugadapter-node) 提供了很多基礎功能,沒什麼理由不用。 #### [vscode-mock-debug](https://github.com/microsoft/vscode-mock-debug) 官方給的 debug extension,用 markdown 文件來模擬 debug 的行為。並沒有真的 debugger 在執行。 ![](https://i.imgur.com/FnmzDXR.png) ```typescript // extension.ts // 用來說明四種跟 debug adapter 溝通的方式。 export function activate(context: vscode.ExtensionContext) { switch (runMode) { case 'server': // run the debug adapter as a server inside the extension and communicate via a socket activateMockDebug(context, new MockDebugAdapterServerDescriptorFactory()); break; case 'namedPipeServer': // run the debug adapter as a server inside the extension and communicate via a named pipe (Windows) or UNIX domain socket (non-Windows) activateMockDebug(context, new MockDebugAdapterNamedPipeServerDescriptorFactory()); break; case 'external': default: // run the debug adapter as a separate process activateMockDebug(context, new DebugAdapterExecutableFactory()); break; case 'inline': // run the debug adapter inside the extension and directly talk to it activateMockDebug(context); break; } } ``` ```typescript export function activateMockDebug(context: vscode.ExtensionContext, factory?: vscode.DebugAdapterDescriptorFactory) { if (!factory) { factory = new InlineDebugAdapterFactory(); } context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory('mock', factory)); } class DebugAdapterExecutableFactory implements vscode.DebugAdapterDescriptorFactory { createDebugAdapterDescriptor(_session: vscode.DebugSession, executable: vscode.DebugAdapterExecutable | undefined): ProviderResult<vscode.DebugAdapterDescriptor> { if (!executable) { const command = "absolute path to my DA executable"; const args = [ "some args", "another arg" ]; const options = { cwd: "working directory for executable", env: { "envVariable": "some value" } }; executable = new vscode.DebugAdapterExecutable(command, args, options); } return executable; } } class MockDebugAdapterServerDescriptorFactory implements vscode.DebugAdapterDescriptorFactory { createDebugAdapterDescriptor(session: vscode.DebugSession, executable: vscode.DebugAdapterExecutable | undefined): vscode.ProviderResult<vscode.DebugAdapterDescriptor> { if (!this.server) { this.server = Net.createServer(socket => { const session = new MockDebugSession(workspaceFileAccessor); session.setRunAsServer(true); session.start(socket as NodeJS.ReadableStream, socket); }).listen(0); } return new vscode.DebugAdapterServer((this.server.address() as Net.AddressInfo).port); } } class MockDebugAdapterNamedPipeServerDescriptorFactory implements vscode.DebugAdapterDescriptorFactory { createDebugAdapterDescriptor(session: vscode.DebugSession, executable: vscode.DebugAdapterExecutable | undefined): vscode.ProviderResult<vscode.DebugAdapterDescriptor> { if (!this.server) { const pipeName = randomBytes(10).toString('utf8'); const pipePath = platform === "win32" ? join('\\\\.\\pipe\\', pipeName) : join(tmpdir(), pipeName); this.server = Net.createServer(socket => { const session = new MockDebugSession(workspaceFileAccessor); session.setRunAsServer(true); session.start(<NodeJS.ReadableStream>socket, socket); }).listen(pipePath); } return new vscode.DebugAdapterNamedPipeServer(this.server.address() as string); } } class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory { createDebugAdapterDescriptor(_session: vscode.DebugSession): ProviderResult<vscode.DebugAdapterDescriptor> { return new vscode.DebugAdapterInlineImplementation(new MockDebugSession(workspaceFileAccessor)); } } ``` 四種方式都需要去繼承 `DebugAdapterDescriptorFactory` 不過也只是初始化並傳些參數給 library 的實作,最後透過 `registerDebugAdapterDescriptorFactory` 來註冊。 `MockDebugSession` 的 base class [`DebugSession`](https://sourcegraph.com/github.com/microsoft/vscode-debugadapter-node@main/-/blob/adapter/src/debugSession.ts) 定義了 `dispatchRequest` 來處理 DAP 的 protocol,`MockDebugSession` 要做的就是 override 這些 methods。真實的例子需要在這些 method 把 DAP 轉成 debugger 的 protocol。 ```typescript // DebugSession.ts protected dispatchRequest(request: DebugProtocol.Request): void { const response = new Response(request); if (request.command === 'initialize') { // ... } else if (request.command === 'launch') { this.launchRequest(<DebugProtocol.LaunchResponse> response, request.arguments, request); } else if (request.command === 'attach') { this.attachRequest(<DebugProtocol.AttachResponse> response, request.arguments, request); } else if (request.command === 'disconnect') { this.disconnectRequest(<DebugProtocol.DisconnectResponse> response, request.arguments, request); } else if (request.command === 'setInstructionBreakpoints') { this.setInstructionBreakpointsRequest(<DebugProtocol.SetInstructionBreakpointsResponse> response, request.arguments, request); } else { this.customRequest(request.command, <DebugProtocol.Response> response, request.arguments, request); } } // MockDebugSession.ts, take one method as an example protected async setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): Promise<void> { const path = args.source.path as string; const clientLines = args.lines || []; // clear all breakpoints for this file this._runtime.clearBreakpoints(path); // set and verify breakpoint locations const actualBreakpoints0 = clientLines.map(async l => { const { verified, line, id } = await this._runtime.setBreakPoint(path, this.convertClientLineToDebugger(l)); const bp = new Breakpoint(verified, this.convertDebuggerLineToClient(line)) as DebugProtocol.Breakpoint; bp.id = id; return bp; }); const actualBreakpoints = await Promise.all<DebugProtocol.Breakpoint>(actualBreakpoints0); // send back the actual breakpoint positions response.body = { breakpoints: actualBreakpoints }; this.sendResponse(response); } ``` #### [code-debug](https://github.com/WebFreak001/code-debug) 支援 gdb & lldb(我沒用過), 一開始有點疑惑,因為微軟的 c++ extension 就可以用 gdb 來 debug 了,後來才想明白因為 gdb 支援多種程式語言,而這個 adapter 就可以用 gdb 來 debug 各種語言了。 Debug adapter 跟 gdb/lldb 之間是用 [Mahcine interface](https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI.html) 這個 protocol,之前完全沒聽過這個東西,概念跟 DAP 差不多,都是想用一個 interface 去接多種 debugger。 #### [cortex-debug](https://github.com/Marus/cortex-debug) 用來 debug ARM cortex-M 系列的裝置。做的事情是 launch openOCD(這裡面會實作 gdb server) 和一些 config。有了 gdb server 其實 adapter 也就不需要知道這個 debugger 其實是 openOCD,具體對裝置的操作也是由 openOCD 來完成。 ## Murmur 看完沒寫個紀錄真的忘得很快。