调试器扩展

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

VS Code 附带一个内置调试器扩展,即Node.js调试器扩展,它很好地展示了 VS Code 支持的许多调试器功能:

VS Code 调试功能

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

  1. 调试配置管理。
  2. 启动/停止和单步调试操作。
  3. 源断点、函数断点、条件断点、内联断点和日志点。
  4. 堆栈跟踪,包括多线程和多进程支持。
  5. 在视图和悬停中浏览复杂的数据结构。
  6. 变量值显示在悬停中或内嵌在源中。
  7. 管理手表表达式。
  8. 用于具有自动完成功能的交互式评估的调试控制台。

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

VS Code 的调试架构

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

VS Code 调试架构

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

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

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

因此,在其最基本的形式中,调试器扩展只是调试适配器实现的声明性贡献,并且该扩展基本上是调试适配器的打包容器,无需任何附加代码。

VS Code 调试架构 2

更现实的调试器扩展向 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 动态生成默认调试配置。
  • 确定要动态使用的调试适配器。
  • 在将调试配置传递到调试适配器之前验证或修改它们。
  • 与调试适配器通信。
  • 将消息发送到调试控制台。

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

模拟调试扩展

由于从头开始创建调试适配器对于本教程来说有点繁重,因此我们将从一个简单的 DA 开始,我们已将其创建为教育性“调试适配器入门套件”。它被称为模拟调试,因为它不与真正的调试器对话,而是模拟调试器。Mock Debug 模拟一个调试器,支持单步、继续、断点、异常和变量访问,但它没有连接到任何真实的调试器。

在深入研究模拟调试的开发设置之前,我们首先从 VS Code Marketplace 安装一个预构建版本 并使用它:

  • 切换到扩展视图并输入“mock”以搜索模拟调试扩展,
  • “安装”和“重新加载”扩展。

尝试模拟调试:

  • 创建一个新的空文件夹mock test并在 VS Code 中打开它。
  • 创建一个文件readme.md并输入多行任意文本。
  • 切换到“运行和调试”视图 ( ⇧⌘D (Windows、Linux Ctrl+Shift+D ))并选择创建 launch.json 文件链接。
  • VS Code 将允许您选择“调试器”以创建默认启动配置。选择“模拟调试”。
  • 按绿色“开始”按钮,然后按 Enter 键确认建议的文件readme.md

调试会话启动,您可以“单步执行”自述文件,设置并命中断点,并遇到异常(如果该单词exception出现在一行中)。

模拟调试器运行

在使用 Mock Debug 作为您自己开发的起点之前,我们建议先卸载预构建的版本:

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

模拟调试的开发设置

现在让我们获取 Mock Debug 的源代码并在 VS Code 中开始对其进行开发:

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

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

包裹里有什么?

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

现在通过选择扩展启动配置并点击 来构建并启动模拟F5调试扩展。最初,这会将 TypeScript 源代码完全转译到out文件夹中。完整构建后,将启动一个观察器任务来转译您所做的任何更改。

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

调试扩展和服务器

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

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

由于我们已经为扩展程序建立了一个活动的调试会话,因此 VS Code 调试器 UI 现在进入多会话模式,这可以通过查看CALL STACK 视图中显示的两个调试会话扩展程序和服务器的名称来指示:

调试扩展和服务器

现在我们可以同时调试扩展和 DA。到达此处的更快方法是使用扩展 + 服务器启动配置,该配置会自动启动两个会话。

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

launchRequest(...)在文件中方法的开头设置断点src/mockDebug.ts,最后一步通过向模拟测试启动配置添加端口debugServer属性来配置模拟调试器以连接到 DA 服务器4711

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "mock",
      "request": "launch",
      "name": "mock test",
      "program": "${workspaceFolder}/readme.md",
      "stopOnEntry": true,
      "debugServer": 4711
    }
  ]
}

如果您现在启动此调试配置,VS Code 不会将模拟调试适配器作为单独的进程启动,而是直接连接到已运行服务器的本地端口 4711,并且您应该在launchRequest.

通过此设置,您现在可以轻松编辑、转换和调试 Mock Debug。

src/mockDebug.ts但现在真正的工作开始了:您必须用src/mockRuntime.ts一些与“真实”调试器或运行时对话的代码替换调试适配器的模拟实现。这涉及理解和实现调试适配器协议。有关此内容的更多详细信息,请参见此处

调试器扩展的 package.json 剖析

除了提供调试适配器的特定于调试器的实现之外,调试器扩展还需要package.json有助于各种与调试相关的贡献点。

那么让我们仔细看看package.jsonMock Debug 的原理。

与每个 VS Code 扩展一样,声明了扩展的package.json基本属性namepublisherversion 。使用类别字段可以更轻松地在 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 支持nodemono运行时。上面的模拟调试适配器就使用了这种方法。

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

    "debuggers": [{
        "type": "gdb",
        "windows": {
            "program": "./bin/gdbDebug.exe",
        },
        "osx": {
            "program": "./bin/gdbDebug.sh",
        },
        "linux": {
            "program": "./bin/gdbDebug.sh",
        }
    }]
    
  3. 两种方法的结合也是可能的。以下示例来自 Mono DA,它作为单声道应用程序实现,需要在 macOS 和 Linux 上运行,但不需要在 Windows 上运行:

    "debuggers": [{
        "type": "mono",
        "program": "./bin/monoDebug.exe",
        "osx": {
            "runtime": "mono"
        },
        "linux": {
            "runtime": "mono"
        }
    }]
    

ConfigurationAttributeslaunch.json声明可用于此调试器的属性的架构。此架构用于launch.json在编辑启动配置时验证并支持 IntelliSense 和悬停帮助。

初始配置定义了launch.json该调试器默认的初始内容。当项目没有 alaunch.json且用户启动调试会话或在“运行和调试”视图中选择创建 launch.json 文件链接时,将使用此信息。在这种情况下,VS Code 允许用户选择一个调试环境,然后创建相应的launch.json

调试器快速选择

可以通过实现 a来动态计算初始配置,而不是launch.json在 中静态定义 的初始内容(有关详细信息,请参阅下面的使用 DebugConfigurationProvider部分)。package.jsonDebugConfigurationProvider

配置片段定义启动配置片段,这些片段在编辑launch.json. 作为惯例,请label在代码片段的属性前加上调试环境名称前缀,以便在出现在许多代码片段提案的列表中时可以清楚地识别它。

变量贡献将变量”绑定到“命令”。这些变量可以使用${command:xyz}语法在启动配置中使用,并且在启动调试会话时,这些变量将被从绑定命令返回的值替换。

命令的实现位于扩展中,其范围可以从没有 UI 的简单表达式到基于扩展 API 中可用的 UI 功能的复杂功能。模拟调试将变量绑定AskForProgramName到命令extension.mock-debug.getProgramName。该命令的实现使用src/extension.tsshowInputBox用户输入程序名称:

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可以使用 a 来动态控制调试扩展的以下方面:

  • 新创建的 launch.json 的初始调试配置可以动态生成,例如基于工作区中可用的一些上下文信息。
  • 启动配置可以在用于启动新的调试会话之前解析(或修改)。这允许根据工作区中可用的信息填充默认值。存在两种解析方法:resolveDebugConfiguration在启动配置中替换变量之前调用,resolveDebugConfigurationWithSubstitutedVariables在替换所有变量之后调用。如果验证逻辑将其他变量插入调试配置中,则必须使用前者。如果验证逻辑需要访问所有调试配置属性的最终值,则必须使用后者。

in实现检测当不存在 launch.json 但在活动编辑器中打开 Markdown 文件时启动调试会话的情况MockConfigurationProvider。这是一个典型的场景,用户在编辑器中打开了一个文件,只想调试它而不创建 launch.json。src/extension.tsresolveDebugConfiguration

调试配置提供程序通过 注册用于特定调试类型vscode.debug.registerDebugConfigurationProvider,通常在扩展的activate函数中。为了确保DebugConfigurationProvider足够早地注册,必须在使用调试功能后立即激活扩展。onDebug这可以通过在以下位置为事件配置扩展激活来轻松实现package.json

"activationEvents": [
    "onDebug",
    // ...
],

onDebug一旦使用任何调试功能,就会触发此包罗万象的操作。只要扩展的启动成本低廉(即在启动顺序上不花费大量时间),这种方法就可以正常工作。如果调试扩展的启动成本很高(例如,由于启动语言服务器),则激活onDebug事件可能会对其他调试扩展产生负面影响,因为它触发得相当早,并且不考虑特定的调试类型。

对于昂贵的调试扩展来说,更好的方法是使用更细粒度的激活事件:

  • onDebugInitialConfigurationsprovideDebugConfigurations在调用的方法之前触发DebugConfigurationProvider
  • onDebugResolve:type在调用指定类型的resolveDebugConfigurationresolveDebugConfigurationWithSubstitutedVariables方法之前触发。DebugConfigurationProvider

经验法则:如果调试扩展的激活成本低廉,请使用onDebug. 如果昂贵,则使用onDebugInitialConfigurations和/或onDebugResolve取决于是否DebugConfigurationProvider实现相应的方法provideDebugConfigurations和/或resolveDebugConfiguration

发布您的调试器扩展

创建调试器扩展后,您可以将其发布到市场:

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

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

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

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

为此,VS Code 提供了扩展 API 来控制调试适配器的创建和运行方式。ADebugAdapterDescriptorFactory有一个方法createDebugAdapterDescriptor,当调试会话启动并且需要调试适配器时,VS Code 会调用该方法。此方法必须返回一个DebugAdapterDescriptor描述调试适配器如何运行的描述符对象 ( )。

如今,VS Code 支持三种不同的运行调试适配器的方式,因此提供了三种不同的描述符类型:

  • DebugAdapterExecutable:该对象将调试适配器描述为具有路径、可选参数和运行时的外部可执行文件。可执行文件必须实现调试适配器协议并通过 stdin/stdout 进行通信。这是 VS Code 的默认操作模式,如果未显式注册,VS Code 会自动使用此描述符以及 package.json 中的相应值DebugAdapterDescriptorFactory
  • DebugAdapterServer:此对象描述作为服务器运行的调试适配器,通过特定的本地或远程端口进行通信。基于vscode-debugadapternpm 模块的调试适配器实现自动支持此服务器模式。
  • DebugAdapterInlineImplementation:此对象将调试适配器描述为实现接口的 JavaScript 或 Typescript 对象vscode.DebugAdapter。基于版本 1.38-pre.4 或更高版本的vscode-debugadapternpm 模块的调试适配器实现会自动实现该接口。

模拟调试显示了三种类型的 DebugAdapterDescriptorFactories 的示例 以及如何将它们注册为“模拟”调试类型可以通过将全局变量设置runMode为可能值externalserver、 或之一来选择要使用的运行模式inline

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