调试器扩展
Visual Studio Code 的调试架构允许扩展作者轻松地将现有调试器集成到 VS Code 中,同时对所有调试器使用通用的用户界面。
VS Code 附带一个内置的调试器扩展,即 Node.js 调试器扩展,它很好地展示了 VS Code 支持的众多调试器功能。
此屏幕截图显示了以下调试功能
- 调试配置管理。
- 用于启动/停止和单步执行的调试操作。
- 源代码、函数、条件、内联断点和日志点。
- 堆栈跟踪,包括多线程和多进程支持。
- 在视图和悬停中浏览复杂的数据结构。
- 在悬停中显示或在源代码中内联显示变量值。
- 管理观察表达式。
- 调试控制台,用于使用自动完成进行交互式评估。
本文档将帮助你创建一个调试器扩展,使任何调试器都能与 VS Code 配合使用。
VS Code 的调试架构
VS Code 基于我们引入的抽象协议来实现通用的(与语言无关的)调试器 UI,用于与调试器后端通信。由于调试器通常不实现此协议,因此需要某种中介来“适配”调试器以适应协议。这种中介通常是一个独立的进程,与调试器通信。
我们将这种中介称为调试适配器(简称DA),用于 DA 和 VS Code 之间的抽象协议称为调试适配器协议(简称DAP)。由于调试适配器协议独立于 VS Code,因此它有自己的 网站,你可以在其中找到 简介和概述、详细的 规范,以及一些列出 已知实现和支持工具 的列表。DAP 的历史和背后的动机在本文中进行了说明 博客文章。
由于调试适配器独立于 VS Code,并且可以在 其他开发工具 中使用,因此它们不符合 VS Code 的扩展性架构,该架构基于扩展和贡献点。
为此,VS Code 提供了一个贡献点,debuggers
,可以在其中以特定调试类型(例如 node
用于 Node.js 调试器)贡献调试适配器。当用户启动该类型的调试会话时,VS Code 将启动已注册的 DA。
因此,在最简化的形式中,调试器扩展只是对调试适配器实现的声明性贡献,而扩展基本上是调试适配器的包装容器,没有任何其他代码。
更现实的调试器扩展会向 VS Code 贡献许多或所有以下声明性项目
- 调试器支持的语言列表。VS Code 使 UI 能够为这些语言设置断点。
- 调试器引入的调试配置属性的 JSON 架构。VS Code 使用此架构来验证 launch.json 编辑器中的配置,并提供 IntelliSense。请注意,JSON 架构构造
$ref
和definition
不受支持。 - VS Code 创建的初始 launch.json 的默认调试配置。
- 用户可以添加到 launch.json 文件的调试配置代码片段。
- 声明可以在调试配置中使用的变量。
你可以在 contributes.breakpoints
和 contributes.debuggers
参考中找到更多信息。
除了上面提到的纯粹的声明性贡献之外,调试扩展 API 还支持此基于代码的功能
- VS Code 创建的初始 launch.json 的动态生成的默认调试配置。
- 动态地确定要使用的调试适配器。
- 在将调试配置传递给调试适配器之前,验证或修改调试配置。
- 与调试适配器通信。
- 向调试控制台发送消息。
在本文档的剩余部分,我们将展示如何开发调试器扩展。
模拟调试扩展
由于从头开始创建调试适配器对于本教程来说有点复杂,因此我们将从一个简单的 DA 开始,我们将其作为教育性的“调试适配器入门工具包”创建。它被称为模拟调试,因为它不与真实调试器通信,而是模拟一个。模拟调试模拟了一个调试器,并支持单步执行、继续、断点、异常和变量访问,但它没有连接到任何真实的调试器。
在深入研究模拟调试的开发设置之前,让我们先从 VS Code Marketplace 安装一个 预构建版本,并试用它
- 切换到扩展视图,然后键入“mock”以搜索模拟调试扩展,
- “安装”并“重新加载”扩展。
要试用模拟调试,
- 创建一个新的空文件夹
mock test
并将其在 VS Code 中打开。 - 创建一个文件
readme.md
并输入几行任意文本。 - 切换到运行和调试视图(⇧⌘D(Windows、Linux Ctrl+Shift+D))并选择创建 launch.json 文件链接。
- VS Code 将让你选择一个“调试器”,以便创建一个默认的启动配置。选择“模拟调试”。
- 按绿色启动按钮,然后按 Enter 确认建议的文件
readme.md
。
调试会话将启动,你可以“单步执行”readme 文件,设置和命中断点,以及遇到异常(如果该行中出现 exception
一词)。
在将模拟调试用作你自己的开发的起点之前,我们建议先卸载预构建版本
- 切换到扩展视图,然后单击模拟调试扩展的齿轮图标。
- 运行“卸载”操作,然后“重新加载”窗口。
模拟调试的开发设置
现在让我们获取模拟调试的源代码,并在 VS Code 中开始对其进行开发
git clone https://github.com/microsoft/vscode-mock-debug.git
cd vscode-mock-debug
yarn
在 VS Code 中打开项目文件夹 vscode-mock-debug
。
包里有什么?
package.json
是模拟调试扩展的清单- 它列出了模拟调试扩展的贡献。
compile
和watch
脚本用于将 TypeScript 源代码转译到out
文件夹中,并监视后续的源代码修改。- 依赖项
vscode-debugprotocol
、vscode-debugadapter
和vscode-debugadapter-testsupport
是用于简化基于节点的调试适配器开发的 NPM 模块。
src/mockRuntime.ts
是一个具有简单调试 API 的模拟运行时。- 将运行时适配到调试适配器协议的代码位于
src/mockDebug.ts
中。在这里你将找到针对 DAP 各种请求的处理程序。 - 由于调试器扩展的实现位于调试适配器中,因此根本不需要有扩展代码(即在扩展主机进程中运行的代码)。但是,模拟调试有一个小的
src/extension.ts
,因为它说明了可以在调试器扩展的扩展代码中完成什么。
现在通过选择扩展启动配置并按 F5
来构建和启动模拟调试扩展。最初,这将对 TypeScript 源代码进行完全转译,并将它们转译到 out
文件夹中。在完全构建后,将启动一个监视器任务,该任务将转译你所做的任何更改。
在转译源代码后,将出现一个名为“[扩展开发主机]”的新 VS Code 窗口,其中模拟调试扩展现在以调试模式运行。从该窗口中打开你的 mock test
项目(包含 readme.md
文件),使用“F5”启动一个调试会话,然后单步执行它
由于你正在以调试模式运行扩展,因此你现在可以在 src/extension.ts
中设置和命中断点,但如前所述,在扩展中没有太多有趣的代码在执行。有趣的代码在调试适配器中运行,它是一个独立的进程。
为了调试调试适配器本身,我们必须以调试模式运行它。这可以通过在服务器模式下运行调试适配器并配置 VS Code 连接到它来最轻松地实现。在 VS Code vscode-mock-debug 项目中,从下拉菜单中选择启动配置服务器,然后按绿色启动按钮。
由于我们已经为扩展启动了一个活动的调试会话,因此 VS Code 调试器 UI 现在进入多会话模式,这可以通过在“调用堆栈”视图中看到两个调试会话的名称扩展和服务器来指示
现在我们可以同时调试扩展和 DA。使用**扩展 + 服务器**启动配置可以更快地到达这里,该配置会自动启动这两个会话。
您可以在下面找到一种调试扩展和 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` 中命中断点。
使用此设置,您现在可以轻松地编辑、转译和调试模拟调试。
但现在真正的工作开始了:您需要用一些与“真实”调试器或运行时通信的代码替换 `src/mockDebug.ts` 和 `src/mockRuntime.ts` 中调试适配器的模拟实现。这涉及了解和实现调试适配器协议。您可以在此处找到有关此方面的更多详细信息。
调试器扩展的 package.json 解剖
除了提供调试适配器的调试器特定实现之外,调试器扩展还需要一个 `package.json`,该文件有助于各种与调试相关的贡献点。
因此,让我们仔细看看模拟调试的 `package.json`。
与每个 VS Code 扩展一样,`package.json` 声明扩展的**名称**、**发布者**和**版本**等基本属性。使用**类别**字段使扩展更容易在 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"]
}
现在看一下**贡献**部分,其中包含特定于调试扩展的贡献。
首先,我们使用**断点**贡献点列出将启用设置断点的语言。如果没有它,就不可能在 Markdown 文件中设置断点。
接下来是**调试器**部分。在这里,在调试**类型** `mock` 下引入了一个调试器。用户可以在启动配置中引用此类型。可选属性**标签**可用于在 UI 中显示调试类型时为其提供一个好听的名字。
由于调试扩展使用调试适配器,因此作为**程序**属性,会提供一个指向其代码的相对路径。为了使扩展自包含,应用程序必须位于扩展文件夹中。按照惯例,我们将这些应用程序保存在名为 `out` 或 `bin` 的文件夹中,但您可以自由使用其他名称。
由于 VS Code 运行在不同的平台上,因此我们必须确保 DA 程序也支持不同的平台。为此,我们有以下选择。
-
如果程序以平台无关的方式实现,例如作为在所有支持的平台上可用的运行时上运行的程序,则可以通过**运行时**属性指定此运行时。截至目前,VS Code 支持 `node` 和 `mono` 运行时。我们上面提到的模拟调试适配器使用这种方法。
-
如果您的 DA 实现需要在不同的平台上使用不同的可执行文件,则可以为特定平台限定**程序**属性,如下所示。
"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 功能的复杂功能。模拟调试将变量 `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 或更高版本的调试适配器实现自动实现了该接口。
模拟调试显示了三种类型的 DebugAdapterDescriptorFactories 的示例以及它们是如何为 'mock' 调试类型注册的。要使用的运行模式可以通过将全局变量 `runMode` 设置为以下可能值之一来选择:`external`、`server` 或 `inline`。
在开发中,`inline` 和 `server` 模式特别有用,因为它们允许在单个进程中调试扩展和调试适配器。