跨平台PHP调试器设计及使用方法——拾遗

        之前七篇博文讲解了跨平台PHP调试器从立项到实现的整个过程,并讲解了其使用方法。但是它们并不能全部涵盖所有重要内容,所以新开一片博文,用来讲述其中一些杂项。(转载请指明出于breaksoftware的csdn博客)

触发调试的方法

        Xdebug提供了两种调试方式。一种需要我们在触发调试的URL中新增XDEBUG_SESSION_START或XDEBUG_SESSION_STOP_NO_EXEC来控制调试开启或关闭。比如我们要调试http://192.168.41.130/var/www/html/index.php触发的逻辑,则需要请求

http://192.168.41.130/var/www/html/index.php?XDEBUG_SESSION_START=netbeans-xdebug

        调试结束后,我们需要请求下面链接以关闭调试

http://192.168.41.130/var/www/html/index.php?XDEBUG_SESSION_STOP_NO_EXEC=netbeans-xdebug

        这种方法存在明显的缺陷。比如我们一个待测功能页中,我们不可能给每个触发调试的URL增加上述标志。更不可能在每次调试后触发一次关闭调试的请求。因为页面中发起请求的方式和位置可能很多,每次手工去修改原始代码也违背了我设计该调试器的初衷。我曾考虑过给待测页面包一层框架,即我们设计一个页面“浏览器”。在这个页面浏览器中访问待测页面。待测页面中触发的请求被外层的页面“浏览器”捕获,并追加相关参数再发起真实请求。但是我觉得这个方案有点让整个调试器的设计偏向于设计一款功能强大的页面“浏览器”,所以它只能作为我最次方案的一种选择。

        Xdebug还有另一种触发调试的方法,就是自动触发,即每次请求来都触发调试行为。我们需要做的就是在配置文件中新增如下内容

xdebug.remote_autostart=On

        这个方案也会有问题。就是我们在调试时往往只是关注于一两个请求对应的处理逻辑,而往往抵达触发这一两个请求场景之前还会有其他请求被发起。打个比方,我们要调试让用户修改自己信息的接口。在此之前肯定有一次获取该用户已有信息的请求,然后把用户信息显示出来。用户修改时,可能有些信息还要经过PHP逻辑校验,这些也是请求。这样在用户保存修改信息之前已经调用了若干接口,而这些接口可能会被我们设置的断点中断。即使我们没有设置断点,也会被中断到代码的第一行。对于这些我们不关心的调试流程,我们可能会不停执行Run当本次调试结束。但是这么繁琐的体验是非常不好的。于是这就有了设计“调试开关”的必要。

        在我们触发调试前,我们调试开关关闭,这样既省事又有效率。当我们要触发调试时,才开启调试开关。

Python错误

        在一些环境下,使用Python2.7搭建和使用该调试器时,会报CTYPE= CTYPE.ENCODE(DEFAULT_ENCODING) # OMIT IN 3.X! UNICODEDECODEERROR错误。好在网上有很多解决方案,就直接删掉那几行就行。

FPM超时问题

        在一些生产环境下,为了增强用户体验以及预防一些错误发生,往往会设置一些超时参数。比如PHP的FPM就可以设置超时时间。但是在开发环境下,一般这个超时可以不用设置,而且设置还会影响调试器的使用。因为我们调试一段代码可能会消耗很多时间,没谁可以估算出这个超时要设置多久。如果遇到这个问题的同学,可能参见《PHP超时处理全面总结》

Pydbgp的缺陷

        在探索Pydbgp库时,我发现这个库并非非常完善,它还存在一些缺陷。同时为了不影响它的整体结构,我基本就是打patch的思路去做修改,且要求做到最小修改以解决问题。

    已结束调试Session残留

        首先我们使用session查看可调试会话ID,然后使用select指令进入调试会话并进行调试。当我们退出调试会话时,存在两种状态:调试已经结束(运行到代码结尾处之后)和调试仍可进行(只是退出调试会话,该会话还有效)。Pydbgp库存在一个问题,它会一直保存会话ID,而不管其是否已经失效。这样随着我们调试次数的增多,session指令获取的会话ID会越来越多,而往往大部分都是无效的。对于我们自动选择调试会话的调试器状态机来说这个工作任务会越来越重,所以这个地方需要做优化。优化的方案也非常简单,在pydbgpd.py的do_quit方法做如下修改

    def do_quit(self, argv):
        """
    quit, exit, q -- exit the DBGP Application Layer shell
        """
        if self._app.currentSession._stop:
            self._app.currentSession._application.releaseSession(self._app.currentSession)

    当前会话设置出错

        在调试器中,有若干会话,其中只有一个会话可能成为当前正在被调试的会话。但是原代码中对当前会话的切换判断存在缺陷,它没有考虑到当前会话是否已经失效。修改点是dbgp\server.py文件中class application的addSession方法

    未返回断点ID信息

        当我们设置一个断点后,应该返回该断点ID。我们可以通过该断点ID去删除它。然而Pydbgp却将这个ID给“私吞”了。于是我们要做修改让它放开这个数据。修改点在dbgp\server.py文件中

    未返回Array和Object类型变量信息

        这个问题也是非常致命的。我们查看一个变量,它可能是int型的,可能是string型的。这些基础类型Pydbgp均作了解析和记录。然而对于复杂类型,比如Array或者Object类型变量,Pydbgp都没对它们进行解析。这块功能只能我们自己写了,我决定使用Json格式来保存这些数据。这块的修改点在dbgp\server.py文件中class property的initWithNode函数

        还要新增一个转换成Json的函数

父子(孙)进程管理

        在我初步的设想中,我们只要让调试器的Python代码在一个进程中执行,然后以其为父进程,启动一个执行Pydbgp库的python子进程进程。这样两个进程之间关系比较简单且易于维护。当我们需要关闭调试时,只要把子进程关闭即可。但是实际实现这段逻辑时,发现Windows上可以做到,但是在我的linux环境则不可以。于是只能靠孙子进程来完成这样的设计。这块代码在class pydbgpd_stub中

    def start(self):
        if (self._exc_cmd == None):
            raise NameError("exc_cmd is none")
        if "Windows" == platform.system():
            self._process = subprocess.Popen(self._exc_cmd, shell = False)
        else:
            self._process = subprocess.Popen(self._exc_cmd, shell = True,  preexec_fn = os.setpgrp)
        time.sleep(2)
        self._cmd_client.Start()
        
    def stop(self):
        self._cmd_client.Stop()
        
        if not self._process:
            raise NameError("subprocess is none")
        else:
            if "Windows" != platform.system():
                pid = self._process.pid
                pgid = os.getpgid(pid)
                os.kill(-pgid, 9)
            self._process.terminate()
            self._process.kill()
            self._process = None

        至此,我们便将该调试器相关设计和使用方法讲完了。代码维护在https://github.com/f304646673/PhpDebugger.git上。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券