现已推出!阅读 10 月份的新功能和修复。

调试器扩展

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

我们将这种中介称为调试适配器(简称DA),用于 DA 和 VS Code 之间的抽象协议称为调试适配器协议(简称DAP)。由于调试适配器协议独立于 VS Code,因此它有自己的 网站,你可以在其中找到 简介和概述、详细的 规范,以及一些列出 已知实现和支持工具 的列表。DAP 的历史和背后的动机在本文中进行了说明 博客文章

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

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

因此,在最简化的形式中,调试器扩展只是对调试适配器实现的声明性贡献,而扩展基本上是调试适配器的包装容器,没有任何其他代码。

VS Code Debug Architecture 2

更现实的调试器扩展会向 VS Code 贡献许多或所有以下声明性项目

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

你可以在 contributes.breakpointscontributes.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 一词)。

Mock Debugger running

在将模拟调试用作你自己的开发的起点之前,我们建议先卸载预构建版本

  • 切换到扩展视图,然后单击模拟调试扩展的齿轮图标。
  • 运行“卸载”操作,然后“重新加载”窗口。

模拟调试的开发设置

现在让我们获取模拟调试的源代码,并在 VS Code 中开始对其进行开发

git clone https://github.com/microsoft/vscode-mock-debug.git
cd vscode-mock-debug
yarn

在 VS Code 中打开项目文件夹 vscode-mock-debug

包里有什么?

  • package.json 是模拟调试扩展的清单
    • 它列出了模拟调试扩展的贡献。
    • compilewatch 脚本用于将 TypeScript 源代码转译到 out 文件夹中,并监视后续的源代码修改。
    • 依赖项 vscode-debugprotocolvscode-debugadaptervscode-debugadapter-testsupport 是用于简化基于节点的调试适配器开发的 NPM 模块。
  • src/mockRuntime.ts 是一个具有简单调试 API 的模拟运行时。
  • 将运行时适配到调试适配器协议的代码位于 src/mockDebug.ts 中。在这里你将找到针对 DAP 各种请求的处理程序。
  • 由于调试器扩展的实现位于调试适配器中,因此根本不需要有扩展代码(即在扩展主机进程中运行的代码)。但是,模拟调试有一个小的 src/extension.ts,因为它说明了可以在调试器扩展的扩展代码中完成什么。

现在通过选择扩展启动配置并按 F5 来构建和启动模拟调试扩展。最初,这将对 TypeScript 源代码进行完全转译,并将它们转译到 out 文件夹中。在完全构建后,将启动一个监视器任务,该任务将转译你所做的任何更改。

在转译源代码后,将出现一个名为“[扩展开发主机]”的新 VS Code 窗口,其中模拟调试扩展现在以调试模式运行。从该窗口中打开你的 mock test 项目(包含 readme.md 文件),使用“F5”启动一个调试会话,然后单步执行它

Debugging Extension and Server

由于你正在以调试模式运行扩展,因此你现在可以在 src/extension.ts 中设置和命中断点,但如前所述,在扩展中没有太多有趣的代码在执行。有趣的代码在调试适配器中运行,它是一个独立的进程。

为了调试调试适配器本身,我们必须以调试模式运行它。这可以通过在服务器模式下运行调试适配器并配置 VS Code 连接到它来最轻松地实现。在 VS Code vscode-mock-debug 项目中,从下拉菜单中选择启动配置服务器,然后按绿色启动按钮。

由于我们已经为扩展启动了一个活动的调试会话,因此 VS Code 调试器 UI 现在进入多会话模式,这可以通过在“调用堆栈”视图中看到两个调试会话的名称扩展服务器来指示

Debugging Extension and Server

现在我们可以同时调试扩展和 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 程序也支持不同的平台。为此,我们有以下选择。

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

  2. 如果您的 DA 实现需要在不同的平台上使用不同的可执行文件,则可以为特定平台限定**程序**属性,如下所示。

    "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 功能的复杂功能。模拟调试将变量 `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` 模式特别有用,因为它们允许在单个进程中调试扩展和调试适配器。