DAP Server 实现
调试适配器是调试器扩展的灵魂:它接收 VS Code 发来的 DAP 请求,驱动真实的目标调试器或运行时,再把结果以响应/事件形式返回。本文从零搭建一个 DAP Server。
适配器的运行方式
适配器与 VS Code 的通信有两条路:
| 方式 | 传输 | 实现 |
|---|---|---|
| 标准输入输出 | VS Code 启动进程,经 stdin/stdout 收发消息 | child_process 启动,适配器读 stdin 写 stdout |
| TCP Socket | 适配器监听端口,VS Code 作为客户端连接 | net.createServer 启动,适配器作服务端 |
方式一:child_process 启动
VS Code 通过 DebugAdapterExecutable 启动进程,适配器进程读 stdin、写 stdout:
typescript
import * as readline from 'readline';
import * as process from 'process';
// 逐行读取 stdin,解析 DAP 消息
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
rl.on('line', (line) => {
// 每行一条 JSON 消息
const msg = JSON.parse(line);
handleMessage(msg);
});方式二:net.createServer 启动
适配器作为 TCP 服务端,监听指定端口:
typescript
import * as net from 'net';
const server = net.createServer((socket) => {
socket.setEncoding('utf8');
let buffer = '';
socket.on('data', (chunk) => {
buffer += chunk;
// 按换行符切分完整消息
let index = buffer.indexOf('\n');
while (index !== -1) {
const line = buffer.slice(0, index).trim();
buffer = buffer.slice(index + 1);
if (line) {
handleMessage(JSON.parse(line), socket);
}
index = buffer.indexOf('\n');
}
});
});
server.listen(4711, '127.0.0.1', () => {
console.log('Debug Adapter listening on 4711');
});消息编解码
DAP 使用 JSON-RPC 风格的消息,三个字段:seq(序号)、type(request/response/event)、command(命令名)。消息以换行符分隔,每行一个 JSON 对象:
typescript
interface DAPMessage {
seq: number;
type: 'request' | 'response' | 'event';
command?: string;
event?: string;
arguments?: any;
body?: any;
success?: boolean;
request_seq?: number;
message?: string;
}
let seq = 0;
// 发送响应
function sendResponse(
socket: any,
request: any,
body: any = {},
success = true
) {
const msg: DAPMessage = {
seq: ++seq,
type: 'response',
request_seq: request.seq,
command: request.command,
success,
body
};
socket.write(JSON.stringify(msg) + '\n');
}
// 发送事件
function sendEvent(socket: any, event: string, body: any = {}) {
const msg: DAPMessage = {
seq: ++seq,
type: 'event',
event,
body
};
socket.write(JSON.stringify(msg) + '\n');
}核心请求处理
一个最小可用 DAP Server 必须处理五个请求,按时序分别是 initialize、launch、setBreakpoints、configurationDone,运行中处理 continue 等控制请求。
InitializeRequest
会话开始的第一个请求,适配器在此声明能力:
typescript
let capabilities: any = {};
function handleRequest(msg: DAPMessage, socket: any) {
switch (msg.command) {
case 'initialize':
// 声明支持的能力
capabilities = {
supportsConfigurationDoneRequest: true,
supportsSetVariable: true,
supportsEvaluateForHovers: true,
supportsConditionalBreakpoints: true,
supportsHitConditionalBreakpoints: true
};
sendResponse(socket, msg, capabilities);
// 初始化完成后必须发送 initialized 事件,编辑器才开始设断点
sendEvent(socket, 'initialized');
break;
}
}关键能力标志:
| 能力 | 含义 |
|---|---|
supportsConfigurationDoneRequest | 支持 configurationDone 请求(强烈建议) |
supportsConditionalBreakpoints | 支持条件断点 |
supportsEvaluateForHovers | 支持悬停求值 |
supportsSetVariable | 支持运行时修改变量 |
supportsTerminateRequest | 支持 terminate 请求 |
LaunchRequest
launch 请求携带 launch.json 中解析好的完整配置,适配器在这里启动被调试程序:
typescript
import { spawn, ChildProcess } from 'child_process';
let runtimeProcess: ChildProcess | undefined;
function handleLaunch(msg: DAPMessage, socket: any) {
const config = msg.arguments;
// 启动目标程序(示例:启动一个 Node 脚本并附加调试)
runtimeProcess = spawn('node', [
'--inspect-brk=9229',
config.program,
...(config.args || [])
], {
cwd: config.cwd,
env: { ...process.env, ...config.env }
});
// 转发程序输出到调试控制台
runtimeProcess.stdout.on('data', (data) => {
sendEvent(socket, 'output', {
category: 'stdout',
output: data.toString()
});
});
runtimeProcess.stderr.on('data', (data) => {
sendEvent(socket, 'output', {
category: 'stderr',
output: data.toString()
});
});
runtimeProcess.on('exit', (code) => {
sendEvent(socket, 'terminated', {});
});
sendResponse(socket, msg, {});
}SetBreakpointsRequest
编辑器在 initialized 事件后发送所有断点:
typescript
let breakpoints: Map<string, number[]> = new Map();
function handleSetBreakpoints(msg: DAPMessage, socket: any) {
const args = msg.arguments;
// args.source.path 文件路径,args.breakpoints 是断点行号数组
const sourcePath = args.source.path;
const lines = (args.breakpoints || []).map((bp: any) => bp.line);
breakpoints.set(sourcePath, lines);
// 返回每个断点的验证结果(可在真实调试器中验证行号是否有效)
const breakpointsResponse = (args.breakpoints || []).map((bp: any) => ({
verified: true,
line: bp.line,
// 可添加 id,供后续命中时关联
id: bp.line * 1000 + Math.random() * 1000
}));
sendResponse(socket, msg, { breakpoints: breakpointsResponse });
}断点条件处理:
typescript
function handleSetBreakpoints(msg: DAPMessage, socket: any) {
const args = msg.arguments;
const result = (args.breakpoints || []).map((bp: any) => {
// bp.condition:条件表达式;bp.hitCondition:命中次数
return {
verified: true,
line: bp.line,
condition: bp.condition,
hitCondition: bp.hitCondition
};
});
sendResponse(socket, msg, { breakpoints: result });
}ConfigurationDoneRequest
编辑器设置完所有断点后发送此请求,适配器应让程序开始运行:
typescript
function handleConfigurationDone(msg: DAPMessage, socket: any) {
sendResponse(socket, msg, {});
// 程序开始执行(如 stopOnEntry 则先停在入口)
if (config.stopOnEntry) {
sendStopped(socket, 'entry');
} else {
continueExecution();
}
}ContinueRequest 与执行控制
typescript
function handleContinue(msg: DAPMessage, socket: any) {
continueExecution();
// 必须返回 allThreadsContinued,并配合 continued 事件
sendResponse(socket, msg, { allThreadsContinued: true });
sendEvent(socket, 'continued', { threadId: 1, allThreadsContinued: true });
}
// 程序停住时通知编辑器
function sendStopped(socket: any, reason: string, threadId = 1) {
sendEvent(socket, 'stopped', {
reason, // breakpoint / step / entry / pause
threadId,
// 命中断点时附上断点 id
hitBreakpointIds: currentBreakpointIds
});
}主循环完整结构
typescript
import { createServer, Socket } from 'net';
class DapServer {
private socket: Socket | undefined;
private seq = 0;
start(port: number) {
const server = createServer((socket) => {
this.socket = socket;
socket.setEncoding('utf8');
let buffer = '';
socket.on('data', (chunk) => {
buffer += chunk;
let index = buffer.indexOf('\n');
while (index !== -1) {
const line = buffer.slice(0, index).trim();
buffer = buffer.slice(index + 1);
if (line) {
this.handle(JSON.parse(line));
}
index = buffer.indexOf('\n');
}
});
});
server.listen(port, '127.0.0.1');
}
handle(msg: any) {
if (msg.type !== 'request') return;
switch (msg.command) {
case 'initialize':
this.sendResponse(msg, this.capabilities());
this.sendEvent('initialized');
break;
case 'launch':
this.onLaunch(msg);
break;
case 'setBreakpoints':
this.onSetBreakpoints(msg);
break;
case 'configurationDone':
this.sendResponse(msg, {});
this.startProgram();
break;
case 'continue':
this.onContinue(msg);
break;
case 'disconnect':
this.sendResponse(msg, {});
process.exit(0);
break;
default:
this.sendResponse(msg, {}, false);
}
}
sendResponse(request: any, body: any, success = true) {
this.socket?.write(JSON.stringify({
seq: ++this.seq,
type: 'response',
request_seq: request.seq,
command: request.command,
success,
body
}) + '\n');
}
sendEvent(event: string, body: any = {}) {
this.socket?.write(JSON.stringify({
seq: ++this.seq,
type: 'event',
event,
body
}) + '\n');
}
}调试适配器日志
适配器独立于 Extension Host 运行,错误定位困难,必须善用日志:
typescript
import * as fs from 'fs';
import * as path from 'path';
const logFile = fs.createWriteStream(
path.join(__dirname, 'dap.log'),
{ flags: 'a' }
);
function log(message: string) {
logFile.write(`[${new Date().toISOString()}] ${message}\n`);
}
// 记录每条收到的请求
function handle(msg: any) {
log(`RECV ${msg.type} ${msg.command || msg.event} ${JSON.stringify(msg.arguments || msg.body)}`);
// ...处理逻辑
}在 launch.json 中增加配置项让用户控制日志输出:
json
{
"trace": true,
"tracePath": "${workspaceFolder}/.debug/dap.log"
}VS Code 也会在调试控制台中显示适配器的 stderr 输出,适配器进程未捕获的异常可直接 console.error 输出便于排查。
DAP Server 是调试器扩展的核心,掌握消息循环与五个基础请求后,下一步实现断点命中时的调用栈、作用域与变量展示。