调试器扩展

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 实现了一个通用的(与语言无关的)调试器用户界面,基于我们引入的抽象协议与调试器后端进行通信。 由于调试器通常不实现此协议,因此需要一些中介来“适配”调试器以符合该协议。 这个中介通常是一个独立的进程,与调试器进行通信。

VS Code 调试架构

我们称这个中介为调试适配器(简称DA),而DA与VS Code之间使用的抽象协议是调试适配器协议(简称DAP)。 由于调试适配器协议独立于VS Code,它有自己的网站,在那里你可以找到介绍和概述,详细的规范,以及一些已知的实现和支持工具的列表。 DAP的历史和动机在这个博客文章中有所解释。

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

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

因此,在最简单的形式中,调试器扩展只是一个调试适配器实现的声明性贡献,而扩展基本上是调试适配器的打包容器,没有任何额外的代码。

VS Code 调试架构 2

一个更现实的调试器扩展为VS Code贡献了许多或所有以下声明性项目:

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

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

除了上述纯粹的声明性贡献外,Debug Extension API 还支持以下基于代码的功能:

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

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

模拟调试扩展

由于从头创建一个调试适配器对于本教程来说有点复杂,我们将从一个简单的调试适配器(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

调试会话开始,您可以“逐步”通过自述文件,设置并命中断点,并遇到异常(如果行中出现exception这个词)。

模拟调试器运行中

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

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

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

调试扩展和服务器

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

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

由于我们已经为扩展程序激活了一个调试会话,VS Code 调试器 UI 现在进入了多会话模式,这可以通过在 CALL STACK 视图中看到两个调试会话的名称ExtensionServer来表明:

调试扩展和服务器

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

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

在文件 src/mockDebug.ts 中的方法 launchRequest(...) 的开头设置一个断点,并最后一步通过在你的模拟测试启动配置中添加一个端口为 4711debugServer 属性来配置模拟调试器连接到 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 中遇到断点。

通过此设置,您现在可以轻松编辑、转译和调试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"]
}

现在看一下贡献部分,其中包含特定于调试扩展的贡献。

首先,我们使用断点贡献点来列出将启用断点设置的语言。如果没有这个,就无法在Markdown文件中设置断点。

接下来是调试器部分。这里,在调试类型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,它被实现为一个在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

调试器快速选择

与其在package.json中静态定义launch.json的初始内容,不如通过实现DebugConfigurationProvider来动态计算初始配置(详情请参见下面的使用DebugConfigurationProvider部分)。

configurationSnippets 定义了在编辑 launch.json 时在 IntelliSense 中显示的启动配置片段。按照惯例,片段的 label 属性应加上调试环境名称的前缀,以便在众多片段建议列表中能够清晰识别。

变量贡献将“变量”绑定到“命令”。这些变量可以在启动配置中使用${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

发布你的调试器扩展

一旦你创建了你的调试器扩展,你可以将其发布到市场:

  • 更新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或更高版本的调试适配器实现会自动实现该接口。

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

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