I am trying to make a website where people can compile and run their code online, thus we need to find an interactive way for users to send instructions.
Actually, what first comes to mind is exec()
or system()
, but when users want to input sth, this way won’t work. So we have to use proc_open()
.
For instance, the following code
int main() { int a; printf("please input a integern"); scanf("%d", &a); printf("Hello World %d!n", a); return 0; }
When I used proc_open()
, like this
$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); }
When running the C code, I want to get the first STDOUT stream, and input the number, then get the second STDOUT stream. But if I have the commented line uncommented, the page will be blocked.
Is there a way to solve the problem? How can I read from the pipe while not all data has been put there? Or is there a better way to write this kind of interactive program?
Advertisement
Answer
It is more a C
or a glibc
problem. You’ll have to use fflush(stdout)
.
Why? And what’s the difference between running a.out
in a terminal and calling it from PHP?
Answer: If you run a.out
in a terminal (being stdin a tty) then the glibc will use line buffered IO. But if you run it from another program (PHP in this case) and it’s stdin is a pipe (or whatever but not a tty) than the glibc will use internal IO buffering. That’s why the first fgets()
blocks if uncommented. For more info check this article.
Good news: You can control this buffering using the stdbuf
command. Change $run_string
to:
$run_string = "cd ".$addr_base.";stdbuf -o0 ./a.out 2>&1";
Here comes a working example. Working even if the C code don’t cares about fflush()
as it is using the stdbuf
command:
Starting subprocess
$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 blocking mode
// 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'); }
get child pid. we need it later
// 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'];
poll until child terminates
// 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: $exitcoden"; 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); } }