背景
经常用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 来处理不同语言进入到对应的底层调试器。如图:
但在 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…