我正在尝试创建一个人们可以在线编译和运行代码的网站,因此我们需要找到一种用户发送指令的交互方式。
实际上,首先想到的是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流。但如果我将注释行取消注释,该页面将被阻止。
有没有办法解决这个问题?如何在没有放入所有数据的情况下从管道中读取数据?或者有更好的方法来编写这种交互式程序吗?
答案 0 :(得分:19)
这更多是C
或glibc
问题。您必须使用fflush(stdout)
。
为什么呢?在终端中运行a.out
和从PHP调用它是什么区别?
答案:如果你在终端中运行a.out
(正在使用stdin),那么glibc将使用行缓冲IO。但是如果你从另一个程序运行它(在这种情况下为PHP)并且它的stdin是一个管道(或者其他什么但不是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);
}
}
答案 1 :(得分:0)
答案出奇的简单:将 $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)
)
感谢 John Stevenson,他是 Composer 的维护者之一。
如果您对为什么会发生这种情况感兴趣: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 的简写。所以当孩子替换内存时,文件描述符就留在原地。