[vscode]断点调试原理

1,282 阅读4分钟

背景

经常用chrome或者vscode的断点调试,今天突发奇想,ide 的断点调试是如何实现的呢?
本文主要是从以下内容来聊下 vscode 的断点调试:
1、总结项目(vue/react/nodejs/其他)中如何进行断点调试;
2、断点调试的原理;
3、如何自己实现断点调试功能;

如何debugger(vue/react/nodejs/其他)

1、前端项目(vue/react/其他前端项目);一般有两种方式进行调试(launch/attach):

// launch
// 1、启动服务;(e.g. react cli 创建服务后执行 react-scripts start)  
// 2、创建launch.json(将 react cli 本地服务端口填写到url字段中)
// 3、点击调试调用新的chrome进程进行调试
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "chrome",
            "request": "launch",
            "name": "Launch Chrome against localhost",
            "url": "http://localhost:3000",
            "webRoot": "${workspaceFolder}"
        }
    ]
}

// attach
// 1、通过调试模式启动 chrome(需要注意要先退出原有的chrome进程),我用的mac: /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222  
// 2、创建launch.json
// 3、启动服务;(e.g. react cli 创建服务后执行 react-scripts start)  
// 4、点击调试调用新的chrome进程进行调试
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "attach",
      "name": "Attach to browser",
      "port": 9222
    }
  ]
}

一般来说都是通过 launch 来启动比较方便一点。

2、nodejs;
其实有很多种写法,但核心就是通过 node 进程执行入口文件。

// 1、只需要填写入口文件路径,默认通过 node 进程进行获取;  
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Launch Program",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "program": "${workspaceFolder}/index.js"
        }
    ]
}
// 2、配置执行进程路径(npm/node/nodemon/tsnd, 需要注意如果nodemon/tsnd只装到项目下需要通过npx执行),以及执行参数。
// e.g. tsnd
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Launch Program",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "program": "${workspaceFolder}/index.ts",
            "runtimeExecutable": "/Users/vb/.nvm/versions/node/v12.9.0/bin/npx",
            "runtimeArgs": ["tsnd", "--respawn", "index.ts"]
        }
    ]
}
// attach
// 1、启动nodejs服务,获取pid
// 2、创建launch.json文件,配置pid
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Attach to Process",
            "type": "node",
            "request": "attach",
            "processId": "26421"
          }
    ]
}

3、其他(e.g. electron, jest, mocha)

// electron
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Main Process",
      "type": "node",
      "request": "launch",
      "cwd": "${workspaceFolder}",
      "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
      "windows": {
        "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
      },
      "args" : ["."],
      "outputCapture": "std"
    }
  ]
}
// Jest
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Jest Tests",
      "type": "node",
      "request": "launch",
      "runtimeArgs": [
        "--inspect-brk",
        "${workspaceRoot}/node_modules/.bin/jest",
        "--runInBand"
      ],
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen",
      "port": 9229
    }
  ]
}

其实可以看出来,一般分两种情况开启vscode的debug,浏览器开启调试模式/node开启调试模式,然后 vscode 通过 V8 Debugger Protocol发送断点指令给底层调试器进行断点。

断点调试原理

因为 vscode 是支持多种语言的IDE,所以设计了 adaper 来处理不同语言进入到对应的底层调试器。如图:
alt adaper
但在 vscode 官网中没找到有关于跟 nodejs 底层调试器通信相关的内容,然后找到了 解密 VS Code 断点调试的原理,这篇文章说的比较详细,总结来说就是:
1、node 开启调试模式(e.g. node --inspect index.js),会创建一个用于调试的 websocket 服务;
2、客户端创建连接,发送调试指令(V8 Debugger Protocol);
更详细的操作可参考这篇文章 深入理解 Node.js 的 Inspector;

总结一下,不管是js或者是nodejs,在调试的时候,需要开启调试模式(实际上是底层调试器开启了websocket服务,通过websocket接受指令插入到代码执行过程中,这里其实非常复杂,后续如果有时间再深入),IDE通过协议发送指令控制进程。

实现 nodejs 断点

浏览器和node的逻辑是类似的,都是通过 v8 debugger protocol 进行断点,但浏览器的断点是内部实现的,没有把调试链接暴露出来,所以暂时没办法测试。
这里通过实现 nodejs 的断点来了解下断点的逻辑:
1、创建一个简单的nodejs服务,启动调试模式;
2、创建一个websocket服务连接 nodejs 调试连接;
3、发送断点指令进行测试;
具体代码如下:

// 1、创建 index.js
const Koa = require('koa')
const app = new Koa()

app.use(async(ctx) => {
    console.log(1)
    ctx.body = 'hello world!'
})

app.listen(3000)

// 2、开启调试模式: node --inspect index.js // 获取到调试连接,例如我本地是: Debugger ending on ws://127.0.0.1:9229/35765e77-1f9d-43ac-b875-41ab5e014c19
// 3、连接socket服务, 创建一个本地连接
const socket = new WebSocket('ws://127.0.0.1:9229/35765e77-1f9d-43ac-b875-41ab5e014c19')

socket.binaryType = 'arraybuffer';
socket.addEventListener('open', (event) => {
  // 初始化,这里只是参考浏览器调试发送的初始化消息,看起来是开启一些功能和初始化的一些参数,暂不需要深究每条消息的作用
  socket.send(`{"id":1,"method":"Runtime.enable","params":{}}`)
  socket.send(`{"id":2,"method":"Debugger.enable","params":{"maxScriptsCacheSize":10000000}}`)
  socket.send(`{"id":3,"method":"Debugger.setPauseOnExceptions","params":{"state":"none"}}`)
  socket.send(`{"id":4,"method":"Debugger.setAsyncCallStackDepth","params":{"maxDepth":32}}`)
  socket.send(`{"id":5,"method":"Profiler.enable","params":{}}`)
  socket.send(`{"id":6,"method":"Debugger.setBlackboxPatterns","params":{"patterns":[]}}`)
  socket.send(`{"id":7,"method":"Runtime.runIfWaitingForDebugger","params":{}}`)
})
socket.addEventListener('message', (event) => {
  console.log('Message from server ', event.data)
})
socket.addEventListener('error', (event) => {
  console.log('WebSocket error: ', event);
})
// 进行断点
function breakPoint() {
  socket.send(`{"id":10,"method":"Debugger.setBreakpointByUrl","params":{"lineNumber":5,"urlRegex":"/Users/vb/demo/node/server/index.js|file:///Users/vb/demo/node/server/index.js","columnNumber":0,"condition":""}}`)
}
// 继续执行
function continueFn() {
  socket.send(`{"id":14,"method":"Debugger.resume","params":{"terminateOnResume":false}}`)
}

其实调试的指令非常多,这里只是做一个非常简单的断点。inspector 非常强大,很多工具通过 inspector 进行性能监控,后面如果有精力的话会再对 inspector 做一篇详细的分析。

参考文章

1、New home for the Debug Adapter Protocol: code.visualstudio.com/blogs/2018/…
2、调试程序时,设置断点的原理是什么?zhuanlan.zhihu.com/p/34003929
3、解密 VS Code 断点调试的原理: www.barretlee.com/blog/2019/1…
4、深入理解 v8 inspector: www.teqng.com/2021/08/21/…
5、V8 Debugger Protocol: github.com/buggerjs/bu…