前端UI层设计

联想下调试器的整体架构设计,它属于前后端分离式架构,对于前端部分:

  • UI层为用户提供调试相关的界面交互逻辑;
  • Service层完成与调试器后端实现的通信,完成对被调试进程的各种控制;

本节就来介绍下这里的UI层的详细设计,以及相关的技术点。

命令行调试界面

该调试器最终的界面是一个终端窗口,以文本模式的形式与用户交互,获取用户输入的调试命令,转换成对应的调试动作执行,并将结果以文本模式的形式显示出来。

终端可以工作在文本模式,或者图形模式下,我们这里采用文本模式。其实主流的命令行调试器gdb、lldb、dlv等都是工作在终端模式下,当然采用了前后端分离式架构的话也比较容易为其提供一个图形化的调试界面。

调试命令管理

需要支持的调试功能众多,我们前面做需求分析时对需要支持的调试命令进行了整理,并将它们按照调试动作的类型进行了分组。

这些要支持的调试命令,根据使用的阶段可以分成两类。一类属于如何发起调试,一类属于在调试会话中如何读写、控制被调试进程状态。这样的话,我们在进行命令管理的时候就要注意区分为两组不同的命令。

方式1:统一由cobra管理

在进行指令级调试器设计实现时,我们采用cobra命令行框架来组织命令。首先我们注册了两个发起调试的命令:godbg execgodbg attach

rootCmd.AddCommand(execCmd)
rootCmd.AddCommand(attachCmd)

当调试器正常attach到被调试进程后,我们会紧接着启动一个调试会话DebugSession,其实这个DebugSession内部能运行的所有调试命令,也是由cobra命令行框架管理的,每个调试会话内部都有一个root *cobra.Command,我们在这个root上注册了一系列调试命令。

// DebugSession 调试会话
type DebugSession struct {
    root   *cobra.Command
    ...
}

debugRootCmd.AddCommand(breakCmd)
debugRootCmd.AddCommand(clearCmd)
...
debugrootCmd.AddCommand(nextCmd)

启动调试的命令、调试会话中的调试命令,这些命令我们都是用cobra来管理的,只不过分了两级来管理,这种设计方式更优雅简单。

方式2:cobra+自定义管理逻辑

但是接下来我们要换一种实现思路,启动调试的命令attach、exec等还是采用cobra管理,调试会话中的调试命令将用自己编写的命令组织逻辑来管理。为什么要这么做呢?

  • 需要允许用户自定义调试命令的别名,而不仅仅是cobra.Command.Aliases中指定的这些,而cobra也没有提供可配置的方式来自由添加别名;
  • cobra框架中各个命令对应的处理函数只有cmd、flags、args参数,但是调试过程中我们需要维护一点状态相关的信息,并且需要将这些信息传递给调试命令的处理函数,当然是以参数的形式,而cobra框架中命令处理函数列表是无法传递的,而这些也不适合通过共享变量的形式来维护;
  • 除了要实现的这些功能,最终也希望能提供额外的扩展能力,我们可以为调试器嵌入starlark脚本引擎、注册新调试命令的函数,这样开发人员可以自定义starlark函数作为dlv的新的调试命令,这样来扩充调试器功能。要实现这些这就要求dlv能够对子命令的管理逻辑细节100%可控制,而cobra作为一个框架存在一些限制;

因此,在接下来的符号级调试器中,调试会话中的调试命令是通过重写的命令管理逻辑来完成的,而非像之前那样由cobra管理。

用户交互管理

这里与用户的交互,主要涉及到用户的输入、调试器的输出两部分。

用户输入

当执行attach或exec启动调试之后,会启动一个调试会话,其实就是一个可以输入调试命令、展示调试结果的命令行窗口:

  • 用户可以在stdin输入调试命令及其参数,然后等待调试器执行对应的调试动作(如读写内存),然后等待调试器结果,结果会输出到stdout;
  • 用户可以输入help命令查看当前调试器支持哪些调试命令,这些命令将按照所属的分组进行汇总显示,如断点相关、运行暂停相关、数据读写相关、goroutine相关、stack等分组;
  • 用户也可以输入help subcmd来显示某个特定命令的详细帮助信息,此时会显示subcmd的各个参数的帮助信息;
  • 用户可以输入调试命令的别名,而非完整的命令名,以简化命令输入;
  • 用户可以直接键入回车键Enter,来重复执行上一次输入的调试命令,这在执行next、step时将非常有用;
  • 为了方便用户输入过去输入过的调试命令,我们还可以记录用户输入过的命令,并允许用户通过方向键up/down来选择过去输入过的命令,并且还可以允许自动补全,以简化命令输入;
  • 当用户向结束调试时,可以通过ctrl+c或者exit、quit等命令结束调试;

用户的输入动作都是非常简单的在stdin上的行输入,在调试会话启动后,我们就可以启动一个for-loop来不停地读取stdin上的行输入,当读取到一个完整的行之后,我们就将输入信息进行解析,解析成命令、及参数,这里的命令也可能是别名。然后查找所有的命令中哪个命令的别名与用户输入相同,一旦找到该命令,则执行命令关联的处理函数,完成调试动作。

关于这里的输入逻辑,接下来将使用peterh/liner这个第三方库来方便地管理用户输入、执行输入处理、记录历史输入、输入自动补全等功能。

调试器输出

调试器的输出信息,包括执行日志,以及调试命令的结果。这两类信息,我们的调试器实现中都是将其输出到stdout,以简化实现复杂度。

  • 本地调试时,调试器前端、后端的日志都是输出到stdout的,调试结果首先是由backend发送给frontend,frontend做些数据转换之后就输出到stdout显示出来。所以本地调试时,日志、调试结果都可以在stdout中查看到;

  • 远程调试时(或者是同一个机器也是起了前端、后端两个进程时),调试器前端、后端的日志各自输出也均输出到stdout,如果是在两个不同的终端中运行,那么日志输出到对应的终端中。对于调试结果则由backend发送给frontend,最终由frontend显示在其对应的终端中。

  • 对于frontend、backend对应的日志如果不关心,可以通过日志级别将其关闭,或者通过选项--log指定个日志文件让其将日志信息输出到指定日志文件中。

    ps:支持--log选项,这么设计并不一定最终这么实现,我们为了赶进度,做了些简化,只允许调试日志、结果输出到stdout,但是会给予一定的日志级别控制。

  • 个别输出信息可能需要颜色高亮,如执行l main.go:10这样来查看源代码时,我们希望能根据源代码中不同的关键字、语句、注释、字符串、当前执行到的源码行等能像IDE中那样有个不同颜色的高亮显示,这样对于用户而言无意是更加友好的。这就意味着我们需要对源代码进行必要的AST分析统计出有哪些词素需要高亮显示。

本节总结

本节简要介绍了调试器前端UI层的一些设计,包括命令行调试界面、调试命令管理、用户交互管理,在后面的实现部分我们将进一步结合源码来展开。

results matching ""

    No results matching ""