基于DAP(Debug Adapter Protocol)开发一个可视化调试器

219 阅读4分钟

在开发GoBot的时候,需要实现调试的功能,最初的时候使用pdb来实现,但是pdb提供的接口太过底层,很多复杂的逻辑需要自己实现,不够方便,后面通过调研发现了DAP,所以使用DAP重新实现了调试功能,特此记录一下。

以前在为编程语言实现调试功能时,必须为每个开发工具重复这项工作,因为每个工具使用不同的API来实现其用户界面。这会导致大量重复的功能(和实现),如下图中的蓝色框所示:

Pasted image 20241220213454.png DAP(Debug Adapter Protocol)就是为了解决这个问题而出现的,它是一种抽象协议,用于在开发工具(例如IDE或编辑器)和调试器或运行时之间进行通信。DAP的主要目的是标准化开发工具与调试器之间的交互方式,使得开发工具能够通过一个通用的调试器与不同的调试器后端进行通信。通过使用DAP协议,可以为每个开发工具实现一个通用的调试器UI,并且可以跨这些工具重用调试器适配器。这大大减少了支持新调试器的工作量。

Pasted image 20241220213625.png

Base protocol 基础协议

基本协议交换由报头和内容部分组成的消息(与HTTP相当)。标题和内容部分由\r\n(回车符,换行符)分隔。

Header Part 报头部分

header部分由header字段组成。每个头字段都由一个键和一个值组成,由“:”(一个冒号和一个空格)分隔。每个标头字段都以\r\n结尾。由于最后一个标头字段和整个标头本身都以\r\n结尾,并且由于标头是强制性的,因此消息的内容部分总是以两个\r\n序列开头(并唯一标识)。目前只支持和需要一个标题字段:Content-Length,用于描述消息体的长度。

Content Part 内容部分

内容部分包含消息的实际内容。消息的内容部分使用JSON来描述请求、响应和事件,使用utf-8编码。

请求示例:

Content-Length: 119\r\n
\r\n
{
    "seq": 153,
    "type": "request",
    "command": "next",
    "arguments": {
        "threadId": 3
    }
}

使用

要实现调试功能,首先需要在python代码中安装debugpy,debugpy 是一个由微软提供的Python调试器,它设计用于与 Visual Studio Code 等现代 IDE 集成,可以使用 VS Code 中的调试界面来调试python代码。 debugpy实现了DAP协议,我们先看看如何通过VSCode调试python代码,再研究如何通过DAP协议调用接口调试

首先安装debugpy

pip install debugpy

然后以debugpy模块的形式执行我们的python代码,这个代码会启动一个端口号为5678的服务,等待调试客户端连接,这里的调试客户端就是VSCode。

python -m debugpy --listen 5678 --log-to ./log --wait-for-client 1.py

或者在代码中使用debugpy

debugpy.listen(("127.0.0.1", robot_inputs.get("debug_port", 5678)))  
debugpy.wait_for_client()

要想实现一个可视化的调试器,就需要实现DAP的客户端,去连接5678这个端口的服务,然后通过网络消息进行调试。微软开源了一个node版本的debugger客户端,vscode-debugadapter-node,谷歌也开源了一个Golang版本的go-dap,因为我们的GoBot使用Golang进行开发,所以也是基于go-dap实现了调试功能。核心代码如下:

func DealDebugSignal(command string) error {  
    if debugInstance != nil {  
       switch command {  
       case "next":  
          debugInstance.Status = "next"  
          debugInstance.Command <- command  
       case "continue":  
          debugInstance.Status = "continue"  
          debugInstance.Command <- command  
       }  
    }  
    return nil  
}  
  
func (dbIns *DebugInstance) initDebugSession() {  
    var err error  
    for debugProcess != nil {  
       dbIns.DebugSession, err = net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(debugPort))  
       if err != nil {  
          time.Sleep(100)  
       } else {  
          break  
       }  
    }  
    if dbIns.DebugSession == nil {  
       return  
    }  
  
    r := bufio.NewReader(dbIns.DebugSession)  
    go readResp(r)  
    initializeRequest := dap.InitializeRequest{  
       Request: dap.Request{  
          ProtocolMessage: dap.ProtocolMessage{  
             Seq:  getReqSeq(),  
             Type: "request",  
          },  
          Command: "initialize",  
       },  
       Arguments: dap.InitializeRequestArguments{  
          ClientID:                            "vscode",  
          ClientName:                          "Visual Studio Code",  
          AdapterID:                           "python",  
          Locale:                              "en-us",  
          LinesStartAt1:                       true,  
          ColumnsStartAt1:                     true,  
          PathFormat:                          "path",  
          SupportsVariableType:                true,  
          SupportsVariablePaging:              true,  
          SupportsRunInTerminalRequest:        true,  
          SupportsMemoryReferences:            false,  
          SupportsProgressReporting:           false,  
          SupportsInvalidatedEvent:            false,  
          SupportsMemoryEvent:                 false,  
          SupportsArgsCanBeInterpretedByShell: false,  
          SupportsStartDebuggingRequest:       false,  
       },  
    }  
    err = dap.WriteProtocolMessage(dbIns.DebugSession, &initializeRequest)  
    if err != nil {  
       log.Logger.Logger.Err(err)  
    }  
    attachRequest := dap.AttachRequest{  
       Request: dap.Request{  
          ProtocolMessage: dap.ProtocolMessage{  
             Seq:  getReqSeq(),  
             Type: "request",  
          },  
          Command: "attach",  
       },  
       Arguments: []byte(`{"name":"Attach","type":"python"}`),  
    }  
    err = dap.WriteProtocolMessage(dbIns.DebugSession, &attachRequest)  
    if err != nil {  
       log.Logger.Logger.Err(err)  
    }  
    firstLine := getFirstLine(dbIns.ProjectPath, dbIns.EntryFile)  
    setBreakpointsRequest := dap.SetBreakpointsRequest{  
       Request: dap.Request{  
          ProtocolMessage: dap.ProtocolMessage{  
             Seq:  getReqSeq(),  
             Type: "request",  
          },  
          Command: "setBreakpoints",  
       },  
       Arguments: dap.SetBreakpointsArguments{  
          Source: dap.Source{  
             Name:             dbIns.EntryFile,  
             Path:             filepath.Join(dbIns.ProjectPath, dbIns.EntryFile),  
             SourceReference:  0,  
             PresentationHint: "",  
             Origin:           "",  
             Sources:          nil,  
             AdapterData:      nil,  
             Checksums:        nil,  
          },  
          Breakpoints: []dap.SourceBreakpoint{  
             {  
                Line: firstLine,  
             },  
          },  
          Lines:          []int{firstLine},  
          SourceModified: false,  
       },  
    }  
    err = dap.WriteProtocolMessage(dbIns.DebugSession, &setBreakpointsRequest)  
    if err != nil {  
       log.Logger.Logger.Err(err)  
    }  
  
    configurationDoneRequest := dap.ConfigurationDoneRequest{  
       Request: dap.Request{  
          ProtocolMessage: dap.ProtocolMessage{  
             Seq:  getReqSeq(),  
             Type: "request",  
          },  
          Command: "configurationDone",  
       },  
       Arguments: nil,  
    }  
    err = dap.WriteProtocolMessage(dbIns.DebugSession, &configurationDoneRequest)  
    if err != nil {  
       log.Logger.Logger.Err(err)  
    }  
}  
  
func readResp(r *bufio.Reader) {  
    for debugInstance != nil {  
       got, err := dap.ReadProtocolMessage(r)  
       if err != nil {  
          log.Logger.Err(err)  
       }  
       log.Logger.Logger.Info().Msg(fmt.Sprintf("事件:%v", got))  
       if resp, ok := got.(dap.EventMessage); ok {  
          event := resp.GetEvent()  
          switch event.Event {  
          case "stopped":  
             if stoppedEvent, ok := resp.(*dap.StoppedEvent); ok {  
                stackTraceRequest := dap.StackTraceRequest{  
                   Request: dap.Request{  
                      ProtocolMessage: dap.ProtocolMessage{  
                         Seq:  getReqSeq(),  
                         Type: "request",  
                      },  
                      Command: "stackTrace",  
                   },  
                   Arguments: dap.StackTraceArguments{  
                      ThreadId: stoppedEvent.Body.ThreadId,  
                   },  
                }  
                resp, err := sendAndWait(debugInstance.DebugSession, r, &stackTraceRequest)  
                if err != nil {  
                   log.Logger.Logger.Err(err)  
                }  
                if stackTraceResponse, ok := resp.(*dap.StackTraceResponse); ok && len(stackTraceResponse.Body.StackFrames) > 0 {  
                   stackFrame := stackTraceResponse.Body.StackFrames[0]  
                   parentDir := filepath.Dir(stackFrame.Source.Path)  
                   filename := stackFrame.Name + ".py"  
                   if parentDir == debugInstance.ProjectPath {  
                      if _, ok := debugInstance.BreakpointMap[filename]; !ok {  
                         debugInstance.BreakpointMap[filename] =  
                            getBreakpoints(debugInstance.ProjectPath, stackFrame.Name)  
                         debugInstance.Line2IdMap[stackFrame.Name+".py"] =  
                            getLine2Id(debugInstance.ProjectPath, stackFrame.Name)  
                      }  
                      if debugInstance.Status == "next" && maputil.HasKey(debugInstance.Line2IdMap[filename], stackFrame.Line) ||  
                         slice.Contain(debugInstance.BreakpointMap[filename], stackFrame.Line) {  
                         scopesRequest := dap.ScopesRequest{  
                            Request: dap.Request{  
                               ProtocolMessage: dap.ProtocolMessage{  
                                  Seq:  getReqSeq(),  
                                  Type: "request",  
                               },  
                               Command: "scopes",  
                            },  
                            Arguments: dap.ScopesArguments{  
                               FrameId: stackFrame.Id,  
                            },  
                         }  
                         resp, err = sendAndWait(debugInstance.DebugSession, r, &scopesRequest)  
                         if err != nil {  
                            log.Logger.Err(err)  
                         }  
                         if scopesResponse, ok := resp.(*dap.ScopesResponse); ok && len(scopesResponse.Body.Scopes) > 0 {  
                            variablesRequest := dap.VariablesRequest{  
                               Request: dap.Request{  
                                  ProtocolMessage: dap.ProtocolMessage{  
                                     Seq:  getReqSeq(),  
                                     Type: "request",  
                                  },  
                                  Command: "variables",  
                               },  
                               Arguments: dap.VariablesArguments{  
                                  VariablesReference: scopesResponse.Body.Scopes[0].VariablesReference,  
                               },  
                            }  
                            resp, err = sendAndWait(debugInstance.DebugSession, r, &variablesRequest)  
                            if err != nil {  
                               log.Logger.Err(err)  
                            }  
                            pauseData := PauseData{  
                               FileName:  stackFrame.Name,  
                               LineId:    debugInstance.Line2IdMap[filename][stackFrame.Line],  
                               Variables: make([]models.Variable, 0),  
                            }  
                            if variablesResponse, ok := resp.(*dap.VariablesResponse); ok {  
                               for _, variable := range variablesResponse.Body.Variables {  
                                  pauseData.Variables = append(pauseData.Variables, models.Variable{  
                                     Name:  variable.Name,  
                                     Type:  variable.Type,  
                                     Value: variable.Value,  
                                  })  
                               }  
                            }  
                            runtime.EventsEmit(debugInstance.AppContext, constants.BreakpointHitEvent, pauseData)  
                            <-debugInstance.Command  
                         }  
                      }  
                   }  
                }  
                nextRequest := dap.NextRequest{  
                   Request: dap.Request{  
                      ProtocolMessage: dap.ProtocolMessage{  
                         Seq:  getReqSeq(),  
                         Type: "request",  
                      },  
                      Command: "next",  
                   },  
                   Arguments: dap.NextArguments{  
                      ThreadId: stoppedEvent.Body.ThreadId,  
                   },  
                }  
                err = dap.WriteProtocolMessage(debugInstance.DebugSession, &nextRequest)  
                if err != nil {  
                   log.Logger.Logger.Err(err)  
                }  
             }  
          case "terminated":  
             log.Logger.Logger.Info().Msg("terminated")  
          }  
       }  
    }  
}  
  
func sendAndWait(w io.Writer, r *bufio.Reader, message dap.Message) (dap.ResponseMessage, error) {  
    err := dap.WriteProtocolMessage(w, message)  
    if err != nil {  
       return nil, err  
    }  
    for {  
       got, err := dap.ReadProtocolMessage(r)  
       if err != nil {  
          return nil, err  
       }  
       if resp, ok := got.(dap.ResponseMessage); ok {  
          if resp.GetResponse().RequestSeq == message.GetSeq() {  
             return resp, nil  
          }  
       }  
    }  
}  
  
var globalSeq = 0  
  
func getReqSeq() int {  
    globalSeq += 1  
    return globalSeq  
}

总结

通过DAP,在GoBot实现了可视化的调试功能,可以设置断点,单步运行,查看变量的值等,便于调试流程发现问题。

Pasted image 20241220221435.png