调试器扩展
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 的可扩展性架构基于扩展和贡献点。
因此,VS Code 提供了一个贡献点 debuggers
,可以在特定的调试类型(例如,Node.js 调试器的 node
)下贡献调试适配器。每当用户启动该类型的调试会话时,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 动态生成的默认调试配置。
- 动态确定要使用的调试适配器。
- 在调试配置传递到调试适配器之前对其进行验证或修改。
- 与调试适配器通信。
- 向调试控制台发送消息。
在本文档的其余部分,我们将展示如何开发调试器扩展。
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
是 NPM 模块,它们简化了基于节点的调试适配器的开发。
src/mockRuntime.ts
是一个 mock 运行时,具有简单的调试 API。- 将运行时 适配 到调试适配器协议的代码位于
src/mockDebug.ts
中。你可以在此处找到 DAP 的各种请求的处理程序。 - 由于调试器扩展的实现位于调试适配器中,因此根本不需要扩展代码(即在扩展主机进程中运行的代码)。但是,Mock Debug 有一个小的
src/extension.ts
,因为它说明了可以在调试器扩展的扩展代码中完成的操作。
现在,通过选择 Extension 启动配置并按 F5
来构建和启动 Mock Debug 扩展。最初,这将对 TypeScript 源代码进行完全转译到 out
文件夹中。在完全构建之后,将启动一个监视器任务,该任务将转译你所做的任何更改。
转译源代码后,将出现一个新的 VS Code 窗口,标记为“[Extension Development Host]”,其中 Mock Debug 扩展现在以调试模式运行。从该窗口中,打开你的 mock test
项目以及 readme.md
文件,使用“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
属性添加到你的 mock test 启动配置中,将 mock 调试器配置为连接到 DA 服务器
{
"version": "0.2.0",
"configurations": [
{
"type": "mock",
"request": "launch",
"name": "mock test",
"program": "${workspaceFolder}/readme.md",
"stopOnEntry": true,
"debugServer": 4711
}
]
}
如果你现在启动此调试配置,VS Code 不会启动 mock 调试适配器作为单独的进程,而是直接连接到已运行服务器的本地端口 4711,你应该会在 launchRequest
中命中该断点。
通过此设置,你现在可以轻松地编辑、转译和调试 Mock Debug。
但现在真正的工作开始了:你将不得不使用一些与“真实”调试器或运行时对话的代码来替换 src/mockDebug.ts
和 src/mockRuntime.ts
中调试适配器的 mock 实现。这涉及到理解和实现调试适配器协议。有关这方面的更多详细信息,请参见 此处。
调试器扩展的 package.json 剖析
除了提供调试适配器的特定于调试器的实现之外,调试器扩展还需要一个 package.json
,该文件贡献于各种与调试相关的贡献点。
因此,让我们更仔细地看一下 Mock Debug 的 package.json
。
与每个 VS Code 扩展一样,package.json
声明了扩展的基本属性名称、发布者和版本。使用 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 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
。如果成本很高,请使用 onDebugInitialConfigurations
和/或 onDebugResolve
,具体取决于 DebugConfigurationProvider
是否实现了相应的方法 provideDebugConfigurations
和/或 resolveDebugConfiguration
。
发布你的调试器扩展
创建调试器扩展后,你可以将其发布到 Marketplace
- 更新
package.json
中的属性以反映调试器扩展的命名和用途。 - 按照 发布扩展 中所述上传到 Marketplace。
开发调试器扩展的替代方法
正如我们所看到的,开发调试器扩展通常涉及在两个并行会话中调试扩展和调试适配器。如上所述,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
模式特别有用,因为它们允许在单个进程中调试扩展和调试适配器。