DebugAdapterDescriptorFactory 调试适配器工厂
调试器扩展的 VS Code 端核心任务只有一个:把 launch.json 中的配置翻译成"如何启动一个调试适配器"。这个翻译工作由 DebugAdapterDescriptorFactory 完成。
什么是调试适配器描述
VS Code 需要知道调试适配器以什么方式运行,DebugAdapterDescriptor 就是对这个问题的回答,它有三种形态:
| 类型 | 说明 | 适用场景 |
|---|---|---|
DebugAdapterExecutable | 启动一个可执行文件 | 适配器是独立程序(Node.js/编译产物/系统命令) |
DebugAdapterServer | 连接一个已有端口 | 适配器已以 server 模式运行,或调试器本身是远程服务 |
DebugAdapterImplementation | 在插件进程内联运行 | 轻量适配器,减少进程开销,但会阻塞 Extension Host |
typescript
import * as vscode from 'vscode';
// 三种描述的构造方式
const executable = new vscode.DebugAdapterExecutable(
'node', // 可执行文件
['-r', 'ts-node/register', './out/debugAdapter.js'], // 参数
{ env: { DEBUG: '1' } } // 可选环境变量
);
const server = new vscode.DebugAdapterServer(
4711, // 端口
'127.0.0.1' // 主机(可选)
);
const impl = new vscode.DebugAdapterInlineImplementation(
new MyDebugAdapter() // 实现了 DebugAdapter 接口的对象
);实现 DebugAdapterDescriptorFactory
工厂接口只有一个方法 createDebugAdapterDescriptor:
typescript
import * as vscode from 'vscode';
import * as path from 'path';
export class MyScriptDebugAdapterFactory
implements vscode.DebugAdapterDescriptorFactory
{
createDebugAdapterDescriptor(
session: vscode.DebugSession,
executable: vscode.DebugAdapterExecutable | undefined
): vscode.ProviderResult<vscode.DebugAdapterDescriptor> {
// session.configuration 是合并了 launch.json 和默认值的配置对象
const config = session.configuration;
// 方式一:根据配置决定启动命令
return new vscode.DebugAdapterExecutable(
config.debugAdapterPath || 'node',
[path.join(__dirname, 'debugAdapter.js')]
);
// 方式二:复用 VS Code 提供的默认可执行文件
// return executable;
// 方式三:连接外部调试服务
// return new vscode.DebugAdapterServer(config.debugPort, 'localhost');
}
}session 参数详解
session 提供了当前调试会话的全部上下文:
| 属性 | 说明 |
|---|---|
session.id | 会话唯一 ID |
session.type | 调试类型(launch.json 中的 type) |
session.name | 会话名称(launch.json 中的 name) |
session.configuration | 合并后的完整配置对象 |
session.workspaceFolder | 发起调试的工作区文件夹 |
session.customRequest() | 向适配器发送自定义请求 |
注册工厂
在 activate 中注册,指定工厂负责的调试类型:
typescript
export function activate(context: vscode.ExtensionContext) {
const factory = new MyScriptDebugAdapterFactory();
context.subscriptions.push(
vscode.debug.registerDebugAdapterDescriptorFactory(
'my-script',
factory
),
// 卸载时注销
new vscode.Disposable(() => {
vscode.debug.unregisterDebugAdapterDescriptorFactory('my-script');
})
);
}注意:registerDebugAdapterDescriptorFactory 对同一个调试类型只能注册一个工厂,重复注册会抛错。工厂注册后,该类型的所有调试会话都会经由工厂创建适配器。
动态选择适配器形态
工厂的一大优势是可以根据配置动态决定适配器的运行方式,例如同时支持本地与远程调试:
typescript
export class FlexibleAdapterFactory
implements vscode.DebugAdapterDescriptorFactory
{
async createDebugAdapterDescriptor(
session: vscode.DebugSession
): Promise<vscode.DebugAdapterDescriptor> {
const config = session.configuration;
// 远程调试:连接 SSH 转发过来的端口
if (config.remote) {
return new vscode.DebugAdapterServer(
config.remotePort,
'localhost'
);
}
// 本地调试:直接启动适配器进程
return new vscode.DebugAdapterExecutable(
'node',
['--inspect', this.adapterScript(config)]
);
}
}startDebugging 启动调试会话
除了用户在调试面板手动启动,插件也可以主动发起调试。vscode.debug.startDebugging 是启动调试的编程入口:
typescript
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('myExt.debugCurrentFile', async () => {
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document.languageId !== 'myscript') {
vscode.window.showWarningMessage('请先打开一个 MyScript 文件');
return;
}
// 从当前文件动态构造调试配置
const config: vscode.DebugConfiguration = {
type: 'my-script',
name: 'Debug Current File',
request: 'launch',
program: editor.document.uri.fsPath
};
// workspaceFolder:调试会话关联的工作区
const folder = vscode.workspace.getWorkspaceFolder(editor.document.uri);
const started = await vscode.debug.startDebugging(
folder, // 可选:工作区文件夹
config, // 调试配置
undefined // 可选:父会话(用于子会话)
);
if (!started) {
vscode.window.showErrorMessage('调试启动失败,请检查配置');
}
})
);
}startDebugging 参数
| 参数 | 类型 | 说明 |
|---|---|---|
folder | WorkspaceFolder | 调试关联的工作区,可为 undefined |
nameOrConfiguration | string | 传入配置对象时直接使用;传入字符串时读取 launch.json 中同名校验后的配置 |
parentSession | DebugSession | 可选,用于父子调试会话(如调试器嵌套调试) |
传配置对象时,VS Code 会先经过 DebugConfigurationProvider 的解析管道再启动;传字符串时则会从 launch.json 读取并经过完整解析流程。
DebugConfigurationProvider 配置解析
launch.json 中的配置不会原样传给适配器,而是经过 DebugConfigurationProvider 的处理管道:
typescript
import * as vscode from 'vscode';
export class MyScriptConfigProvider
implements vscode.DebugConfigurationProvider
{
// 第一步:提供默认配置(合并到用户配置之前)
provideDebugConfigurations(
folder: vscode.WorkspaceFolder | undefined,
token?: vscode.CancellationToken
): vscode.ProviderResult<vscode.DebugConfiguration[]> {
return [
{
type: 'my-script',
name: 'Launch Script',
request: 'launch',
program: '${workspaceFolder}/main.ms',
stopOnEntry: false
}
];
}
// 第二步:解析动态值(在变量替换之前调用)
resolveDebugConfiguration(
folder: vscode.WorkspaceFolder | undefined,
config: vscode.DebugConfiguration,
token?: vscode.CancellationToken
): vscode.ProviderResult<vscode.DebugConfiguration> {
// 用户未填必要字段时给出默认值
if (!config.type && !config.request && !config.name) {
// 返回 undefined 表示忽略本次启动,适合提示用户手动配置
return undefined;
}
// 补全默认路径
if (!config.program) {
config.program = '${workspaceFolder}/main.ms';
}
// 默认 stopOnEntry
if (config.stopOnEntry === undefined) {
config.stopOnEntry = false;
}
return config;
}
// 第三步:解析调试器特定变量(在标准变量替换之后)
resolveDebugConfigurationWithSubstitutedVariables(
folder: vscode.WorkspaceFolder | undefined,
config: vscode.DebugConfiguration,
token?: vscode.CancellationToken
): vscode.ProviderResult<vscode.DebugConfiguration> {
// 此时 ${workspaceFolder} 等标准变量已被替换为真实路径
if (config.program && !config.program.startsWith('/') && !/^[A-Z]:/.test(config.program)) {
config.program = require('path').join(folder.uri.fsPath, config.program);
}
return config;
}
}解析管道顺序
text
launch.json 原始配置
│
▼
provideDebugConfigurations 提供默认配置(合并)
│
▼
resolveDebugConfiguration 解析配置、校验必填项
│
▼
标准变量替换 ${workspaceFolder}、${file} 等
│
▼
resolveDebugConfigurationWithSubstitutedVariables
│ 调试器特定变量与路径处理
▼
createDebugAdapterDescriptor 工厂创建适配器描述返回值的三种含义
| 返回值 | 含义 |
|---|---|
| 修改后的配置 | 正常启动,使用处理后的配置 |
undefined | 放弃本次调试,不会启动会话 |
| 抛错/显示消息 | 中断启动,向用户展示错误 |
完整组合示例
把工厂与配置提供者组合成一个可用的调试器扩展骨架:
typescript
import * as vscode from 'vscode';
import * as path from 'path';
class AdapterFactory implements vscode.DebugAdapterDescriptorFactory {
createDebugAdapterDescriptor(
session: vscode.DebugSession
): vscode.ProviderResult<vscode.DebugAdapterDescriptor> {
const config = session.configuration;
// 支持 attach 到外部调试服务
if (config.request === 'attach') {
return new vscode.DebugAdapterServer(config.port, 'localhost');
}
// launch 模式:启动适配器进程
const adapterScript = path.join(__dirname, 'debugAdapter.js');
const env = { ...process.env, MYS_SCRIPT_DEBUG: '1' };
return new vscode.DebugAdapterExecutable('node', [adapterScript], { env });
}
}
class ConfigProvider implements vscode.DebugConfigurationProvider {
provideDebugConfigurations() {
return [{ type: 'my-script', name: 'Launch', request: 'launch', program: '' }];
}
resolveDebugConfiguration(_folder, config) {
if (!config.request) {
vscode.window.showErrorMessage('缺少 request 字段');
return undefined;
}
return config;
}
}
export function activate(context: vscode.ExtensionContext) {
const factory = new AdapterFactory();
const provider = new ConfigProvider();
context.subscriptions.push(
vscode.debug.registerDebugAdapterDescriptorFactory('my-script', factory),
vscode.debug.registerDebugConfigurationProvider('my-script', provider)
);
}工厂 + 配置提供者是调试器扩展 VS Code 端的全部装配工作,接下来需要实现真正的调试适配器,回答 DAP 协议中的各种请求。