在 VS Code 中尝试

调试器扩展

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,该 UI 基于我们引入的用于与调试器后端通信的抽象协议。由于调试器通常不实现此协议,因此需要某个中间层将调试器“适配”到该协议。此中间层通常是与调试器通信的独立进程。

VS Code Debug Architecture

我们将此中间层称为 调试适配器(简称 DA),DA 和 VS Code 之间使用的抽象协议是 调试适配器协议(简称 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 使用此 schema 在 launch.json 编辑器中验证配置并提供 IntelliSense。请注意,不支持 JSON schema 的 $refdefinition 构造。
  • VS Code 创建的初始 launch.json 的默认调试配置。
  • 用户可以添加到 launch.json 文件中的调试配置片段。
  • 可在调试配置中使用的变量声明。

你可以在 contributes.breakpointscontributes.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 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 扩展的贡献。
    • compilewatch 脚本用于将 TypeScript 源代码转译到 out 文件夹中,并监视后续的源文件修改。
    • 依赖项 vscode-debugprotocolvscode-debugadaptervscode-debugadapter-testsupport 是 NPM 模块,它们简化了基于 Node 的调试适配器的开发。
  • src/mockRuntime.ts 是一个带有简单调试 API 的 mock 运行时。
  • 将运行时 适配 到调试适配器协议的代码位于 src/mockDebug.ts 中。在这里,你可以找到 DAP 各种请求的处理程序。
  • 由于调试器扩展的实现位于调试适配器中,因此根本不需要有扩展代码(即在扩展主机进程中运行的代码)。然而,Mock Debug 有一个小的 src/extension.ts 文件,因为它展示了在调试器扩展的扩展代码中可以做什么。

现在,选择 扩展 启动配置并按 F5 构建并启动 Mock Debug 扩展。最初,这将把 TypeScript 源代码完全转译到 out 文件夹中。完整构建后,会启动一个 监视任务,用于转译你所做的任何更改。

源代码转译完成后,会出现一个新的 VS Code 窗口,标记为“[扩展开发主机]”,Mock Debug 扩展此时正在调试模式下运行。在该窗口中打开你的 mock test 项目及其 readme.md 文件,按 'F5' 启动调试会话,然后单步执行它。

Debugging Extension and Server

由于你正在调试模式下运行扩展,你现在可以在 src/extension.ts 中设置并命中断点,但正如我上面提到的,扩展中没有太多有趣的代码在执行。有趣的代码运行在调试适配器中,它是一个单独的进程。

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

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

Debugging Extension and Server

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

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

在文件 src/mockDebug.ts 中方法 launchRequest(...) 的开头设置一个断点,最后一步通过向你的 Mock Test 启动配置添加一个端口为 4711debugServer 属性来配置 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.tssrc/mockRuntime.ts 中的 Mock 调试适配器实现替换为与“真实”调试器或运行时对话的代码。这需要理解并实现调试适配器协议。有关此内容的更多详细信息可以在 这里 找到。

调试器扩展 package.json 结构剖析

除了提供调试器特定的调试适配器实现之外,调试器扩展还需要一个 package.json 文件,该文件会向各种与调试相关的贡献点做出贡献。

所以让我们仔细看看 Mock Debug 的 package.json 文件。

像所有 VS Code 扩展一样,package.json 声明了扩展的基本属性 namepublisherversion。使用 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 属性给出其代码的相对路径。为了使扩展自包含,应用程序必须位于扩展文件夹内。按照惯例,我们将此应用程序保留在名为 outbin 的文件夹内,但你可以自由使用不同的名称。

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

  1. 如果程序以平台无关的方式实现,例如作为一个可以在所有受支持平台上运行的运行时程序,则可以通过 runtime 属性指定此运行时。截至目前,VS Code 支持 nodemono 运行时。我们上面的 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 属性的 schema。此 schema 用于验证 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 激活事件可能会对其他调试扩展产生负面影响,因为它触发得比较早,并且不考虑特定的调试类型。

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

  • onDebugInitialConfigurations 在调用 DebugConfigurationProviderprovideDebugConfigurations 方法之前触发。
  • onDebugResolve:type 在调用指定类型的 DebugConfigurationProviderresolveDebugConfigurationresolveDebugConfigurationWithSubstitutedVariables 方法之前触发。

经验法则: 如果调试扩展的激活成本较低,使用 onDebug。如果成本较高,则根据 DebugConfigurationProvider 是否实现了相应的 provideDebugConfigurations 和/或 resolveDebugConfiguration 方法,使用 onDebugInitialConfigurations 和/或 onDebugResolve

发布你的调试器扩展

创建调试器扩展后,你可以将其发布到 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 对象的调试适配器。基于 1.38-pre.4 或更高版本的 vscode-debugadapter npm 模块实现的调试适配器会自动实现此接口。

Mock Debug 展示了 三种类型的 DebugAdapterDescriptorFactories 的示例,以及如何为 'mock' 调试类型 注册它们。可以通过将全局变量 runMode 设置为可能的值 externalserverinline 之一来选择要使用的运行模式。

对于开发而言,inlineserver 模式特别有用,因为它们允许在单个进程内调试扩展和调试适配器。