【问题标题】:Reading from STDIN pipe when using proc_open使用 proc_open 时从 STDIN 管道读取
【发布时间】:2013-04-27 09:47:28
【问题描述】:

我正在尝试建立一个人们可以在线编译和运行代码的网站,因此我们需要找到一种交互方式让用户发送指令。

其实首先想到的是exec()或者system(),但是当用户想输入某事时,这种方式是行不通的。所以我们必须使用proc_open()

比如下面的代码

int main()
{
    int a;
    printf("please input a integer\n");
    scanf("%d", &a);
    printf("Hello World %d!\n", a);
    return 0;
}

当我使用proc_open()时,像这样

$descriptorspec = array(      
0 => array( 'pipe' , 'r' ) ,  
    1 => array( 'pipe' , 'w' ) ,  
    2 => array( 'file' , 'errors' , 'w' ) 
);  
$run_string = "cd ".$addr_base."; ./a.out 2>&1";
$process = proc_open($run_string, $descriptorspec, $pipes);
if (is_resource($process)) {
    //echo fgets($pipes[1])."<br/>";
    fwrite($pipes[0], '12');
    fclose($pipes[0]);
    while (!feof($pipes[1]))
        echo fgets($pipes[1])."<br/>";
    fclose($pipes[1]);
    proc_close($process);
}

在运行 C 代码时,我想获取第一个 STDOUT 流,并输入数字,然后获取第二个 STDOUT 流。但是,如果我将注释行取消注释,该页面将被阻止。

有没有办法解决这个问题?当并非所有数据都放在那里时,如何从管道中读取?或者有没有更好的方法来编写这种交互式程序?

【问题讨论】:

  • 您的“用户”是否通过网站进行交互?因为通过这种方式,用户似乎无法直接访问您服务器的STDIN
  • @Passerby 用户按下按钮编译,输入x,发送到服务器。但在他输入 x 之前,服务器必须先从STDIN 获取流并将其发送到网站,以便用户知道他应该输入 x。问题是服务器此时无法获取流..

标签: php proc-open


【解决方案1】:

这更像是Cglibc 问题。您必须使用fflush(stdout)

为什么?在终端中运行 a.out 和从 PHP 中调用它有什么区别?

答案:如果您在终端中运行a.out(作为标准输入终端),那么 glibc 将使用行缓冲 IO。但是,如果您从另一个程序(在这种情况下为 PHP)运行它并且它的标准输入是管道(或其他任何但不是 tty),则 glibc 将使用内部 IO 缓冲。这就是为什么如果未注释第一个 fgets() 会阻塞。有关更多信息,请查看此article

好消息:您可以使用stdbuf 命令控制此缓冲。将$run_string 更改为:

$run_string = "cd ".$addr_base.";stdbuf -o0 ./a.out 2>&1";

这里有一个工作示例。即使 C 代码不关心 fflush() 也可以工作,因为它使用 stdbuf 命令:

启动子进程

$cmd = 'stdbuf -o0 ./a.out 2>&1';

// what pipes should be used for STDIN, STDOUT and STDERR of the child
$descriptorspec = array (
    0 => array("pipe", "r"),
    1 => array("pipe", "w"),
    2 => array("pipe", "w")
 );

// open the child
$proc = proc_open (
    $cmd, $descriptorspec, $pipes, getcwd()
);

将所有流设置为非阻塞模式

// set all streams to non blockin mode
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
stream_set_blocking(STDIN, 0);

// check if opening has succeed
if($proc === FALSE){
    throw new Exception('Cannot execute child process');
}

获取子 pid。我们以后需要它

// get PID via get_status call
$status = proc_get_status($proc);
if($status === FALSE) {
    throw new Exception (sprintf(
        'Failed to obtain status information '
    ));
}
$pid = $status['pid'];

轮询直到孩子终止

// now, poll for childs termination
while(true) {
    // detect if the child has terminated - the php way
    $status = proc_get_status($proc);
    // check retval
    if($status === FALSE) {
        throw new Exception ("Failed to obtain status information for $pid");
    }
    if($status['running'] === FALSE) {
        $exitcode = $status['exitcode'];
        $pid = -1;
        echo "child exited with code: $exitcode\n";
        exit($exitcode);
    }

    // read from childs stdout and stderr
    // avoid *forever* blocking through using a time out (50000usec)
    foreach(array(1, 2) as $desc) {
        // check stdout for data
        $read = array($pipes[$desc]);
        $write = NULL;
        $except = NULL;
        $tv = 0;
        $utv = 50000;

        $n = stream_select($read, $write, $except, $tv, $utv);
        if($n > 0) {
            do {
                $data = fread($pipes[$desc], 8092);
                fwrite(STDOUT, $data);
            } while (strlen($data) > 0);
        }
    }


    $read = array(STDIN);
    $n = stream_select($read, $write, $except, $tv, $utv);
    if($n > 0) {
        $input = fread(STDIN, 8092);
        // inpput to program
        fwrite($pipes[0], $input);
    }
}

【讨论】:

  • 谢谢~但是C代码是来自用户的,他只需按Enter编译运行。是否可以更改 test.php 的某些部分以使其工作?
  • 有趣的问题! :) 将对此进行调查。
  • 我想知道这是否适用于 Mac OSX?它显示sh: stdbuf: command not found。我稍后会在 Ubuntu 上尝试!
  • 非常感谢,它成功了!但是我如何判断pipes[1] 中的所有内容是否已被阅读?当使用while(!feof($pipes[1]))时,它再次阻塞..
  • 抱歉,之前没有看到您的评论。使用非阻塞流,请参阅stream_set_blocking()。此外,您还必须使用 proc_get_status() 并检查运行状态。
【解决方案2】:

答案非常简单:将$descriptorspec 留空。如果这样做,子进程将简单地使用父进程的 STDIN/STDOUT/STDERR 流。

➜  ~  ✗ cat stdout_is_atty.php
<?php

var_dump(stream_isatty(STDOUT));
➜  ~  ✗ php -r 'proc_close(proc_open("php stdout_is_atty.php", [], $pipes));'
/home/chx/stdout_is_atty.php:3:
bool(true)
➜  ~  ✗ php -r 'passthru("php stdout_is_atty.php");'
/home/chx/stdout_is_atty.php:3:
bool(false)
➜  ~  ✗ php -r 'exec("php stdout_is_atty.php", $output); print_r($output);'
Array
(
    [0] => /home/chx/stdout_is_atty.php:3:
    [1] => bool(false)
)

这要归功于作曲家的维护者之一约翰·史蒂文森。

如果您对为什么会发生这种情况感兴趣:PHP 对空描述符不做任何事情,而是使用恰好是所需的 C/OS 默认值。

因此,负责proc_open 的 C 代码始终只是迭代描述符。如果没有指定描述符,那么所有代码​​都不会执行任何操作。之后,子进程的实际执行——至少在 POSIX 系统上——通过调用fork(2) 发生,这使得子进程继承文件描述符(参见answer)。然后孩子打电话给execvp(3) / execle(3) / execl(3) 之一。正如manual 所说

exec() 系列函数用新的进程映像替换当前进程映像。

也许说包含父级的内存区域被新程序替换更容易理解。这可以通过/proc/$pid/mem 访问,更多信息请参见answer。但是,系统会记录该区域之外打开的文件。您可以在/proc/$pid/fd/ 中看到它们——而 STDIN/STDOUT/STDERR 只是文件描述符 0/1/2 的简写。所以当孩子替换内存时,文件描述符就留在原地。

【讨论】:

    最近更新 更多