9

问题

我正在使用一个proc_open()用于调用 shell 命令的函数。看来我做 STDIO 的方式是错误的,有时会导致 PHP 或目标命令锁定。这是原始代码:

function execute($cmd, $stdin=null){
    $proc=proc_open($cmd,array(0=>array('pipe','r'),1=>array('pipe','w'),2=>array('pipe','w')),$pipes);
    fwrite($pipes[0],$stdin);                fclose($pipes[0]);
    $stdout=stream_get_contents($pipes[1]);  fclose($pipes[1]);
    $stderr=stream_get_contents($pipes[2]);  fclose($pipes[2]);
    return array( 'stdout'=>$stdout, 'stderr'=>$stderr, 'return'=>proc_close($proc) );
}

大部分时间都有效,但这还不够,我想让它一直有效。

如果 STDIO 缓冲区超过 4k 的数据,问题就在于stream_get_contents()锁定。

测试用例

function out($data){
    file_put_contents('php://stdout',$data);
}
function err($data){
    file_put_contents('php://stderr',$data);
}
if(isset($argc)){
    // RUN CLI TESTCASE
    out(str_repeat('o',1030);
    err(str_repeat('e',1030);
    out(str_repeat('O',1030);
    err(str_repeat('E',1030);
    die(128); // to test return error code
}else{
    // RUN EXECUTION TEST CASE
    $res=execute('php -f '.escapeshellarg(__FILE__));
}

我们两次向 STDERR 和 STDOUT 输出一个字符串,总长度为 4120 字节(超过 4k)。这会导致 PHP 在两边都锁定。

解决方案

显然,stream_select()是要走的路。我有以下代码:

function execute($cmd,$stdin=null,$timeout=20000){
    $proc=proc_open($cmd,array(0=>array('pipe','r'),1=>array('pipe','w'),2=>array('pipe','w')),$pipes);
    $write  = array($pipes[0]);
    $read   = array($pipes[1], $pipes[2]);
    $except = null;
    $stdout = '';
    $stderr = '';
    while($r = stream_select($read, $write, $except, null, $timeout)){
        foreach($read as $stream){

            // handle STDOUT
            if($stream===$pipes[1])
/*...*/         $stdout.=stream_get_contents($stream);

            // handle STDERR
            if($stream===$pipes[2])
/*...*/         $stderr.=stream_get_contents($stream);
        }

        // Handle STDIN (???)
        if(isset($write[0])) ;

// the following code is temporary
$n=isset($n) ? $n+1 : 0; if($n>10)break; // break while loop after 10 iterations

    }
}

剩下的唯一一块拼图是处理 STDIN(见标有 的行(???))。 我发现 STDIN 必须由调用我的函数的任何东西提供,execute(). 但是如果我根本不想使用 STDIN 怎么办?在上面的测试用例中,我没有要求输入,但我应该对 STDIN 做点什么。

也就是说,上述方法仍然冻结stream_get_contents(). 我很不确定下一步该做什么/尝试。

学分

Jakob Truelsen 提出了解决方案,并发现了原始问题。4k 小费也是他的主意。在此之前,我对为什么该函数工作正常感到困惑(不知道这完全取决于缓冲区大小)。

4

4 回答 4

5

好吧,似乎一年过去了,忘记了这件事还在等待中!

然而,我在一个不错的 PHP 类中结束了这个混乱,你可以在 Github上找到它。

剩下的主要问题是读取 STDERR 会导致 PHP 脚本阻塞,因此它已被禁用。

从好的方面来说,多亏了事件和一些漂亮的编码(我希望!),人们实际上可以与正在执行的进程进行交互(因此类名,InterExec)。所以你可以在 PHP 中拥有机器人风格的行为。

于 2012-12-17T11:46:33.237 回答
3

您在 stream_select() 的 PHP 手册中错过了这个注释:

当 stream_select() 返回时,数组 read、write 和 except 被修改以指示哪些流资源实际改变了状态。

您需要在每次调用 stream_select() 之前重新创建数组。

根据您打开的过程,这可能是您的示例仍然阻塞的原因。

于 2013-11-06T23:05:15.130 回答
0
while($r = stream_select($read, $write, $except, null, $timeout)){

据我所知,这会将 $r 设置为已更改流的数量,可能为 0,并且循环将不再继续。我会按照 PHP 手册中的描述亲自重新编码:

while(false !== ($r = stream_select($read, $write, $except, null, $timeout))){

就您的 STDIN 而言,如果您的流程不是交互式的,则可能不需要 STDIN。你正在执行的过程是什么?

于 2011-05-18T06:43:40.600 回答
0

挂在 stream_get_contents 中的整个问题在于如何创建流程。正确的方法是用管道的读/写模式打开STDOUT,例如:

$descriptor = array (0 => array ("pipe", "r"), 1 => array ("pipe", "rw"), 2 => array ("pipe", "rw"));
//Open the resource to execute $command
$t->pref = proc_open($command,$descriptor,$t->pipes);
//Set STDOUT and STDERR to non-blocking 
stream_set_blocking ($t->pipes[0], 0);
stream_set_blocking ($t->pipes[1], 0);

很明显,当 stream_get_contents 想要读取 STDOUT 管道时,它需要读取模式。与 hang/freeze/block 相同的错误在这个不错的类中https://gist.github.com/Arbow/982320

然后阻塞消失。但是阅读并不是什么都不读。

于 2013-04-20T12:55:12.207 回答