PHP 中正确的 shell 执行

发布于 2024-11-07 03:19:29 字数 2557 浏览 0 评论 0原文

问题是

我使用了一个利用 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提示也是他的主意。在此之前,我很困惑为什么该函数运行良好(不知道这完全取决于缓冲区大小)。

The problem

I was using a function that made use of proc_open() to invoke shell commands. It seems the way I was doing STDIO was wrong and sometimes caused PHP or the target command to lock up. This is the original code:

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) );
}

It works most of the time, but that is not enough, I want to make it work always.

The issue lies in stream_get_contents() locking up if the STDIO buffers exceed 4k of data.

Test Case

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__));
}

We output a string twice to STDERR and STDOUT with the combined length of 4120 bytes (exceeding 4k). This causes PHP to lock up on both sides.

Solution

Apparently, stream_select() is the way to go. I have the following code:

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

    }
}

The only remaining piece of the puzzle is handling STDIN (see the line marked (???)).
I figured out STDIN must be supplied by whatever is calling my function, execute(). But what if I don't want to use STDIN at all? In my testcase, above, I didn't ask for input, yet I'm supposed to do something to STDIN.

That said, the above approach still freezes at stream_get_contents(). I'm quite unsure what to do/try next.

Credits

The solution was suggested by Jakob Truelsen, as well as discovering the original issue. The 4k tip was also his idea. Prior to this I was puzzled as to why the function was working fine (didn't know it all depended on buffer size).

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

风和你 2024-11-14 03:19:29

好吧,好像一年过去​​了,忘了这件事仍然悬而未决!

不过,我将这个混乱的问题封装在一个不错的 PHP 类中,您可以在 Github 上找到该类。

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

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

Well, seems a year passed and forgot this thing is still pending!

However, I wrapped up this mess in a nice PHP class which you can find on Github.

The main remaining problem is that reading STDERR causes the PHP script to block, so it has been disabled.

On the bright side, thanks to events and some nice coding (I hope!), one can actually interact with the process being executed (hence the class name, InterExec). So you can have bot-style behavior in PHP.

朱染 2024-11-14 03:19:29

您错过了 PHP 手册中有关 stream_select() 的注释:

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

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

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

You've missed this note in the PHP manual for stream_select():

When stream_select() returns, the arrays read, write and except are modified to indicate which stream resource(s) actually changed status.

You need to re-create the arrays before calling stream_select() each time.

Depending on the process you're opening, this may be why your example still blocks.

锦爱 2024-11-14 03:19:29
while($r = stream_select($read, $write, $except, null, $timeout)){

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

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

就您的 STDIN 而言,如果您的进程不是交互式的,那么 STDIN 可能不是必需的。您正在执行的流程是什么?

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

As far as I know this will set $r to the number of changed streams, which may be 0 and the loop would no longer continue. I would personally recode this as described in the PHP manual:

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

As far as your STDIN is concerned if your process is not interactive then the STDIN may not be necessary. What is the process you are executing?

深爱不及久伴 2024-11-14 03:19:29

挂在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

然后阻塞消失。但读并不等于什么也没读。

The whole problem with hanging in stream_get_contents is in the way how process is created.
The correct way is to open STDOUT with read/write mode of pipe, eg:

$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);

This is obvious that when stream_get_contents wants to read the STDOUT pipe it needs read mode.
The same bug with hang/freeze/block is in this nice class https://gist.github.com/Arbow/982320

Then blocking disappears. But read does not read nothing.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文