调试器扩展
Visual Studio Code 的调试架构允许扩展作者轻松地将现有调试器集成到 VS Code 中,同时所有调试器都拥有一个通用的用户界面。
VS Code 自带一个内置调试器扩展,即 Node.js 调试器扩展,它很好地展示了 VS Code 支持的许多调试器功能
此屏幕截图显示了以下调试功能
- 调试配置管理。
- 启动/停止和单步执行的调试操作。
- 源、函数、条件、内联断点和日志点。
- 堆栈跟踪,包括多线程和多进程支持。
- 在视图和悬停中导航复杂数据结构。
- 变量值显示在悬停提示中或内联在源代码中。
- 管理监视表达式。
- 用于交互式评估和自动完成的调试控制台。
本文档将帮助你创建调试器扩展,使任何调试器都能与 VS Code 配合使用。
VS Code 的调试架构
VS Code 实现了一个通用的(与语言无关的)调试器 UI,该 UI 基于我们引入的抽象协议来与调试器后端通信。由于调试器通常不实现此协议,因此需要一些中间层来“适配”调试器以符合该协议。此中间层通常是一个与调试器通信的独立进程。
我们将此中间层称为 调试适配器(简称 DA),而 DA 与 VS Code 之间使用的抽象协议是 调试适配器协议(简称 DAP)。由于调试适配器协议独立于 VS Code,它有自己的网站,你可以在其中找到介绍和概述、详细的规范以及包含已知实现和支持工具的列表。DAP 的历史和动机在此博客文章中有所解释。
由于调试适配器独立于 VS Code 并且可以在其他开发工具中使用,因此它们与 VS Code 基于扩展和贡献点的可扩展性架构不符。
因此,VS Code 提供了一个贡献点 debuggers
,调试适配器可以在其中以特定的调试类型(例如 Node.js 调试器的 node
)进行贡献。每当用户启动该类型的调试会话时,VS Code 就会启动注册的 DA。
因此,在最简单的形式下,调试器扩展只是调试适配器实现的一种声明性贡献,扩展基本上是调试适配器的打包容器,没有任何额外的代码。
一个更实际的调试器扩展会向 VS Code 贡献以下许多或所有声明性项
- 调试器支持的语言列表。VS Code 允许为这些语言设置断点。
- 调试器引入的调试配置属性的 JSON 模式。VS Code 使用此模式验证 launch.json 编辑器中的配置,并提供 IntelliSense。请注意,不支持 JSON 模式构造
$ref
和definition
。 - VS Code 创建的初始 launch.json 的默认调试配置。
- 用户可以添加到 launch.json 文件中的调试配置代码片段。
- 可在调试配置中使用的变量声明。
你可以在 contributes.breakpoints
和 contributes.debuggers
参考中找到更多信息。
除了上述纯粹的声明性贡献之外,调试扩展 API 还支持以下基于代码的功能
- 为 VS Code 创建的初始 launch.json 动态生成默认调试配置。
- 动态确定要使用的调试适配器。
- 在将调试配置传递给调试适配器之前对其进行验证或修改。
- 与调试适配器通信。
- 向调试控制台发送消息。
本文的其余部分将展示如何开发调试器扩展。
Mock Debug 扩展
从头开始创建一个调试适配器对于本教程来说有点复杂,因此我们将从一个简单的 DA 开始,我们将其创建为教育性的“调试适配器入门套件”。它被称为 Mock Debug 是因为它不与真正的调试器通信,而是模拟一个。Mock Debug 模拟调试器并支持单步、继续、断点、异常和变量访问,但它没有连接到任何真正的调试器。
在深入了解 mock-debug 的开发设置之前,我们首先从 VS Code Marketplace 安装一个预构建版本并试用它。
- 切换到扩展视图,输入“mock”搜索 Mock Debug 扩展,
- “安装”并“重新加载”扩展。
试用 Mock Debug
- 创建一个新的空文件夹
mock test
并在 VS Code 中打开它。 - 创建一个文件
readme.md
并输入几行任意文本。 - 切换到运行和调试视图 (⇧⌘D (Windows, Linux Ctrl+Shift+D)) 并选择 创建 launch.json 文件 链接。
- VS Code 将允许你选择一个“调试器”以创建默认启动配置。选择“Mock Debug”。
- 按下绿色的 开始 按钮,然后按 Enter 确认建议的文件
readme.md
。
调试会话开始,你可以“单步执行”readme 文件,设置并命中断点,并遇到异常(如果行中出现单词 exception
)。
在使用 Mock Debug 作为你自己的开发起点之前,我们建议首先卸载预构建版本。
- 切换到扩展视图,点击 Mock Debug 扩展的齿轮图标。
- 运行“卸载”操作,然后“重新加载”窗口。
Mock Debug 开发设置
现在让我们获取 Mock Debug 的源代码并在 VS Code 中开始开发它。
git clone https://github.com/microsoft/vscode-mock-debug.git
cd vscode-mock-debug
yarn
在 VS Code 中打开项目文件夹 vscode-mock-debug
。
包里有什么?
package.json
是 mock-debug 扩展的清单- 它列出了 mock-debug 扩展的贡献。
compile
和watch
脚本用于将 TypeScript 源代码转译到out
文件夹中,并监视后续的源代码修改。- 依赖项
vscode-debugprotocol
、vscode-debugadapter
和vscode-debugadapter-testsupport
是简化基于 Node 的调试适配器开发的 NPM 模块。
src/mockRuntime.ts
是一个具有简单调试 API 的 mock 运行时。- 将运行时 适配 到调试适配器协议的代码位于
src/mockDebug.ts
中。你可以在这里找到 DAP 各种请求的处理程序。 - 由于调试器扩展的实现位于调试适配器中,因此根本不需要有扩展代码(即在扩展宿主进程中运行的代码)。然而,Mock Debug 有一个小的
src/extension.ts
,因为它说明了在调试器扩展的扩展代码中可以做些什么。
现在,通过选择 Extension 启动配置并按下 F5
来构建和启动 Mock Debug 扩展。最初,这将把 TypeScript 源完全转译到 out
文件夹中。完整构建后,会启动一个 监视任务,它会转译你所做的任何更改。
源代码转译完成后,会弹出一个新的 VS Code 窗口,标题为“[扩展开发宿主]”,其中 Mock Debug 扩展现在以调试模式运行。从该窗口中打开你包含 readme.md
文件的 mock test
项目,按“F5”启动调试会话,然后单步执行它。
由于你正在调试模式下运行扩展,你现在可以在 src/extension.ts
中设置并命中断点,但正如我上面提到的,在扩展中执行的有趣代码并不多。有趣的代码在调试适配器中运行,它是一个独立的进程。
为了调试调试适配器本身,我们必须以调试模式运行它。最简单的方法是以 服务器模式 运行调试适配器,并配置 VS Code 连接到它。在你的 VS Code vscode-mock-debug 项目中,从下拉菜单中选择启动配置 Server 并按下绿色的启动按钮。
由于我们已经有一个活动的扩展调试会话,VS Code 调试器 UI 现在进入 多会话 模式,这通过在调用堆栈视图中看到两个调试会话 Extension 和 Server 的名称来表示。
现在我们可以同时调试扩展和 DA。更快的方法是使用 Extension + Server 启动配置,它会自动启动这两个会话。
调试扩展和 DA 的另一种更简单的方法可以在下面找到。
在文件 src/mockDebug.ts
中方法 launchRequest(...)
的开头设置一个断点,最后一步是通过为你的模拟测试启动配置添加一个端口 4711
的 debugServer
属性来配置模拟调试器连接到 DA 服务器。
{
"version": "0.2.0",
"configurations": [
{
"type": "mock",
"request": "launch",
"name": "mock test",
"program": "${workspaceFolder}/readme.md",
"stopOnEntry": true,
"debugServer": 4711
}
]
}
如果你现在启动此调试配置,VS Code 不会启动模拟调试适配器作为单独的进程,而是直接连接到已运行服务器的本地端口 4711,并且你应该会命中 launchRequest
中的断点。
通过此设置,你现在可以轻松编辑、转译和调试 Mock Debug。
但现在真正的工作开始了:你必须用与“真实”调试器或运行时通信的代码替换 src/mockDebug.ts
和 src/mockRuntime.ts
中的模拟调试适配器实现。这涉及到理解和实现调试适配器协议。有关此的更多详细信息可以在这里找到。
调试器扩展的 package.json 结构
除了提供调试适配器的特定于调试器的实现外,调试器扩展还需要一个 package.json
文件,该文件会贡献到各种与调试相关的贡献点。
那么,让我们仔细看看 Mock Debug 的 package.json
文件。
像每个 VS Code 扩展一样,package.json
声明了扩展的基本属性:名称 (name)、发布者 (publisher) 和 版本 (version)。使用 类别 (categories) 字段可以使扩展在 VS Code 扩展市场中更容易被找到。
{
"name": "mock-debug",
"displayName": "Mock Debug",
"version": "0.24.0",
"publisher": "...",
"description": "Starter extension for developing debug adapters for VS Code.",
"author": {
"name": "...",
"email": "..."
},
"engines": {
"vscode": "^1.17.0",
"node": "^7.9.0"
},
"icon": "images/mock-debug-icon.png",
"categories": ["Debuggers"],
"contributes": {
"breakpoints": [{ "language": "markdown" }],
"debuggers": [
{
"type": "mock",
"label": "Mock Debug",
"program": "./out/mockDebug.js",
"runtime": "node",
"configurationAttributes": {
"launch": {
"required": ["program"],
"properties": {
"program": {
"type": "string",
"description": "Absolute path to a text file.",
"default": "${workspaceFolder}/${command:AskForProgramName}"
},
"stopOnEntry": {
"type": "boolean",
"description": "Automatically stop after launch.",
"default": true
}
}
}
},
"initialConfigurations": [
{
"type": "mock",
"request": "launch",
"name": "Ask for file name",
"program": "${workspaceFolder}/${command:AskForProgramName}",
"stopOnEntry": true
}
],
"configurationSnippets": [
{
"label": "Mock Debug: Launch",
"description": "A new configuration for launching a mock debug program",
"body": {
"type": "mock",
"request": "launch",
"name": "${2:Launch Program}",
"program": "^\"\\${workspaceFolder}/${1:Program}\""
}
}
],
"variables": {
"AskForProgramName": "extension.mock-debug.getProgramName"
}
}
]
},
"activationEvents": ["onDebug", "onCommand:extension.mock-debug.getProgramName"]
}
现在,我们来看一下 contributes 部分,其中包含调试扩展特有的贡献。
首先,我们使用 breakpoints 贡献点来列出允许设置断点的语言。如果没有这个,将无法在 Markdown 文件中设置断点。
接下来是 debuggers 部分。在这里,一个调试器以调试 类型 (type) mock
引入。用户可以在启动配置中引用此类型。可选属性 label 可用于在 UI 中显示调试类型时为其提供一个友好的名称。
由于调试扩展使用调试适配器,因此其代码的相对路径作为 程序 (program) 属性给出。为了使扩展自包含,应用程序必须位于扩展文件夹内。按照惯例,我们将此应用程序保留在名为 out
或 bin
的文件夹中,但你可以自由使用不同的名称。
由于 VS Code 在不同的平台上运行,我们必须确保 DA 程序也支持不同的平台。为此,我们有以下选项:
-
如果程序以平台无关的方式实现,例如作为在所有支持平台上可用的运行时上运行的程序,你可以通过 运行时 (runtime) 属性指定此运行时。截至目前,VS Code 支持
node
和mono
运行时。我们上面介绍的 Mock 调试适配器就采用了这种方法。 -
如果你的 DA 实现需要在不同平台上使用不同的可执行文件,则 程序 (program) 属性可以针对特定平台进行限定,如下所示:
"debuggers": [{ "type": "gdb", "windows": { "program": "./bin/gdbDebug.exe", }, "osx": { "program": "./bin/gdbDebug.sh", }, "linux": { "program": "./bin/gdbDebug.sh", } }]
-
两种方法的组合也是可能的。以下示例来自 Mono DA,它被实现为一个 mono 应用程序,在 macOS 和 Linux 上需要运行时,但在 Windows 上不需要。
"debuggers": [{ "type": "mono", "program": "./bin/monoDebug.exe", "osx": { "runtime": "mono" }, "linux": { "runtime": "mono" } }]
configurationAttributes 声明了此调试器可用的 launch.json
属性的模式。此模式用于验证 launch.json
并在编辑启动配置时支持 IntelliSense 和悬停帮助。
initialConfigurations 定义了此调试器默认 launch.json
的初始内容。当项目没有 launch.json
且用户启动调试会话或在运行和调试视图中选择 创建 launch.json 文件 链接时,会使用此信息。在这种情况下,VS Code 会让用户选择一个调试环境,然后创建相应的 launch.json
。
除了在 package.json
中静态定义 launch.json
的初始内容外,还可以通过实现 DebugConfigurationProvider
来动态计算初始配置(详细信息请参阅下面的使用 DebugConfigurationProvider 部分)。
configurationSnippets 定义了在编辑 launch.json
时在 IntelliSense 中浮现的启动配置代码片段。按照惯例,片段的 label
属性应以调试环境名称作为前缀,以便在大量片段建议列表中呈现时能够清晰识别。
variables 贡献将“变量”绑定到“命令”。这些变量可以在启动配置中使用 ${command:xyz} 语法,并且在调试会话启动时,变量会被绑定命令返回的值替换。
命令的实现位于扩展中,它可以是简单的无 UI 表达式,也可以是基于扩展 API 中可用 UI 功能的复杂功能。Mock Debug 将变量 AskForProgramName
绑定到命令 extension.mock-debug.getProgramName
。此命令在 src/extension.ts
中的实现使用了 showInputBox
让用户输入程序名称。
vscode.commands.registerCommand('extension.mock-debug.getProgramName', config => {
return vscode.window.showInputBox({
placeHolder: 'Please enter the name of a markdown file in the workspace folder',
value: 'readme.md'
});
});
现在,该变量可以在启动配置的任何字符串类型值中作为 ${command:AskForProgramName} 使用。
使用 DebugConfigurationProvider
如果 package.json
中调试贡献的静态特性不足以满足需求,可以使用 DebugConfigurationProvider
动态控制调试扩展的以下方面:
- 新创建的 launch.json 的初始调试配置可以动态生成,例如,根据工作区中可用的某些上下文信息。
- 启动配置可以在用于启动新的调试会话之前被 解析(或修改)。这允许根据工作区中可用的信息填充默认值。存在两种 解析 方法:
resolveDebugConfiguration
在启动配置中的变量被替换之前调用,resolveDebugConfigurationWithSubstitutedVariables
在所有变量都被替换之后调用。如果验证逻辑在调试配置中插入额外的变量,则必须使用前者。如果验证逻辑需要访问所有调试配置属性的最终值,则必须使用后者。
src/extension.ts
中的 MockConfigurationProvider
实现了 resolveDebugConfiguration
,以检测在没有 launch.json 文件但活动编辑器中打开了 Markdown 文件时启动调试会话的情况。这是用户在编辑器中打开文件并希望调试它而无需创建 launch.json 的典型场景。
调试配置提供程序通过 vscode.debug.registerDebugConfigurationProvider
为特定调试类型注册,通常在扩展的 activate
函数中。为了确保 DebugConfigurationProvider
足够早地注册,扩展必须在使用调试功能时立即激活。这可以通过在 package.json
中为 onDebug
事件配置扩展激活来实现:
"activationEvents": [
"onDebug",
// ...
],
这个通用的 onDebug
会在任何调试功能被使用时立即触发。只要扩展的启动成本低廉(即在启动过程中不会花费大量时间),这种方法就有效。如果一个调试扩展的启动成本很高(例如,因为它启动了一个语言服务器),那么 onDebug
激活事件可能会对其他调试扩展产生负面影响,因为它触发得相当早,并且没有考虑到特定的调试类型。
对于启动成本高的调试扩展,更好的方法是使用更细粒度的激活事件:
onDebugInitialConfigurations
在调用DebugConfigurationProvider
的provideDebugConfigurations
方法之前触发。onDebugResolve:type
在调用指定类型的DebugConfigurationProvider
的resolveDebugConfiguration
或resolveDebugConfigurationWithSubstitutedVariables
方法之前触发。
经验法则:如果调试扩展的激活成本低廉,请使用 onDebug
。如果成本高昂,请根据 DebugConfigurationProvider
是否实现了相应的 provideDebugConfigurations
和/或 resolveDebugConfiguration
方法,使用 onDebugInitialConfigurations
和/或 onDebugResolve
。
发布你的调试器扩展
创建调试器扩展后,你可以将其发布到市场:
- 更新
package.json
中的属性,以反映你的调试器扩展的命名和目的。 - 按照发布扩展中的描述上传到市场。
开发调试器扩展的替代方法
正如我们所见,开发调试器扩展通常涉及在两个并行会话中调试扩展和调试适配器。如上所述,VS Code 很好地支持了这一点,但如果扩展和调试适配器是同一个程序,并且可以在一个调试会话中进行调试,那么开发可能会更容易。
只要你的调试适配器是用 TypeScript/JavaScript 实现的,这种方法实际上很容易实现。基本思想是直接在扩展内部运行调试适配器,并让 VS Code 连接到它,而不是每个会话启动一个新的外部调试适配器。
为此,VS Code 提供了扩展 API 来控制调试适配器的创建和运行方式。DebugAdapterDescriptorFactory
有一个方法 createDebugAdapterDescriptor
,当调试会话启动并需要调试适配器时,VS Code 会调用此方法。此方法必须返回一个描述对象(DebugAdapterDescriptor
),该对象描述调试适配器的运行方式。
目前,VS Code 支持三种不同的运行调试适配器的方式,因此提供了三种不同的描述符类型:
DebugAdapterExecutable
:此对象将调试适配器描述为一个外部可执行文件,具有路径、可选参数和运行时。该可执行文件必须实现调试适配器协议并通过 stdin/stdout 进行通信。这是 VS Code 的默认操作模式,如果未显式注册DebugAdapterDescriptorFactory
,VS Code 会自动使用 package.json 中的相应值来使用此描述符。DebugAdapterServer
:此对象将调试适配器描述为作为服务器运行,通过特定的本地或远程端口进行通信。基于vscode-debugadapter
npm 模块的调试适配器实现会自动支持此服务器模式。DebugAdapterInlineImplementation
:此对象将调试适配器描述为一个实现vscode.DebugAdapter
接口的 JavaScript 或 TypeScript 对象。基于vscode-debugadapter
npm 模块的 1.38-pre.4 或更高版本的调试适配器实现会自动实现该接口。
Mock Debug 展示了三种 DebugAdapterDescriptorFactories 的示例,以及它们如何为 'mock' 调试类型注册。要使用的运行模式可以通过将全局变量 runMode
设置为 external
、server
或 inline
中的一个来选择。
对于开发,inline
和 server
模式特别有用,因为它们允许在单个进程中调试扩展和调试适配器。