实战:自定义脚本调试器
本文实现一个完整的脚本调试器:定义一个迷你脚本语言 MiniScript,编写它的解释器,再通过 DAP Server 把断点、单步、变量、监视全部接进 VS Code 调试面板。
总体设计
text
VS Code my-debugger 扩展 MiniScript 调试器
│ launch.json │ DebugAdapterExecutable │
│ ────────────────────────► │ 启动进程 ───────────────────► │
│ DAP 请求(stdio) │ 传递 │ 解释器执行脚本
│ ◄──────────────────────── │ ◄─────────────────────────── │ 命中断点/单步- 插件侧(extension.ts):注册
my-script调试类型,工厂启动适配器进程 - 适配器侧(adapter.js):
net创建 DAP Server + MiniScript 解释器 - 协议:DAP over stdio,适配器经 stdin/stdout 与 VS Code 通信
第一步:注册调试器类型
package.json 声明调试类型:
json
{
"contributes": {
"debuggers": [
{
"type": "my-script",
"label": "MiniScript Debugger",
"languages": ["myscript"],
"configurationAttributes": {
"launch": {
"required": ["program"],
"properties": {
"program": {
"type": "string",
"description": "要调试的 .ms 脚本"
},
"args": { "type": "array", "items": { "type": "string" } }
}
}
},
"configurationSnippets": [
{
"label": "MiniScript: Launch",
"body": {
"type": "my-script",
"request": "launch",
"name": "Launch Script",
"program": "${workspaceFolder}/${1:main.ms}"
}
}
]
}
]
}
}第二步:插件侧装配
extension.ts 注册工厂与配置提供者:
typescript
import * as vscode from 'vscode';
import * as path from 'path';
class AdapterFactory implements vscode.DebugAdapterDescriptorFactory {
createDebugAdapterDescriptor(
session: vscode.DebugSession
): vscode.ProviderResult<vscode.DebugAdapterDescriptor> {
// 启动适配器子进程,经 stdio 通信
const adapterPath = path.join(__dirname, 'adapter.js');
return new vscode.DebugAdapterExecutable('node', [adapterPath]);
}
}
class ConfigProvider implements vscode.DebugConfigurationProvider {
provideDebugConfigurations() {
return [{
type: 'my-script',
name: 'Launch Script',
request: 'launch',
program: '${workspaceFolder}/main.ms'
}];
}
resolveDebugConfiguration(_folder, config) {
if (!config.program) {
vscode.window.showErrorMessage('请配置 program 字段');
return undefined;
}
return config;
}
}
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.debug.registerDebugAdapterDescriptorFactory('my-script', new AdapterFactory()),
vscode.debug.registerDebugConfigurationProvider('my-script', new ConfigProvider())
);
}第三步:MiniScript 语言
定义一门极简命令式语言,足够演示调试特性:
text
// 示例脚本 main.ms
var total = 0
var i = 1
while i <= 5 do
total = total + i
i = i + 1
end
print total语法规则(简化):
| 语句 | 形式 | 说明 |
|---|---|---|
| 变量声明 | var name = value | 声明局部变量 |
| 赋值 | name = value | 修改变量 |
| 循环 | while cond do ... end | 条件循环 |
| 输出 | print expr | 打印表达式 |
| 函数 | function name(a, b) ... end | 定义函数 |
第四步:解释器(带调试钩子)
解释器逐语句执行,在执行每条语句前检查是否命中调试事件:
typescript
// interpreter.js
class MiniScriptInterpreter {
constructor(program: string, lineMap: number[]) {
this.lines = this.parse(program); // 语句列表,带行号
this.variables = new Map(); // 全局变量表
this.frames = []; // 调用栈
this.pc = 0; // 指令指针
}
// 执行完整程序(调试器暂停时返回)
async run(): Promise<void> {
while (this.pc < this.lines.length) {
// 关键调试钩子:每条语句执行前调用
const shouldPause = await this.debugHook.beforeStatement(this);
if (shouldPause) {
return; // 被暂停,等待 continue/step 恢复
}
const line = this.lines[this.pc];
this.execute(line);
this.pc++;
}
}
// 执行单条语句
execute(line: Statement) {
switch (line.type) {
case 'var':
this.variables.set(line.name, line.value);
break;
case 'assign':
this.variables.set(line.name, this.evalExpr(line.expr));
break;
case 'print':
console.log(this.evalExpr(line.expr));
break;
case 'while':
if (!this.evalExpr(line.condition)) {
this.pc = line.jumpTarget; // 跳出循环
}
break;
case 'jump':
this.pc = line.target;
break;
}
}
evalExpr(expr: any): number {
if (typeof expr === 'number') return expr;
if (expr.type === 'var') return this.variables.get(expr.name) || 0;
if (expr.type === 'binary') {
const left = this.evalExpr(expr.left);
const right = this.evalExpr(expr.right);
switch (expr.op) {
case '+': return left + right;
case '-': return left - right;
case '==': return left === right ? 1 : 0;
case '<=': return left <= right ? 1 : 0;
case '>=': return left >= right ? 1 : 0;
}
}
return 0;
}
}调试钩子
调试钩子连接解释器与 DAP Server,负责三件事:检查断点、执行单步、通知暂停:
typescript
// debugHook.js
class DebugHook {
constructor(interpreter, dap) {
this.interpreter = interpreter;
this.dap = dap;
this.breakpoints = new Map(); // line -> breakpoint
this.state = 'running'; // running / paused
this.stepAction = null; // next / stepIn / stepOut
}
// 每条语句执行前调用
async beforeStatement(interpreter): Promise<boolean> {
const line = interpreter.lines[interpreter.pc];
const currentLine = line.lineNumber;
// 命中断点
const bp = this.breakpoints.get(currentLine);
if (bp && this.state === 'running') {
if (bp.condition) {
const value = interpreter.evalExpr(parseExpr(bp.condition));
if (value === 0) return false;
}
this.pause('breakpoint');
return true;
}
// 单步:当前行与上次不同则暂停
if (this.stepAction === 'next' && this.state === 'running') {
if (currentLine !== this.lastLine) {
this.stepAction = null;
this.pause('step');
return true;
}
}
this.lastLine = currentLine;
return false;
}
// 暂停并通知 VS Code
pause(reason: string) {
this.state = 'paused';
this.dap.sendEvent('stopped', {
reason,
threadId: 1
});
}
}第五步:DAP Server 适配器
适配器组装解释器与调试钩子,处理全部 DAP 请求:
typescript
// adapter.js
import { createConnection } from './dapConnection';
import { MiniScriptInterpreter } from './interpreter';
import { DebugHook } from './debugHook';
import * as fs from 'fs';
class DebugAdapter {
constructor(connection) {
this.conn = connection;
this.breakpointMap = new Map();
}
handleRequest(msg) {
switch (msg.command) {
case 'initialize':
this.onInitialize(msg);
break;
case 'launch':
this.onLaunch(msg);
break;
case 'setBreakpoints':
this.onSetBreakpoints(msg);
break;
case 'configurationDone':
this.onConfigurationDone(msg);
break;
case 'threads':
this.onThreads(msg);
break;
case 'stackTrace':
this.onStackTrace(msg);
break;
case 'scopes':
this.onScopes(msg);
break;
case 'variables':
this.onVariables(msg);
break;
case 'continue':
case 'next':
this.onResume(msg, msg.command === 'continue' ? 'continue' : 'next');
break;
case 'evaluate':
this.onEvaluate(msg);
break;
case 'disconnect':
this.conn.sendResponse(msg, {});
process.exit(0);
break;
default:
this.conn.sendResponse(msg, {});
}
}
// initialize:声明能力
onInitialize(msg) {
this.conn.sendResponse(msg, {
supportsConfigurationDoneRequest: true,
supportsEvaluateForHovers: true,
supportsSetVariable: true,
supportsConditionalBreakpoints: true
});
this.conn.sendEvent('initialized');
}
// launch:读取脚本并创建解释器
onLaunch(msg) {
const config = msg.arguments;
const source = fs.readFileSync(config.program, 'utf8');
this.interpreter = new MiniScriptInterpreter(source);
this.hook = new DebugHook(this.interpreter, this.conn);
this.interpreter.debugHook = this.hook;
this.sourcePath = config.program;
this.conn.sendResponse(msg, {});
}
// setBreakpoints:登记断点
onSetBreakpoints(msg) {
const args = msg.arguments;
const file = args.source.path;
const result = (args.breakpoints || []).map((bp) => {
const line = bp.line;
const verified = this.interpreter.isExecutableLine(line);
if (verified) {
this.breakpointMap.set(line, bp);
}
return { verified, line };
});
this.conn.sendResponse(msg, { breakpoints: result });
}
// configurationDone:开始执行
onConfigurationDone(msg) {
this.conn.sendResponse(msg, {});
this.runProgram();
}
async runProgram() {
this.hook.state = 'running';
await this.interpreter.run();
// 程序自然结束
this.conn.sendEvent('terminated');
}
// 线程列表
onThreads(msg) {
this.conn.sendResponse(msg, { threads: [{ id: 1, name: 'Main' }] });
}
// 调用栈:从解释器构建
onStackTrace(msg) {
const frames = this.interpreter.frames.map((f, index) => ({
id: index + 1,
name: f.name || '<main>',
line: f.lineNumber,
column: 0,
source: { name: 'main.ms', path: this.sourcePath }
}));
// 追加当前栈底帧
frames.push({
id: frames.length + 1,
name: '<main>',
line: this.interpreter.lines[this.interpreter.pc]?.lineNumber || 1,
column: 0,
source: { name: 'main.ms', path: this.sourcePath }
});
this.conn.sendResponse(msg, { stackFrames: frames, totalFrames: frames.length });
}
// 作用域
onScopes(msg) {
this.conn.sendResponse(msg, {
scopes: [{
name: 'Locals',
variablesReference: 1,
expensive: false
}]
});
}
// 变量展开:直接读取解释器变量表
onVariables(msg) {
const ref = msg.arguments.variablesReference;
if (ref === 1) {
const vars = [...this.interpreter.variables.entries()].map(([name, value]) => ({
name,
value: String(value),
type: typeof value,
variablesReference: 0
}));
this.conn.sendResponse(msg, { variables: vars });
} else {
this.conn.sendResponse(msg, { variables: [] });
}
}
// continue / next
onResume(msg, action) {
this.conn.sendResponse(msg, { allThreadsContinued: true });
this.conn.sendEvent('continued', { threadId: 1, allThreadsContinued: true });
this.hook.state = 'running';
this.hook.stepAction = action === 'next' ? 'next' : null;
// 异步继续执行
this.interpreter.run().then(() => {
if (this.hook.state !== 'paused') {
this.conn.sendEvent('terminated');
}
});
}
// 表达式求值
onEvaluate(msg) {
const expr = msg.arguments.expression;
// 简单变量查找
if (this.interpreter.variables.has(expr)) {
const value = this.interpreter.variables.get(expr);
this.conn.sendResponse(msg, { result: String(value), type: typeof value });
return;
}
// 尝试作为表达式求值
try {
const value = this.interpreter.evalExpr(parseExpr(expr));
this.conn.sendResponse(msg, { result: String(value), type: 'number' });
} catch (e) {
this.conn.sendResponse(msg, { result: '无法求值: ' + e.message, type: 'string' }, false);
}
}
}第六步:启动适配器进程
适配器通过 stdin/stdout 与 VS Code 通信:
typescript
// dapConnection.js
import * as readline from 'readline';
export function createConnection() {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
const conn = {
seq: 0,
sendResponse(request, body, success = true) {
const msg = {
seq: ++this.seq,
type: 'response',
request_seq: request.seq,
command: request.command,
success,
body
};
process.stdout.write(JSON.stringify(msg) + '\n');
},
sendEvent(event, body) {
const msg = {
seq: ++this.seq,
type: 'event',
event,
body
};
process.stdout.write(JSON.stringify(msg) + '\n');
}
};
const adapter = new DebugAdapter(conn);
rl.on('line', (line) => {
try {
const msg = JSON.parse(line);
if (msg.type === 'request') {
adapter.handleRequest(msg);
}
} catch (e) {
console.error('消息解析失败:', e.message);
}
});
return adapter;
}
createConnection();第七步:完整调试流程验证
在 launch.json 中配置后启动调试:
json
{
"version": "0.2.0",
"configurations": [
{
"type": "my-script",
"request": "launch",
"name": "Launch Script",
"program": "${workspaceFolder}/main.ms"
}
]
}流程推演
text
1. 点击调试 → VS Code 读取配置
2. 工厂创建 DebugAdapterExecutable 启动 adapter.js
3. VS Code 发送 initialize → 适配器声明能力
4. 适配器发送 initialized → VS Code 发送 setBreakpoints
5. VS Code 发送 configurationDone → 解释器开始执行
6. 执行到断点行 → beforeStatement 命中 → 发送 stopped
7. VS Code 请求 threads/stackTrace/scopes/variables
8. 用户点击"单步跳过" → next 请求 → 执行下一行 → 再发 stopped
9. 监视面板输入 total → evaluate 请求 → 返回当前值
10. 脚本结束 → 发送 terminated → 会话结束能力对照
| 调试特性 | 实现位置 | 状态 |
|---|---|---|
| 断点(含条件断点) | DebugHook.beforeStatement + breakpointMap | 已实现 |
| 单步跳过 | DebugHook.stepAction = 'next' | 已实现 |
| 调用栈 | interpreter.frames | 已实现 |
| 变量查看 | onVariables 读取变量表 | 已实现 |
| 监视表达式 | onEvaluate | 已实现 |
| 步入/步出 | stepIn/stepOut 钩子扩展 | 可扩展 |
扩展方向
- 步入/步出:在解释器进入/退出函数时记录栈深,实现 stepIn/stepOut
- 暂停/继续:发送
pause请求时中断执行循环 - 运行时改值:
setVariable请求写回变量表 - 栈帧多帧:函数调用时压入
frames,栈帧 ID 对应不同变量快照
至此,一个能独立运行的调试器扩展完成:插件侧提供配置与进程装配,适配器侧用解释器钩子驱动 DAP 协议,VS Code 调试面板的断点、单步、变量、监视全部打通。