Decrypt the principle of VS Code breakpoint debugging

Original link: https://www.barretlee.com/blog/2019/11/15/vscode-study-03-debug-protocol/

VS Code related series of articles, I have written three articles recently ( startup process , installation , development and debugging ), and will continue to output in the process of research and learning, hoping to bring some help to interested readers .

The style of my articles is generally to continue to dig out the underlying principles, break the casserole and ask the bottom line, and I will not stop until the depth that the public can understand. Therefore, when I make an introduction, I will occasionally talk about other related technologies. In order to pave the way for readers to understand.

Usually we often use Chrome for JS breakpoint debugging. Have you ever thought about why the program stops when I set a breakpoint? Why can breakpoints be possible without Chrome Devtool on VS Code? Behind it is How? This is what the text will take you to understand.

Debugging for Node.js

Many languages ​​can be debugged on VS Code, just install the corresponding debugging package. This article takes Node.js as an example. Readers can follow the gourd and try to debug other languages. The principle is clear, and the rest is physical strength live.

First, let’s take a look at how Node.js is debugged. In fact, this blog has introduced it many times, and this time we will discuss it in more depth.

Connect Node.js Debugger

In Node.js before version 6.3, the debugging tool is relatively simple, and the debugging tool that is more active in the community is called node-inspector . What is the principle of this thing, why can it be debugged, we can achieve it with a few simple lines of code A node-inspector with a single function, as shown below is the code to be debugged:

 // file: test.js
let http = require ( 'http' );

const server = http. createServer ( ( req, res ) => {
res. write ( 'hello\n' );
setTimeout ( () => {
debugger ;
res.end ( 'world' );
}, 0 );
});

server. listen ( 8888 , () => {
console.log ( 'start server' ) ;
});

Through node --debug test.js startup program, we started an HTTP Server on port 8888.

Here, please use nvm to switch the version number of node to below 6.3, such as v6.2.2 version. The difference between the old version of Node and the v6.3+ version of Node will be discussed later.

Then initiate a request as follows:

 ➜ curl 127.0.0.1:8888

You will see that the command line only outputs hello\n , and then enters the pending state. Let’s understand step by step why this is the case:

  • Start Node.js with the --debug parameter, and the program will open a built-in Debugger module
  • Since we did not specify a parameter, --debug=PORT , the default port is 5858, and the Debugger module will listen to port 5858
  • When a request is made, if debugger keyword is encountered, the program will suspend execution until it receives the “continue to the next step” instruction

Demo source address: barrel/node-legacy-debug

We can write a program to see what the Debugger module does:

 // File: debugClient.js
const net = require ( 'net' );

const socket = net.createConnection ( 5858 , '127.0.0.1' );
socket. on ( 'data' , ( data ) => {
console . log (data. toString ())
});

process.stdin.on ( 'data' , ( data ) => {
const json = data. toString ();
const msg = `Content-Length: ${Buffer.byteLength(json, 'utf8' )} \r\n\r\n ${json} `;
socket.write (msg, 'utf8' );
});

We use the Net module to connect to the monitor that the program opened on port 5858 just now. When it is connected, it will print the following information:

 Type : connect
V8- Version : 5.0 . 71.52
Protocol- Version : 1
Embedding- Host : node v6.2.2
Content-Length: 0

It means to tell you that you tried to connect to the Debugger module, and the connection has been successful. The V8 version number used by the current debug program is 5.0.71.52 , the protocol version used is 1 , and the Node version number is v6.2.2 . This information In fact, it is telling you what protocol to use to communicate with the debugger. The protocol used in the v6.2.2 version is V8 Debugger Protocol .

In order to print comprehensive information, pay attention to the sequence of operations, first use the debug module to start the program test.js, then start debugClient.js, and finally execute curl to initiate a request.

When curl initiates a request, Debugger will send a message to debugClient:

 Content-Length : 283

{ "seq" : 0 , "type" : "event" , "event" : "break" , "body" : { "invocationText" : "#<Timeout>._onTimeout()" , "sourceLine" : 9 , " sourceColumn" : 4 , "sourceLineText" : " debugger;" , "script" : { "id" : 59 , "name" : "/path/to/test.js" , "lineOffset" : 0 , "columnOffset" : 0 , "lineCount" : 18 } } }

Didn’t curl just now enter pending? Let’s try to send a command to the Debugger (refer to the protocol content ) to tell it to enter the next breakpoint. The content is:

 { "seq" : 1 , "type" : "request" , "command" : "continue" }

In debugClient, we monitor process.stdin , so you can directly paste the above content into the command line and press Enter, and you will also see the feedback sent by Debugger:

 { "seq" : 1 , "request_seq" : 1 , "type" : "response" , "command" : "continue" , ​​"success" : true , "running" : true }

Tell you that you have successfully entered the next breakpoint. At this time, you will also see that curl has output world .

In the above program, you can enter other commands on the command line, such as querying the value of the url of req during pause:

 { "seq" : 2 , "type" : "request" , "command" : "evaluate" , "arguments" : { "expression" : "req.url" } }

You will receive a response like this:

 Content-Length : 177

{ "seq" : 3 , "request_seq" : 2 , "type" : "response" , "command" : "evaluate" , "success" : true , "body" : { "handle" : 150 , "type" : "string" , "value" : "/" , "length" : 1 , "text" : "/" } , "refs" : [ ] , "running" : false }

Have you ever seen the picture of debugging Node.js in your mind? That’s right, that’s how it works.

node-inspector, the debugging agent

The debugging principle has probably been figured out, but I believe that few people are willing to debug the program through the above method, because it is too troublesome, not only to know the names of various instructions, but also to know the parameters of the instructions and the specifications of the protocol , sequence count, etc., so there is node-inspector , which can help us debug Node.js programs visually on Chrome Devtool, so what does it do?

 +-----------------+
| Node Program |
+---------+-------+
^
|
+-----------------+ +---------+-------+
| Chrome Devtool | | Node.js Debugger|
+--------+--------+ +---------+-------+
| ^
| |
CRDP V8DP
| +-------------------+ |
+----->+ node inspector +--------+
+-------------------+

CRDP: Chrome Remote Debugging Protocol
V8DP: V8 Debugging Protocol

To put it simply, node inspector establishes a channel with Chrome Devtool through Chrome’s Remote Debugging Protocol , and then establishes a connection with the Debugger module of the program through V8 Debugging Protocol , so that developers can implement Node.js through visual operations on Chrome. Debugging, so I call it “debugging proxy”, which is a protocol transit service.

Since node-inspector greatly improves the debugging experience of Node, in v6.3, Node.js officials directly integrated this capability into it. You will see that when debugging the program in Node.js using v6.3+, it will print a websocket link adapted to the CRDP protocol:

 ➜ node --inspect test.js
Debugger listening on ws: // 127.0 . 0.1 : 9229 /db309268- 623 a- 4 abe-b19a-c4407ed8998d
For help see https://nodejs.org/en/docs/inspector

We can directly configure this address on Chrome Devtool to enter visual debugging, so the whole link becomes:

 +------------------+ +---------------+
| | | |
| Chrome Devtool | | Node Program |
| | | +-----------+
+--------+---------+ +-----------+ Debugger |
| +-----+-----+
| ^
| CRDP |
+----------------------------------+

Without the access of debugging agent, it will be much easier for developers to use, and the CRDP specification is very frequently used in the community, and there are many codes for reference in the implementation.

Speaking of this, if we want to implement a visual debugging interface by ourselves, is it a bit clear:

 +-----------------+ +----------------+
| | | |
| My IDE | | Node Program |
| +----------+---+ | +----+------+
+------| Debug Client | +-----------+ Debugger |
+--------+---+ +-----+-----+
| ^
| CRDP |
+--------------------------+

You only need to implement Debug Client part in the figure below, and the linkage between Debug Client and the view of the IDE, and you can realize customized visual debugging.

If your requirement is to customize the visual debugging of Node.js, this article can end here. I believe that you are fully capable of implementing a debugging interface with the knowledge learned above. But in VS Code, we need to expand more space.

Debug Adapter Protocol

The cost of implementing a Debug Client is actually quite high. You need to thoroughly understand all debugging protocols, such as the V8 Debugging Protocol, which contains dozens of instructions, and each instruction needs to be adapted for communication and UI. This is just a language , if your IDE is oriented to multiple languages, you need to adapt to multiple debugging protocols. The differences between different protocols may be quite large, and these tasks will completely crash you.

In addition, from the perspective of the community, this kind of construction can be abstracted. Just imagine, Atom needs to implement a Debug Client to debug Node. It is not necessary, so there is Debug Adaptor Protocol , which is a set of general debugging protocols proposed by Microsoft, which has become the de facto standard in the community.

So which layer is this DAP in debugging? We can look at this graph ( source ):

DAP

You can debug all languages ​​by implementing only one communication and UI adaptation to the DAP protocol on the IDE. All you need to do is:

  • Does the community have a DAP implementation of the target language? If so, use it directly, so that it can be quickly adapted and debugged
  • If not, use the knowledge we have learned above to implement a DA and contribute to the community

This set of protocols regulates 5 pieces of content:

  • Base Protocol , which describes the communication format of requests, responses, events, errors, etc.
  • Events , describing more than a dozen event standards such as initialization, configuration completion, output, breakpoint, stop, etc.
  • Request , which describes the request format of various instructions for debugging
  • Response , describes the response format of various commands for debugging
  • Types , describing the types and interface descriptions involved in the above various contents

In principle, all the content mentioned in the specification needs to be implemented in DA. Even if the underlying engine of the language does not have this capability, an error should be thrown to ensure consistency.

Implement a Node.js debugger

In order to understand the several protocols mentioned above, I wrote a DEMO by hand, and realized the simplest operation: display the stack information of the current Debug, as shown in the following figure:

Debug Demo

The warehouse address is: barrellee/node-debug

You can download it and run it, and start it after downloading the dependencies:

 npm run init;
npm run start;
open http://127.0.0.1:4445

The completion of this demo is very low, and the key links are Mock, just to help myself understand the whole process. After understanding, I will not continue to improve the details. Interested students can study it.

I didn’t implement a complete Node.js Debug Adapter by myself, not because of the complexity, so I directly researched VS Code and open sourced two Adaptors, which are:

Choose which version to use based on Embedding-Host information returned when connecting. VS Code seems to install these two packages by default. The overall idea is the same as what I mentioned above.

The specific implementation of VS Code

There are two ways to debug a program, one is to debug a program that has been started, and the other is to debug a program that has not been started. The former VS Code will directly attach it, and the latter will first fork a process launch program. Here we only briefly introduce VS Code Several knowledge points in the actual operation process:

There is no debug parameter when the program starts

If the Node.js program does not start with --debug or --inspect parameter, the Debugger module of Node.js will not be started by default. In this case, it is not impossible to debug. We can start it manually Debug module:

 # Find the PID of the corresponding Node.js process
➜ ps -aux | grep 'node'
# Send SIGUSR1 signal to this PID
kill -SIGUSR1 NODE_PID

stopOnEntry

When forking a Node.js process for debugging, there are two types of input parameters:

  • --debug and --inspect , the default execution program
  • --debug-brk and --inspect-brk , breakpoint directly on the first line by default

Generally speaking, we will choose the first method. If your program will be executed directly and the speed is very fast, you can consider the second method for debugging.

How VS Code connects to the Node.js debugging process

The core content of the Debug Adapter for VS Code debugging Node.js is in the vscode-chrome-debug-core package, which is actually a specific implementation of the Chrome Remote Debugging Protocol, with a lot of internal logic, which seems a bit difficult.

summary

Well, I will write here first, and there are still many details that I will not describe one by one. It is of little value. I have done a lot of preliminary work in writing this article. I have studied DAP and Node.js Debugger for several nights, but Finally, I understand it better. If you have any questions during the test, please leave a message below .

This article is transferred from: https://www.barretlee.com/blog/2019/11/15/vscode-study-03-debug-protocol/
This site is only for collection, and the copyright belongs to the original author.