Connect

实现目标: tinydbg connect <addr>

在远程调试模式下,connect命令用来连接一个调试器后端,完成网络通信层的初始化,然后初始化一个前端调试会话,开发者即可交互式地进行调试了。

$ tinydbg help connect
Connect to a running headless debug server with a terminal client. Prefix with 'unix:' to use a unix domain socket.

Usage:
  tinydbg connect <addr> [flags]

Flags:
  -h, --help   help for connect

Global Flags:
      --init string         Init file, executed by the terminal client.
      --log                 Enable debugging server logging.
      --log-dest string     Writes logs to the specified file or file descriptor (see 'dlv help log').
      --log-output string   Comma separated list of components that should produce debug output (see 'dlv help log')

基础知识

相比attach、exec、debug (or test)、core这几个调试命令,connect是彻彻底底的为远程调试准备的。既然是远程调试,就涉及到调试器前端、后端独立运行。

调试器后端运行,可以通过attach、exec、debug(or test)、core,并配合参数 --headless 参数就可以启动一个调试器后端,它等待调试前端通过TCPConn或UnixConn以JSON RPC或者DAP RPC的形式进行通信。在我们的demo tinydbg中,我们只支持JSON-RPC进行通信。关于DAP (Debugger Adapater Protocol),我们在 "3-高级功能扩展" 小节进行介绍。

调试器后端运行时,允许通过参数 -l | --listen 来指定一个监听地址:

-l, --listen string                    Debugging server listen address. Prefix with 'unix:' to use a unix domain socket. (default "127.0.0.1:0")
  • default:127.0.0.1:0,port没有指定的情况下,会自动分配一个port,调试器进程会打印出监听地址,以方便调试器前端连接; 与VSCode集成后为了更方便地进行调试,就需要前后端能够就监听地址达成一致,以方便VSCode调试器前端连接;
  • 指定具体的 IP:PORT,如果提前规划好了使用某个IP:PORT用于RPC通信,也可以指定IP:PORT;
  • 指定 unix:/path-to/socket,也可以使用Unix Domain Socket进行通信;

如果考虑到VSCode远程开发、容器开发以及WebIDE远程开发,那我们还得掰扯掰扯VSCode的C/S分离式架构,以及插件运行方式(extensionKind,在UI/Local Extension Host、Remote/Workspace Extension Host、或二者均可)。如果咱们有时间的话,就分享下这些内容,以及VSCode(C/S)、VSCode调试器插件(local/remote extension host)、调试器前后端(C/S)它们之间是如何进行交互的。

OK,先言归正传,我们先介绍下connect命令的代码实现。

代码实现

前面调试器会话小节,我们提到过connect的大致实现方式,这里再简单回顾一遍吧,建立调试会话的代码路径是:

main.go:main.main
    \--> cmds.New(false).Execute()
            \--> connectCommand.Run()
                    \--> connectCmd(...)
                            \--> connect(addr, nil, conf)
                                    \--> conn := netDial(addr)
                                            \--> if isTCPAddress, conn, _ := net.Dial("tcp", addr) 
                                            \--> if isUnixAddress, conn, _ := net.Dial("unix", addr)
                                    \--> client := rpc2.NewClientFromConn(conn)
                                    \--> session := debug.New(client, conf)
                                    \--> session.Run()
                                            \--> forloop
                                                    \--> read input
                                                    \--> parse debugcmd flags args
                                                    \--> session.client.Call('RPCServer.'+method, req, rsp)
                                                            \--> json-rpc over tcpconn or unixconn
                                                    \--> update UI based on rsp

执行connect命令,大致会经历上述代码路径,connect会根据传递的参数addr来确定是一个tcp监听地址,还是一个unix domain socket,然后建立对应的连接。一旦连接建立了,就可以初始化rpcclient。然后初始化一个调试会话,调试会话运行起来后就是一个类似repl的forloop,读取输入,解析命令、参数、选项,然后执行。只不过这里的执行,需要与调试器服务器交互,而且几乎所有的调试命令都如此。调试器会话与调试器服务器之间通过建立的通信链路完成请求发送、响应接受。然后根据响应,调试器前端更新显示,如显示变量值、指令列表、打印类型详情、显示当前程序执行到的指令地址及源码位置,等等。

调试器会话初始化、网络通信层的初始化过程,以及后续调试器前端与调试器后端的详细交互过程,我们都已经在调试会话小节已经详细介绍了,这里就不再赘述了。

值得一提的是,调试器后端启动调试时如果指定了 --accept-multiclient 那么才允许调试器后端执行期间接受多个入客户端连接请求:

  • 客户端1正在调试,此时客户端2来连接;
  • 客户端2已经结束调试,并且已经与调试服务器分离,但是没有杀死进程实例,此时客户端来连接;

这两种情况,如果想允许客户端2来连接,都需要在启动调试器后端时显示指定上述选项 --accept-multiclient。那么为什么不默认启用选项 --accept-multiclient 呢?

对于常见的 tinydbg debug ... 操作来说,因为程序是我们自动构建出来的,也是自己启动的进程,所以调试完后默认预期是这个进程已经被调试利用完了,没有继续存在的必要了,所以会提示调试人员是否需要自动kill该进程,绝大多数情况下,大家会点“是”。这才是绝大多数情况。而对于前一次调试完了,后面又发起一次调试,但是这种情况下,说明一时半会确定不了问题,需要多次调试跟踪,此时在有明确诉求的情况下,直接加选项 --accept-multicilent 后启动即可。另外,如果我们加了这个选项,在我们调试期间,如果真的有人连接进来了,它执行的一些调试动作可能会影响到我们。但是,允许多个客户端同时登录也增加了一定的灵活性,如这样可能允许多人联合调试、联合定位异常。

执行测试

本节小结

本节介绍了connect命令的实现,它允许调试器前端连接到独立运行的调试器后端进程。我们详细讲解了连接建立的过程、调试会话的初始化,以及多客户端连接支持的相关考虑。这为理解分布式调试场景下调试器的工作方式提供了基础。

results matching ""

    No results matching ""