用PHP实时输出运行进程

77

我正在尝试在网页上运行一个进程,并实时返回其输出。例如,如果我运行'ping'进程,它应该每次返回一个新的行时更新我的页面(目前,当我使用exec(command, output)时,我被迫使用-c选项并等待进程完成才能在我的网页上看到输出)。这在php中是否可能实现?

我还想知道当有人离开页面时,正确的杀死此类进程的方法是什么。对于'ping'进程,我仍然能够在系统监视器中看到进程正在运行(这是有意义的)。


1
Nginx X-Accel缓冲可能会阻止接受的答案起作用,为了使其工作,在此之前添加以下标题:header('X-Accel-Buffering: no'); - Nylls
对我来说,Nginx缓冲是问题所在,上面的评论解决了我的问题。 - Aryaman Gupta
12个回答

104

这对我有用:

$cmd = "ping 127.0.0.1";

$descriptorspec = array(
   0 => array("pipe", "r"),   // stdin is a pipe that the child will read from
   1 => array("pipe", "w"),   // stdout is a pipe that the child will write to
   2 => array("pipe", "w")    // stderr is a pipe that the child will write to
);
flush();
$process = proc_open($cmd, $descriptorspec, $pipes, realpath('./'), array());
echo "<pre>";
if (is_resource($process)) {
    while ($s = fgets($pipes[1])) {
        print $s;
        flush();
    }
}
echo "</pre>";

21
对我来说"+1适用于我",但仅当我在开头添加"ob_implicit_flush(true);ob_end_flush();"时。 - tetris11
2
将这个与Robb的答案结合起来,你就可以开始了。 - Petah
一个很好的例子是在 proc_open() PHP 手册页面 上。 - sobi3ch
PHPCI项目源代码 - sobi3ch
在调用Python脚本时,请使用“-u”以便不缓冲输出。 - Craig Francis
显示剩余3条评论

39

这是一个不错的方法,可以显示您的 shell 命令的实时输出:

<?php
header("Content-type: text/plain");

// tell php to automatically flush after every output
// including lines of output produced by shell commands
disable_ob();

$command = 'rsync -avz /your/directory1 /your/directory2';
system($command);
你需要这个函数来防止输出缓冲:
function disable_ob() {
    // Turn off output buffering
    ini_set('output_buffering', 'off');
    // Turn off PHP output compression
    ini_set('zlib.output_compression', false);
    // Implicitly flush the buffer(s)
    ini_set('implicit_flush', true);
    ob_implicit_flush(true);
    // Clear, and turn off output buffering
    while (ob_get_level() > 0) {
        // Get the curent level
        $level = ob_get_level();
        // End the buffering
        ob_end_clean();
        // If the current level has not changed, abort
        if (ob_get_level() == $level) break;
    }
    // Disable apache output buffering/compression
    if (function_exists('apache_setenv')) {
        apache_setenv('no-gzip', '1');
        apache_setenv('dont-vary', '1');
    }
}

不过,它在我尝试的每台服务器上都不起作用,我希望能够提供一些关于在您的php配置中查找什么来确定是否应该在服务器上尝试这种类型行为时绝望地抓狂的建议!还有其他人知道吗?

以下是一个简单的PHP示例:

<?php
header("Content-type: text/plain");

disable_ob();

for($i=0;$i<10;$i++) 
{
    echo $i . "\n";
    usleep(300000);
}

我希望这篇内容能帮助那些通过谷歌搜索到这里的人。


1
这个不能用于 exec()。但是 system() 可以正常工作。 - divinedragon
这对我非常有效,它不会经常更新,但是会不时地进行更新。虽然不是最好的,但总比没有好。谢谢。 - Exoon
这对我在CentOS 6.5 / apache 2.2 / php 5.3上产生了预期的效果。干杯! - mxmader
由于浏览器通常会等待一些初始数据包,因此您可以在开头发送大量的空格,以便填充初始缓冲区:for ($i=0; $i < 100000; $i++) { echo " "; } echo "\n"; flush(); - Martin Pecka
这个在我的一台机器上可以运行,但是在我的另一台服务器上出现了这个错误:“警告:ini_set():无法更改zlib.output_compression - headers already sent in /var/www/html/index.php on line 16”,它指向这一行:“ini_set('zlib.output_compression', false);”。有什么帮助吗? - Vlad
显示剩余2条评论

13

检查了所有答案,都不起作用...

在这里找到了解决方案

它适用于Windows(我认为这个答案对在那里搜索的用户有帮助)

<?php
    $a = popen('ping www.google.com', 'r'); 
    
    while($b = fgets($a, 2048)) { 
        echo $b."<br>\n"; 
        ob_flush();flush(); 
    }

    pclose($a); 
?>

1
这是我运行Python脚本时有效的方法,其他方法效果不佳。 - J. Martin

7

IE/Edge不支持此功能... ;) - ed22

6

对于命令行使用:

function execute($cmd) {
    $proc = proc_open($cmd, [['pipe','r'],['pipe','w'],['pipe','w']], $pipes);
    while(($line = fgets($pipes[1])) !== false) {
        fwrite(STDOUT,$line);
    }
    while(($line = fgets($pipes[2])) !== false) {
        fwrite(STDERR,$line);
    }
    fclose($pipes[0]);
    fclose($pipes[1]);
    fclose($pipes[2]);
    return proc_close($proc);
}

如果您想运行一个文件,您可能需要先给它执行权限:
chmod('/path/to/script',0755);

5
尝试以下操作(已在Windows机器和WAMP服务器上测试):
        header('Content-Encoding: none;');

        set_time_limit(0);

        $handle = popen("<<< Your Shell Command >>>", "r");

        if (ob_get_level() == 0) 
            ob_start();

        while(!feof($handle)) {

            $buffer = fgets($handle);
            $buffer = trim(htmlspecialchars($buffer));

            echo $buffer . "<br />";
            echo str_pad('', 4096);    

            ob_flush();
            flush();
            sleep(1);
        }

        pclose($handle);
        ob_end_flush();

2
这个答案是错误的。问题是如何实时获取页面输出。使用ob_start()会产生相反的效果:你将所有的输出写入到一个内部服务器缓冲区中,而不是立即发送到浏览器! - Elmue
1
我已经尝试了你的代码,实际上它并没有实时显示shell命令,而是等待shell命令完成加载所有文本,并使用sleep(1)逐位显示。抱歉,你应该被点踩。 - Ryan Arief

5
我曾尝试在Windows上使用各种PHP执行命令,结果发现它们存在很大差异。以下是它们的表现:
  • shell_execexecpassthru:不支持流式传输。
  • proc_openpopen:“有点”可行,因为无法向命令传递参数(例如 my.exe --something 无法运行,但可以使用 _my_something.bat)。
最好(也是最简单)的方法如下:
  1. 确保你的exe正在刷新命令(参考printf flushing problem)。否则,无论你做什么,最终都会收到大约4096个字节的文本批处理。
  2. 如果可能,请使用 header('Content-Type: text/event-stream');(而非 header('Content-Type: text/plain; charset=...');)。然而,这并不能在所有浏览器/客户端上实现!即使没有此设置,流式传输仍将起作用,不过第一行将被浏览器缓冲。
  3. 你还可能需要禁用缓存:header('Cache-Control: no-cache');
  4. 关闭输出缓冲(在php.ini或使用ini_set('output_buffering', 'off');)。这也可能需要在Apache/Nginx/等用于前端的服务器中进行设置。
  5. 关闭压缩(在php.ini或使用 ini_set('zlib.output_compression', false);)。这也可能需要在Apache/Nginx/等用于前端的服务器中进行设置。
因此,在你的C++程序中,你可以这样做(同样,有其他解决方案,请参考printf flushing problem):
Logger::log(...) {
  printf (text);
  fflush(stdout);
}

在PHP中,您可以执行以下操作:

function setupStreaming() {
    // Turn off output buffering
    ini_set('output_buffering', 'off');
    // Turn off PHP output compression
    ini_set('zlib.output_compression', false);
    // Disable Apache output buffering/compression
    if (function_exists('apache_setenv')) {
        apache_setenv('no-gzip', '1');
        apache_setenv('dont-vary', '1');
    }
}
function runStreamingCommand($cmd){
    echo "\nrunning $cmd\n";
    system($cmd);
}
...

setupStreaming();
runStreamingCommand($cmd);

3
如果您想通过PHP运行系统命令,请查看exec文档
但是我不建议在高流量站点上执行此操作,每个请求都要派生一个进程是相当繁重的过程。一些程序提供了将其进程ID写入文件的选项,以便您可以随时检查和终止该进程,但对于像ping这样的命令,我不确定是否可能,请查看man页面。
您可以更好地通过在远程主机上打开期望监听的端口(例如HTTP的80端口)来服务,这样您就知道用户区和网络上的一切都进行得很顺利。
如果您尝试输出二进制数据,请查看php的header函数,并确保设置正确的content-typecontent-disposition。请查看文档,了解有关使用/禁用输出缓冲区的更多信息。

3

首先检查一下flush()是否适合您的情况。如果可以,那就太好了。如果不行,那可能是因为Web服务器出于某种原因进行了缓冲,例如启用了mod_gzip。

对于诸如ping之类的东西,最简单的技术是在PHP内循环运行"ping -c 1"多次,并在每次输出后调用flush()。假设PHP配置为在用户关闭HTTP连接时中止(这通常是默认设置,或者您可以调用ignore_user_abort(false)确保),那么您也不必担心跑偏的ping进程。

如果真的有必要只运行一次子进程并连续显示其输出,则可能更困难--您可能需要在后台运行它,将输出重定向到流中,然后让PHP将该流回显给用户,并穿插使用常规的flush()调用。


1

我曾经遇到过同样的问题,但只能使用Symfony进程组件(https://symfony.com/doc/current/components/process.html)来解决。

以下是一个快速示例:

<?php 

use Symfony\Component\Process\Process;

$process = new Process(['ls', '-lsa']);
$process->run(function ($type, $buffer) {
    if (Process::ERR === $type) {
        echo 'ERR > '.$buffer;
    } else {
        echo 'OUT > '.$buffer;
    }
});

?>

这使事情变得非常容易。10/10 - Joel Mellon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接