# 示例-调试适配器

虽然VS Code实现了一个原生的调试界面,但是它不能直接和调试器通信,而是依赖于调试插件实现调试和运行时的功能特性。

这些调试插件各不相同,主要是因为他们的实现并不运行在扩展主机中,而是作为一个分离的独立程序。我们称这些调试插件为适配器是因为他们能“适配”API、具体的调试器,或者由VS Code定义的调试适配器协议(DAP)。

适配器运行框架

将调试适配器作为单独的运行程序有两方面的原因:第一,对于适合的调试器或者运行时,有其对应的语言来实现;第二,一个独立的程序能运行于底层调试器或者运行时上,也更轻松地运行在特权模式中。

为了避免本地防火墙的问题,VS Code依靠stdin/stdou和适配器通信,而不是sockets等的通信流行机制。

每个调试器插件定义了一种调试类型,而且会被VS Code的启动配置使用。当调试器session结束,调试器也会关闭。

VS Code自带Node.js的调试插件。你也可以在插件市场找到更多调试插件,本篇教程接下来会告诉你怎么开发一个调试器插件。

# 插件Mock Debug


从零开始做一个调试适配器对本篇教程来说实在太重了。因此我们会从一个简单的教学适配器开始,这个模拟的调试插件支持步进,继续,断点,异常捕捉和变量审查,但它并不会真的连接到调试器上。

不过在深入开发模拟调试器之前,我们先安装一个插件市场上的预编译版本来玩玩:

  • 切换到插件侧边栏,输入“mock”然后找到Mock Debug插件。
  • 安装然后重启VS Code

试试Mock Debug吧:

  • 创建一个空文件夹mock test,然后用VS Code打开
  • 新建readme.md,然后输入几行文字
  • 切换到调试侧边栏,然后按齿轮⚙按钮
  • VS Code会让你选择“environment”,然后创建一个默认的启动配置文件。选择“Mock Debug”。
  • 按下绿色的开始按钮,然后进入我们的readme.md文件

调试会话(debug session)就开始了,你可以按“步进(step)”,设置断点,捕捉异常(如果文本中出现exception字样)

那么现在我们要正式学习Mock Debug示例了,我们建议你先删除这个插件。

# Mock Debug开发设置


接下来让我们获取源码,在VS Code中开始开发吧:

git clone https://github.com/Microsoft/vscode-mock-debug.git
cd vscode-mock-debug
npm install
1
2
3

打开项目文件夹vscode-mock-debug,看看里面有什么:

  • package.json mock-debug插件的配置清单
    • 列出了插件的发布内容配置点
    • complie脚本用于编译Typescript到out文件夹中,watch则是侦听后续的源码变动
    • vscode-debugprotocolvscode-debugadaptervscode-debugadapter-testsupport是NPM模块,简化了基于node的调试适配器开发工作
  • src/mockRuntime.ts 提供了简单API的虚拟运行时
  • src/mockDebug.ts适配代码运行时到调试适配器协议中,你能在这个文件中找到各种各样的调试适配器协议和请求。
  • 因为调试插件的实现就在调试适配器中,所以我们就完全没有编写平常要求的插件代码的必要了(如:运行在扩展主机环境的代码),不过Mock Debug有一个小的src/extension.ts文件,这个文件描述了一个调试插件都能做些什么。

现在构建然后加载Mock Debug插件,选择插件加载配置,然后按下F5,一开始,全部的Typescript编译会进入编译,然后输出到out文件夹中。全部构建完成后,会启动一个watch task用于监听你所作的任何改动。

之后会弹出一个新的VS Code窗口“[扩展开发主机]”,Mock Debug插件就运行在调试模式中。在这个窗口中打开mock test,再打开readme.md文件,F5启动调试会话。

现在插件已经启动在调试模式中了,你可以在src/extension.ts打断点,但是就如我之前所说,这个文件没有什么有意思的代码,真正重要的部分都在调试适配器中。

想要调试调试适配器本身,我们必须首先运行在调试模式中。运行调试适配器服务,然后配置VS Code连接上就好了。选中调试面板,从下拉面板中加载Server配置文件,然后按下绿色的开始按钮。

因为我们已经激活了一个插件的调试会话,VS Code调试器界面现在进入了一个多会话模式,我们也能从调试窗口中看到ExtentionServer的调用栈视图:

现在我们可以调试2个插件了,同时模拟调试适配器。使用Extension + Server的启动配置文件可以更快到达这一步。我们在下面提供了一个更为简单的调试插件调试适配器的方案。

那么回到正题,我们现在给src/mockDebug.tslaunchRequest(...)方法开头添加一个断点,最后在启动配置中添加debugServer4711端口将mock debugger连接到调试适配服务器。

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "mock",
            "request": "launch",
            "name": "mock test",
            "program": "${workspaceFolder}/readme.md",
            "stopOnEntry": true,
            "debugServer": 4711
        }
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13

如果你现在加载了这份调试配置,VS Code就不会将mock debug适配器作为分离进程启动,而是直接连接到已启动的服务器端口4711上,然后你就可以在launchRequest上打断点了。

在这个步骤之后,你就可以轻松地编辑,编译和调试Mock Debug项目了。

但是真正的工作现在才开始:你需要把src/mockDebug.tssrc/mockRuntime.ts中的关于"mock"的实现替换成真正和调试器或者运行时通信的代码,你首先可能需要理解调试适配器协议

# 剖析调试插件的package.json


package.json文件除了提供调试器所需的调试适配器和调试插件的实现细节之外,这个文件还包括了各式各样和调试相关的内容配置点

我们现在就来仔细看看Mock Debug的package.json

就像每个VS Code插件一样,package.json声明了基本的插件属性如namepublisherversion。而categories帮助我们能更快地在插件市场找到它。

{
    "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"
    ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

现在我们看看调试插件所需的**发布内容配置(contributes)**部分。

第一个是breakpoints内容配置点,它列出了支持设置断点的语言,没有这个配置点的话,我们就不能在Markdown文件里设置断点了。

第二个是debuggers部分,这里给出了一个调试器,也就是type描述的"mock"。用户可以在选择加载调试配置文件的时候看到这个type。可选的label属性作为type的别称最后会显示在界面上。

既然调试插件用了调试适配器,所以它的相对路径可以在program中定义。为了使插件自包含,应用必须在插件文件夹内。为了方便起见,我们把这个应用所在的文件夹命名为outbin,不过你想用别的名字也随便。

因为让VS Code运行在不同的平台上,所以我们的调试适配程序也必须支持不同的平台,所以我们有了下列选项:

  1. 如果程序是平台无关的,如:程序需要一个统一的运行时支持,那么你可以通过runtime指定,本项目就是采用了这个方法。到目前为止,VS Code支持nodemono运行时。

  2. 如果实现你的调试适配器需要执行在不同平台,那么program配置可以像下面这样指定:

"debuggers": [{
    "type": "gdb",
    "windows": {
        "program": "./bin/gdbDebug.exe",
    },
    "osx": {
        "program": "./bin/gdbDebug.sh",
    },
    "linux": {
        "program": "./bin/gdbDebug.sh",
    }
}]
1
2
3
4
5
6
7
8
9
10
11
12
  1. 组合上面的两种方式也是可行的。下面是一个Mono Debug适配器,只需要macOs和Linux,而不需要Windows支持。
"debuggers": [{
    "type": "mono",
    "program": "./bin/monoDebug.exe",
    "osx": {
        "runtime": "mono"
    },
    "linux": {
        "runtime": "mono"
    }
}]
1
2
3
4
5
6
7
8
9
10

configurationAttributes表示这个调试器是支持launch.json配置,开启之后它会校验launch.json。另外这个属性还会启用:编辑启动配置文件时的智能提示和悬浮帮助文本。

initialConfigurations定义了launch.json的初始默认内容。以便于项目不包含launch.json文件时,用户打开调试或者按下调试侧边栏的齿轮按钮后仍能启动调试会话,VS Code会让用户选择一个调试环境,然后生成对应的launch.json

configurationSnippets定义了编辑launch.json启动配置文件的配置补全。方便起见,给调试环境名称加上label前缀以便于补全下拉框出现时能快速地找到我们的目标。

variables字段将"variables"绑定到了"commands",用**${command:Xxxx}**语法在启动配置中使用这些变量,调试会话启动后这些值会被替换掉。

命令是在插件中实现的,它可既可以提供简单的无界面实现,也可以通过插件API提供界面中的复杂功能点。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"
    });
});
1
2
3
4
5
6

这个变量可以用**${command:AskForProgramName}**形式注入到任何加载配置允许字符串类型的地方。

# 使用DebugConfigurationProvider


如果package.json中的静态调试发布配置内容不够,那DebugConfigurationProvider就派上用场了,它能动态控制调试插件的下列内容:

  • 动态生成launch.json中的debug配置,例如:基于一些工作区可用的上下文信息。
  • 启动新的调试会话前“解析”加载配置,比如根据工作区的一些信息填充默认值。
  • 动态计算调试适配器的可执行路径或者其他命令行参数。

src/extension.ts中的MockConfigurationProvider实现了resolveDebugConfiguration,如果Markdown文件打开而启动调试会话后检测是否有launch.json文件存在。这是一个非常典型的用户场景,用户在编辑器内打开了文件,想要调试的时候却发现没有launch.json文件。

vscode.debug.registerDebugConfigurationProvider是一个调试供应器(debug configuration provider)注册的特殊调试类型,一般出现在activate函数中。为了确保DebugConfigurationProvider尽早注册,插件必须在调试功能启动之前就激活,我们可以在package.jsononDebug事件中配置插件激活事件:

"activationEvents": [
    "onDebug",
    // ...
],
1
2
3
4

这个总的onDebug事件在调试功能一启动就会被调用。如果插件的启动开销不大(如:启动时不要花太多时间)那就会运行正常。如果调试插件的启动开销很大(如:启动一个语言服务器),那么onDebug激活事件就会对其他调试插件产生负面影响了,因为它只想尽早启动,而不会把其他类型的调试器考虑在内。

对于高开销调试插件的更好办法是用粒度更细的激活事件:

  • onDebugInitialConfigurations:在DebugConfigurationProviderprovideDebugConfigurations调用前触发。
  • onDebugResolve:type:在DebugConfigurationProviderresolveDebugConfiguration调用前触发。

首要准则

如果插件的开销不大,就用onDebug。如果插件的开销比较高,根据DebugConfigurationProvider是否调用provideDebugConfigurationsresolveDebugConfiguration,在对应的onDebugInitialConfigurations或者onDebugResolve中处理。

# 发布你的调试适配器


通过下面的步骤将你的调试适配器发布到市场上:

  • 更新package.json中的发布配置内容表明你调试适配器的功能和目标
  • 根据分享插件部分把你的插件上传到市场上

# 可选方案:开发一个调试插件


开发调试插件一般既包含插件,也包含调试器调试适配器这两个平行session。就如上面解释的,VS Code对一点支持非常友好,不过如果想要开发得更容易的话,还是把插件和调试适配器放在一个程序里,用一个会话启动更方便些。

当你在使用Typescript/Javascript实现调试适配器的时候会更容易。

基本的思路是在DebugConfigurationProviderresolveDebugConfiguration方法中拦截调试会话的加载,然后对连接请求就行侦听,对每一个请求创建新的调试适配器会话。为了确保VS Code使用连接请求(而不是总是加载新的调试适配器),可以修改加载配置,加上debugServer

这是几行Mock Debug项目实现的这个方法的代码

通过将编译时间标识EMBED_DEBUG_ADAPTER设置为true启用这个特性。现在如果你用F5启动调试,你不仅仅可以插件中打断点了,调试器适配器里也同样可以。