刘泉皓

没有最强的法术,只有最强的法师。

vscode用xdebug调试php多进程程序

26 Jul 2018 » memory

心血来潮,开始分析workerman源码,源码过了一遍,想用debug工具运行跟踪调试一下,结果卡在pcntl_fork()方法,因为每次执行之后,在接下来主进程执行exit(0)后当前进程就结束了,xdebug远程连接也就断开连接了。网上翻了半天,最终找到解决办法,那就是使用xdebug-2.7.0alpha1版本调试即可。

首先是vscode的Debug的配置文件:

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Listen for XDebug",
            "type": "php",
            "request": "launch",
            "port": 9001,
        },
        {
            "name": "Launch currently open script",
            "type": "php",
            "request": "launch",
            "program": "${file}",
            "cwd": "${fileDirname}",
            "port": 9001,
            "args": [
                "start",
                "-d",
            ],
        }
    ]
}

调试时需要Launch currently open script这个配置,配置上我把端口改到了9001,因为php-fpm默认占用了9000端口,当我用cli启动workerman时无法绑定到9000。然后添加了args参数,因为我在cli下启动workerman的参数是php test.php start -d,其中test.php是测试程序:

<?php
require_once __DIR__ . '/Autoloader.php';
use Workerman\Worker;
// Create a Websocket server
$ws_worker = new Worker("websocket://0.0.0.0:2346");
// 4 processes
$ws_worker->count = 4;
// Emitted when new connection come
$ws_worker->onConnect = function($connection)
{
    echo "New connection\n";
 };
// Emitted when data received
$ws_worker->onMessage = function($connection, $data)
{
    // Send hello $data
    $connection->send('hello ' . $data);
};
// Emitted when connection closed
$ws_worker->onClose = function($connection)
{
    echo "Connection closed\n";
};
// Run worker
Worker::runAll();

其中导致调试结束的代码是Worker.php中的代码:

 /**
     * Run as deamon mode.
     *
     * @throws Exception
     */
    protected static function daemonize()
    {
        if (!static::$daemonize || static::$_OS !== OS_TYPE_LINUX) {
            return;
        }
        umask(0);
        $pid = pcntl_fork();
        if (-1 === $pid) {
            throw new Exception('fork fail');
        } elseif ($pid > 0) {
            exit(0);
        }
        if (-1 === posix_setsid()) {
            throw new Exception("setsid fail");
        }
        // Fork again avoid SVR4 system regain the control of terminal.
        $pid = pcntl_fork();
        if (-1 === $pid) {
            throw new Exception("fork fail");
        } elseif (0 !== $pid) {
            exit(0);
        }
    }

问题就出在这里:

$pid = pcntl_fork();
...
elseif ($pid > 0) {
    exit(0);
}
...

fork后主进程exit(0)退出,但是此时我xdebug连在主进程,所以就调试器就挂了。

接下来解决这个问题,根据xdebug官方bug报告平台的一篇文章,最后面一个回复提到了修复了此bug,真的是非常幸运,今天是2018-7-26,而这个回答是2018-07-09,2个星期前:laughing:。github修复这个bug的commit在这。2018-2-24修复,于2018-4-1号打包2.7.0alpha1版本,包含了修复这个bug的commit。

所以我就在github上下载了最新的代码。由于我的系统是ubuntu18.04,而且已经用apt安装了php-xdebug,所以我先卸载:

liuxu:~$ sudo dpkg -l | grep xdebug
ii  php-xdebug                                                  2.6.0-0ubuntu1                      amd64        Xdebug Module for PHP
liuxu:~$ sudo apt remove php-xdebug

看看,是2.6.0-0ubuntu1,这个版本没有修复php中fork的bug。

然后我需要先安装php编译工具包,需要里面的phpizephp-config

liuxu:Downloads$ sudo apt install php7.2-dev

然后解压编译xdebug:

liuxu:Downloads$ unzip xdebug-master.zip
liuxu:Downloads$ cd xdebug-master/
liuxu:xdebug-master$ phpize
liuxu:xdebug-master$ ./configure
liuxu:xdebug-master$ make
liuxu:xdebug-master$ ls modules/
xdebug.la  xdebug.so

现在已经编译好了,把xdebug.so复制到php的模块目录,要说明的是,不同系统不同php版本可以安装目录不一样,要自己查一下:

liuxu:xdebug-master$ whereis php
php: /usr/bin/php7.2 /usr/bin/php /usr/lib/php /etc/php /usr/include/php /usr/share/php7.2-json /usr/share/php7.2-mysql /usr/share/php /usr/share/php7.2-curl /usr/share/php7.2-readline /usr/share/php7.2-common /usr/share/php7.2-opcache /usr/share/php7.2-xml /usr/share/man/man1/php.1.gz
liuxu:xdebug-master$ ls /usr/lib/php
20170718  7.2  php7.2-fpm-reopenlogs  php-helper  php-maintscript-helper  sessionclean
liuxu:xdebug-master$ ls /usr/lib/php/20170718/
build        curl.so  fileinfo.so  iconv.so     mysqli.so   pdo_mysql.so  posix.so     simplexml.so  sysvsem.so    wddx.so       xmlwriter.so
calendar.so  dom.so   ftp.so       json.so      mysqlnd.so  pdo.so        readline.so  sockets.so    sysvshm.so    xmlreader.so  xsl.so
ctype.so     exif.so  gettext.so   memcache.so  opcache.so  phar.so       shmop.so     sysvmsg.so    tokenizer.so  xml.so
liuxu:xdebug-master$ sudo cp modules/xdebug.so /usr/lib/php/20170718/

可以看到已经有很多so文件,php默认在这个目录里找php模块,放在其他目录的话,需要在配置文件里写绝对路径,放在这里只需要写文件名就可以了。接下来配置xdebug的配置文件:

liuxu:conf.d$ cat /etc/php/7.2/mods-available/xdebug.ini
zend_extension=xdebug.so
xdebug.remote_enable=1
xdebug.remote_autostart=1
xdebug.remote_port=9001
liuxu:mods-available$ cd ../cli/
liuxu:conf.d$ sudo ln -s /etc/php/7.2/mods-available/xdebug.ini 20-xdebug.ini
liuxu:conf.d$ ls -al /etc/php/7.2/cli/conf.d/20-xdebug.ini
lrwxrwxrwx 1 root root 38 7月  26 16:36 /etc/php/7.2/cli/conf.d/20-xdebug.ini -> /etc/php/7.2/mods-available/xdebug.ini

这个配置是有点讲究,其实可以直接将文件写到/etc/php/7.2/cli/conf.d/下,但是这里只是做了一个软连接,方便cliphp-fpm共用/etc/php/7.2/mods-available/中的一个配置文件。

这样就配置好了,接下来再用vscode调试,在$pid = pcntl_fork();打上中断,运行中断在这后,单步执行下去,可以不用被主进程退出了。

img1

注意调试的时候要在test.php标签下,因为是从它开始启动程序的,左上角要选Launch currently open script,因为我是用php test.php ...执行的启动脚本。可以看见左边中间的Request 1(5:30:40 PM)Request 2(5:30:44 PM),这个应该是调试时连接上xdebug_remote的时间,可以认为是进程启动时间。看看左边,默认应该只有Request 1,但在当前代码前执行了2个pcntl_fork(),所以新开了2个进程,xdebug_remote自动连接上了这2个进程,所以左边有3个Request。随便点击一个可以跳转到不同进程,真的很方便,但是需要注意的是不要执行exit(0);,不然调试又要断,这个应该还是个bug,需要再修复。