现已发布!阅读关于 12 月份的新功能和修复。

调试器扩展

Visual Studio Code 的调试架构允许扩展作者轻松地将现有调试器集成到 VS Code 中,同时与所有调试器拥有通用的用户界面。

VS Code 自带一个内置调试器扩展,即 Node.js 调试器扩展,它完美展示了 VS Code 支持的众多调试器功能。

VS Code Debug Features

此屏幕截图显示了以下调试功能:

  1. 调试配置管理。
  2. 用于启动/停止和单步执行的调试操作。
  3. 源代码、函数、条件、内联断点以及日志点。
  4. 堆栈跟踪,包括多线程和多进程支持。
  5. 在视图和悬停提示中导航复杂数据结构。
  6. 变量值显示在悬停提示中或内联在源代码中。
  7. 管理监视表达式。
  8. 用于交互式求值的调试控制台,支持自动补全。

本文档将帮助您创建调试器扩展,使任何调试器都能与 VS Code 一起使用。

VS Code 的调试架构

VS Code 基于我们引入的用于与调试器后端通信的抽象协议,实现了通用的(与语言无关的)调试器 UI。由于调试器通常不实现此协议,因此需要一个中间件来将调试器“适配”到该协议。此中间件通常是一个与调试器通信的独立进程。

VS Code Debug Architecture

我们将此中间件称为 **调试适配器 (Debug Adapter)**(或简称 **DA**),在 DA 和 VS Code 之间使用的抽象协议称为 **调试适配器协议 (Debug Adapter Protocol)**(简称 **DAP**)。由于调试适配器协议独立于 VS Code,因此它有自己的 网站,您可以在其中找到 介绍和概述、详细的 规范,以及一些 已知实现和支持工具 列表。DAP 的历史和动机在此 博客文章 中得到了解释。

由于调试适配器独立于 VS Code,并且可以用于 其他开发工具,因此它们不匹配 VS Code 基于扩展和贡献点的可扩展性架构。

因此,VS Code 提供了一个名为 `debuggers` 的贡献点,可以在特定调试器类型下(例如,Node.js 调试器的 `node`)贡献一个调试适配器。每当用户启动该类型的调试会话时,VS Code 都会启动注册的 DA。

所以,在最简化的形式下,调试器扩展仅仅是对调试适配器实现的声明式贡献,而扩展本身就是一个打包容器,不包含任何额外代码。

VS Code Debug Architecture 2

更实际的调试器扩展会向 VS Code 贡献以下许多或全部声明式项目:

  • 调试器支持的语言列表。VS Code 会为这些语言启用设置断点的 UI。
  • 调试器引入的调试配置属性的 JSON Schema。VS Code 使用此模式来验证 `launch.json` 编辑器中的配置并提供 IntelliSense。请注意,不支持 JSON Schema 的 `$ref` 和 `definition` 构造。
  • 用于 VS Code 创建的初始 `launch.json` 的默认调试配置。
  • 用户可以添加到 `launch.json` 文件的调试配置片段。
  • 声明可在调试配置中使用的变量。

您可以在 contributes.breakpointscontributes.debuggers 参考中找到更多信息。

除了上述纯声明式贡献外,调试器扩展 API 还支持以下基于代码的功能:

  • 为 VS Code 创建的初始 `launch.json` 动态生成默认调试配置。
  • 动态确定要使用的调试适配器。
  • 在将调试配置传递给调试适配器之前对其进行验证或修改。
  • 与调试适配器通信。
  • 向调试控制台发送消息。

在本文档的其余部分,我们将展示如何开发调试器扩展。

Mock Debug 扩展

由于从头开始创建调试适配器对于本教程来说有些繁琐,我们将从一个简单的 DA 开始,这个 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 会让您选择一个“调试器”来创建默认的 `launch.json` 配置。选择“Mock Debug”。
  • 按绿色的 **开始** 按钮,然后按 Enter 确认建议的文件 `readme.md`。

调试会话开始,您可以逐行“步进”`readme` 文件,设置和命中断点,并触发异常(如果某一行包含 `exception` 单词)。

Mock Debugger running

在使用 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 模块,可以简化基于 Node 的调试适配器的开发。
  • src/mockRuntime.ts 是一个具有简单调试 API 的 **模拟** 运行时。
  • 将运行时适配到调试适配器协议的代码位于 `src/mockDebug.ts`。在这里,您可以找到 DAP 各项请求的处理程序。
  • 由于调试器扩展的实现存在于调试适配器中,因此不需要任何扩展代码(即在扩展主机进程中运行的代码)。然而,Mock Debug 有一个小的 `src/extension.ts`,因为它说明了调试器扩展的扩展代码中可以做什么。

现在,通过选择 **Extension** 启动配置并按 `F5` 来构建并启动 Mock Debug 扩展。最初,这将在 `out` 文件夹中对 TypeScript 源代码进行完全转译。完全构建后,将启动一个 *监视任务*,该任务将转译您所做的任何更改。

转译源代码后,会出现一个新的、标记为“[Extension Development Host]”的 VS Code 窗口,其中 Mock Debug 扩展正在调试模式下运行。从该窗口中,使用 `readme.md` 文件打开您的 `mock test` 项目,使用 'F5' 启动调试会话,然后逐行进行调试。

Debugging Extension and Server

由于您正在以调试模式运行扩展,因此现在可以在 `src/extension.ts` 中设置和命中断点,但正如我上面提到的,扩展中执行的有趣代码并不多。有趣的实际代码运行在单独的进程——调试适配器中。

为了调试调试适配器本身,我们必须以调试模式运行它。最简单的方法是让调试适配器以 *服务器模式* 运行,并配置 VS Code 连接到它。在您的 VS Code `vscode-mock-debug` 项目中,从下拉菜单中选择 **Server** 启动配置,然后按绿色的开始按钮。

由于我们已经有一个活动的扩展调试会话,VS Code 调试器 UI 现在进入 *多会话* 模式,这可以通过在 CALL STACK 视图中看到 **Extension** 和 **Server** 这两个调试会话的名称来指示。

Debugging Extension and Server

现在我们可以同时调试扩展和 DA。更快地到达这里的方法是使用 **Extension + Server** 启动配置,它会自动启动这两个会话。

另一种更简单的方法来调试扩展和 DA 可以在 下方 找到。

在 `src/mockDebug.ts` 文件中 `launchRequest(...)` 方法的开头设置一个断点,最后一步是通过在 `mock test` 启动配置中为端口 `4711` 添加一个 `debugServer` 属性来配置 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` 中的模拟实现。这需要理解并实现调试适配器协议。有关更多详细信息,请参阅 此处

调试器扩展 `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** 部分。在这里,一个调试器在调试类型 `mock` 下被引入。用户可以在启动配置中引用此类型。可选属性 **label** 可用于在 UI 中显示调试类型时赋予其一个易于理解的名称。

由于调试扩展使用调试适配器,因此其代码的相对路径作为 **program** 属性给出。为了使扩展自包含,应用程序必须位于扩展文件夹内。按照惯例,我们将此应用程序保留在名为 `out` 或 `bin` 的文件夹中,但您可以自由使用其他名称。

由于 VS Code 在不同平台上运行,我们必须确保 DA 程序也能支持不同的平台。为此,我们有以下选项:

  1. 如果程序是以平台无关的方式实现的,例如作为在所有支持的平台上都可用的运行时上运行的程序,则可以通过 **runtime** 属性指定此运行时。目前,VS Code 支持 `node` 和 `mono` 运行时。我们上面提到的 Mock 调试适配器就是采用了这种方法。

  2. 如果您的 DA 实现需要在不同平台上使用不同的可执行文件,则 `program` 属性可以针对特定平台进行限定,如下所示:

    "debuggers": [{
        "type": "gdb",
        "windows": {
            "program": "./bin/gdbDebug.exe",
        },
        "osx": {
            "program": "./bin/gdbDebug.sh",
        },
        "linux": {
            "program": "./bin/gdbDebug.sh",
        }
    }]
    
  3. 也可以结合使用这两种方法。下面的示例来自 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`。

Debugger Quickpick

与在 `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` 激活事件可能会对其他调试扩展产生负面影响,因为它触发得相当早,并且不考虑特定的调试类型。

对于启动成本高的调试扩展,更好的方法是使用更细粒度的激活事件:

  • 在调用 `DebugConfigurationProvider` 的 `provideDebugConfigurations` 方法之前,会触发 `onDebugInitialConfigurations`。
  • 在调用指定类型的 `DebugConfigurationProvider` 的 `resolveDebugConfiguration` 或 `resolveDebugConfigurationWithSubstitutedVariables` 方法之前,会触发 `onDebugResolve:type`。

**经验法则:** 如果调试扩展的激活成本很低,请使用 `onDebug`。如果成本很高,请使用 `onDebugInitialConfigurations` 和/或 `onDebugResolve`,具体取决于 `DebugConfigurationProvider` 是否实现了相应的 `provideDebugConfigurations` 和/或 `resolveDebugConfiguration` 方法。

发布您的调试器扩展

创建调试器扩展后,您可以将其发布到 Marketplace。

  • 更新 `package.json` 中的属性,以反映您的调试器扩展的命名和目的。
  • 按照 发布扩展 中的说明上传到 Marketplace。

开发调试器扩展的替代方法

如我们所见,开发调试器扩展通常涉及在两个并行会话中调试扩展和调试适配器。如上所述,VS Code 很好地支持这一点,但如果扩展和调试适配器是同一个程序,可以在一个调试会话中调试,开发可能会更容易。

只要您的调试适配器是用 TypeScript/JavaScript 实现的,这种方法实际上很容易实现。基本思想是直接在扩展内部运行调试适配器,并让 VS Code 连接到它,而不是为每个会话启动一个新的外部调试适配器。

为此,VS Code 提供了扩展 API 来控制调试适配器的创建和运行方式。当启动调试会话并需要调试适配器时,`DebugAdapterDescriptorFactory` 会调用 `createDebugAdapterDescriptor` 方法。此方法必须返回一个描述器对象(`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` 模式尤其有用,因为它们允许在单个进程中调试扩展和调试适配器。

© . This site is unofficial and not affiliated with Microsoft.