本站点文档内容均翻译自code.visualstudio.com,仅供个人学习,如有差异请以官网为准。

调试器扩展

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 提供了一个贡献点,调试器,调试适配器可以在特定的调试类型(例如)下贡献。节点用于 Node.js 调试器)。VS Code 在用户启动该类型的调试会话时启动注册的 DA。

因此,调试器扩展的最简单形式就是一个声明性贡献的调试适配器实现,扩展基本上是一个不包含任何附加代码的调试适配器包装容器。

VS Code 调试架构 2

一个更现实的调试器扩展向 VS Code 贡献了以下声明项中的一个或多个:

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

您可以在 中找到更多信息贡献了 breakpoints贡献者.调试器参考文献。

除了上面的纯粹声明性贡献外,调试扩展 API 使这种基于代码的功能成为可能:

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

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

模拟调试扩展

由于从头开始创建调试适配器对这个教程来说有点复杂,我们将从一个简单的适配器开始,这个适配器是我们作为教育用途的“调试适配器入门套件”创建的。它被称为模拟调试,因为它不与真正的调试器通信,而是模拟一个调试器。模拟调试模拟了一个调试器并支持单步、继续、断点、异常和变量访问,但它连接到任何真正的调试器。

在深入研究模拟调试的开发设置之前,让我们首先从VS Code市场安装一个预构建版本 并进行尝试:

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

尝试模拟调试:

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

调试会话开始,您可以“逐步执行”读取文件,设置和到达断点,并遇到异常(如果遇到的单词异常出现在一行中。

模拟调试器正在运行

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

  • 切换到扩展视图并点击 Mock Debug 扩展的齿轮图标。
  • 运行“卸载”操作,然后“重新加载”Windows。

模拟调试开发环境设置

现在获取 Mock Debug 的源代码,并在 VS Code 中开始开发:

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

打开项目文件夹vscode-模拟调试在 VS Code 中。

包裹里有什么?

  • package.json是模拟调试扩展的清单:
    • 它列出了模拟调试扩展的贡献。
    • 编译手表脚本用于将 TypeScript 源代码编译成输出文件夹并监视后续的源代码修改。
    • 依赖关系vscode调试协议vscode调试适配器,和vscode调试器测试支持是简化基于节点的调试适配器开发的NPM模块。
  • src/mockRuntime.ts 是一个 模拟运行时,带有简单的调试API。
  • 用于适配运行时与调试适配器协议的代码位于src/mockDebug.ts在这里,您可以找到DAP的各种请求的处理程序。
  • 由于调试器扩展实现于调试适配器中,因此不需要扩展代码(即在扩展主机进程运行的代码)。然而,Mock Debug 有一个小src/extension.ts因为它展示了调试器扩展的扩展代码中可以做什么。

现在通过选择扩展启动配置并按F5最初,这将完全编译TypeScript源代码到输出 文件夹。 完成构建后,会启动一个 监视任务,将您所做的任何更改编译成目标语言。

在编译源代码后,一个标有“[扩展开发主机]”的新 VS Code Windows出现,Mock Debug 扩展现在调试模式中运行。从该Windows打开你的模拟测试项目与读取我.md文件,按‘F5’开始调试会话,然后逐步执行:

调试扩展和服务器

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

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

由于我们已经有一个扩展的活动调试会话,VS Code 调试器用户界面现在进入多会话模式,这可以通过在调用栈视图中看到两个调试会话的名字来指示扩展服务器

调试扩展和服务器

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

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

在方法的开头设置断点启动请求(...)在文件中src/mockDebug.ts最后一步是通过添加来配置模拟调试器以连接到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,你应该在 启动请求输入:.

通过这种设置,您现在可以轻松地编辑、编译和调试 Mock Debug。

但是现在真正的准备工作开始了:你将不得不替换调试适配器的模拟实现src/mockDebug.tssrc/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文件中设置断点。

接下来是调试器部分。在这里,每种调试类型下介绍了一种调试器类型 模拟用户可以在启动配置中引用此类型。可选的属性label可以在用户界面中显示调试类型时为其提供一个不错的名字。

由于调试扩展使用调试适配器,其代码的相对路径作为程序属性提供。 为了使扩展自包含,应用程序必须位于扩展文件夹内。按照约定,我们将此应用程序保留在名为的文件夹内。输出,但您可以使用不同的名字。

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

  1. 如果程序以跨平台的方式实现,例如作为一个在所有支持平台上都可用的运行时上运行的程序,你可以通过runtime属性指定这个运行时。截至今天,VS Code 支持节点单声道运行时。我们上面的模拟调试适配器使用这种方法。

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

    "调试器": [{
        "类型": "gdb",
        "Windows": {
            "程序": "./bin/gdbDebug.exe",
        },
        "OSX": {
            "程序": "./bin/gdbDebug.sh",
        },
        "Linux": {
            "程序": "./bin/gdbDebug.sh",
        }
    }]
    
  3. 两种方法的结合也是可能的。以下示例来自 Mono DA,它是一个作为单例应用程序实现的项目,在 macOS 和 Linux 上需要运行时,但在 Windows 上不需要:

    "调试器": [{
        "类型": "mono",
        "程序": "./bin/monoDebug.exe",
        "osx": {
            "运行时": "mono"
        },
        "linux": {
            "运行时": "mono"
        }
    }]
    

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

初始配置 定义了默认内容的初始内容launch.json对于这个调试器。此信息在项目没有时使用launch.json 并且用户开始一个调试会话或选择 创建 launch.json 文件 链接在运行和调试视图中。在这种情况下,VS Code 让用户选择一个调试环境,然后创建相应的 launch.json输入:

调试器快速选择

与其定义初始内容launch.json静态地在package.json通过实现一个,可以动态地计算初始配置。调试配置提供程序 (详情请参见下面的 使用 DebugConfigurationProvider 部分)。

配置片段 定义在编辑时显示在IntelliSense中的启动配置片段launch.json. 作为惯例,前缀Tab通过调试环境名称为片段分配属性,以便在多个片段建议列表中清晰识别。

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

命令的实现位于扩展中,它可以是一个没有用户界面的简单表达式,也可以是基于扩展 API 中可用的用户界面功能的复杂功能。 Mock Debug 绑定一个变量请求程序名称到命令扩展.mock-debug.getProgramName这个命令的实现src/extension.ts使用显示输入框让用户输入程序名称:

vscode.commands.registerCommand('extension.mock-debug.getProgramName', config => {
  return vscode.window.showInputBox({
    placeHolder: '请输入工作区文件夹中的markdown文件名',
    value: 'readme.md'
  });
});

该变量现在可以在启动配置中的任何字符串输入值中使用为${command:AskForProgramName}

使用调试配置提供程序

如果调试贡献的静态性质在package.json是不够的,一个调试配置提供程序可以动态控制调试扩展的以下方面:

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

模拟配置提供者src/extension.ts实施解析调试配置检测在没有 launch.json 的情况下启动调试会话,但活动编辑器中打开了一个 Markdown 文件的情况。这是用户在编辑器中打开一个文件并只想调试它而不需要创建 launch.json 的典型场景。

一个调试配置提供者通过注册来提供特定的调试类型。vscode.debug.registerDebugConfigurationProvider通常在扩展的激活功能。 为了确保调试配置提供程序如果扩展在足够早的时间注册,当使用调试功能时,必须立即激活扩展。这可以通过为扩展激活进行配置来轻松实现。在调试时事件在package.json输入:

"激活事件": [
    "onDebug",
    // ...
],

这个兜底的在调试时一使用任何调试功能就会触发。只要扩展的启动成本低(即在启动序列中不花费太多时间),这就可以正常工作。如果调试扩展的启动成本高(例如,因为启动语言服务器),则在调试时激活事件可能会对其他调试扩展产生负面影响,因为它触发得相当早,并且不考虑特定的调试类型。

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

  • 在调试初始配置在...之前被触发提供调试配置方法调试配置提供程序被调用了。
  • onDebugResolve:类型在...之前被触发解析调试配置解析调试配置并替换变量方法调试配置提供程序调用了指定类型的函数。

经验法则: 如果启用调试扩展的成本较低,请使用在调试时如果它很贵,使用在调试初始配置和/或onDebugResolve取决于调试配置提供程序实现相应的方法提供调试配置和/或解析调试配置输入:.

发布你的调试器扩展

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

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

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

正如我们所看到的,开发调试器扩展通常涉及在两个并行的调试会话中调试扩展和调试适配器。如上所述,VS Code 对此提供了很好的支持,但如果扩展和调试适配器是同一程序,能够在单个调试会话中进行调试,开发可能会更简单。

实际上,只要你的调试适配器是用 TypeScript/JavaScript 实现的,这个方法是非常容易实现的。基本思路是直接在扩展中运行调试适配器,并让 VS Code 连接到它,而不是在每个会话中启动一个新的外部调试适配器。

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

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

  • 调试适配器可执行文件这个对象描述了一个调试适配器,作为一个带有路径和可选参数及运行时的外部可执行文件。该可执行文件必须实现调试适配器协议,并通过stdin/stdout进行通信。这是VS Code的默认操作模式,VS Code会自动使用这个描述符和来自package.json的相应值,如果未提供调试适配器描述符工厂是明确注册的。
  • 调试适配器服务器这个对象描述了一个作为服务器运行的调试适配器,通过一个特定的本地或远程端口进行通信。基于的调试适配器实现vscode调试适配器npm 模块会自动支持此服务器模式。
  • 调试适配器内联实现这个对象描述了一个调试适配器,作为JavaScript或TypeScript对象,实现了vscode.调试适配器接口。基于1.38-pre.4或更高版本的调试适配器实现。vscode调试适配器npm 模块会自动实现接口。

模拟调试展示了三种类型的DebugAdapterDescriptorFactory 的示例以及它们如何注册为'模拟'调试类型。可以通过设置全局变量来选择运行模式运行模式到可能的值之一外部服务器,或内联输入:.

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