调试器扩展

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 架构。VS Code 使用此架构验证 launch.json 编辑器中的配置并提供 IntelliSense。请注意,不支持 JSON 架构的 $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 应用商店安装预构建版本并体验一下:

  • 切换到“扩展”视图,输入“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 是简化基于 Node 的调试适配器开发的 NPM 模块。
  • src/mockRuntime.ts 是一个带有简单调试 API 的模拟运行时。
  • 将运行时适配到调试适配器协议的代码位于 src/mockDebug.ts 中。在这里,您可以找到针对各种 DAP 请求的处理程序。
  • 由于调试器扩展的实现位于调试适配器中,因此根本不需要扩展代码(即在扩展宿主进程中运行的代码)。然而,Mock Debug 有一个小的 src/extension.ts,因为它说明了在调试器扩展的扩展代码中可以做什么。

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

转译源代码后,会出现一个标有 "[Extension Development Host]" 的新 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 现在进入多会话模式,这体现在调用堆栈(CALL STACK)视图中显示了两个调试会话名称:ExtensionServer

Debugging Extension and Server

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

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

src/mockDebug.ts 文件中的 launchRequest(...) 方法开头设置一个断点,作为最后一步,通过将端口 4711debugServer 属性添加到您的 mock 测试启动配置中,来配置 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 中的模拟实现替换为与“真实”调试器或运行时对话的代码。这涉及理解和实现调试适配器协议。有关此内容的更多详细信息,请参见此处

调试器扩展的 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 部分。这里,一个调试器被引入,其调试 typemock。用户可以在启动配置中引用此类型。可选属性 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,它被实现为一个在 macOS 和 Linux 上需要运行时,但在 Windows 上不需要的 mono 应用程序。

    "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 的情况下对其进行调试。

调试配置提供程序通常在扩展的 activate 函数中通过 vscode.debug.registerDebugConfigurationProvider 为特定的调试类型注册。为了确保 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 模式特别有用,因为它们允许在单个进程中调试扩展和调试适配器。

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