dongwu1992 2012-02-20 04:38
浏览 66
已采纳

在Linux上创建PHP在线评分系统:exec行为,进程ID和grep

Background

I am writing a simple online judge (a code grading system) using PHP and MySQL. It takes submitted codes in C++ and Java, compiles them, and tests them.

This is Apache running PHP 5.2 on an old version of Ubuntu.

What I am currently doing

I have a php program that loops infinitely, calling another php program by

//for(infinity)
    exec("php -f grade.php");
//...

every tenth of a second. Let's call the first one looper.php and the second one grade.php. (Checkpoint: grade.php should completely finish running before the "for" loop continues, correct?)

grade.php pulls the earliest submitted code that needs to be graded from the MySQL database, puts that code in a file (test.[cpp/java]), and calls 2 other php programs in succession, named compile.php and test.php, like so:

//...
exec("php -f compile.php");
//...
//for([all tests])
    exec("php -f test.php");
//...

(Checkpoint: compile.php should completely finish running before the "for" loop calling test.php even starts, correct?)

compile.php then compiles the program in test.[cpp/java] as a background process. For now, let's assume that it's compiling a Java program and that test.java is located in a subdirectory. I now have

//...
//$dir = "./sub/" or some other subdirectory; this may be an absolute path
$start_time = microtime(true); //to get elapsed compilation time later
exec("javac ".$dir."test.java -d ".$dir." 2> ".$dir
        ."compileError.txt 1> ".$dir."compileText.txt & echo $!", $out);
//...

in compile.php. It's redirecting the output from javac, so javac should be running as a background process... and it seems like it works. The $out should be grabbing the process id of javac in $out[0].

The real problem

I want to stop compiling if for some reason compiling takes more than 10 seconds, and I want to end compile.php if the program stops compiling before 10 seconds. Since the exec("javac... I called above is a background process (or is it?), I have no way of knowing when it has completed without looking at the process id, which should have been stored in $out earlier. Right after, in compile.php, I do this with a 10 second loop calling exec("ps ax | grep [pid].*javac"); and seeing if the pid still exists:

//...
$pid = (int)$out[0];
$done_compile = false;

while((microtime(true) - $start_time < 10) && !$done_compile) {

    usleep(20000); // only sleep 0.02 seconds between checks
    unset($grep);
    exec("ps ax | grep ".$pid.".*javac", $grep);

    $found_process = false;

    //loop through the results from grep
    while(!$found_process && list(, $proc) = each($grep)) {
        $boom = explode(" ", $proc);
        $npid = (int)$boom[0];

        if($npid == $pid)
            $found_process = true;
    }
    $done_compile = !$found_process;
}

if(!done_compile)
    exec("kill -9 ".$pid);
//...

... which doesn't seem to be working. At least some of the time. Often, what happens is test.php starts running before the javac even stops, resulting in test.php not being able to find the main class when it tries to run the java program. I think that the loop is bypassed for some reason, though this may not be the case. At other times, the entire grading system works as intended.

Meanwhile, test.php also uses the same strategy (with the X-second loop and the grep) in running a program in a certain time limit, and it has a similar bug.

I think the bug lies in the grep not finding javac's pid even when javac is still running, resulting in the 10 second loop breaking early. Can you spot an obvious bug? A more discreet bug? Is there a problem with my usage of exec? Is there a problem with $out? Or is something entirely different happening?

Thank you for reading my long question. All help is appreciated.

  • 写回答

1条回答 默认 最新

  • doufen1890 2012-02-20 07:00
    关注

    I just came up with this code that will run a process, and terminate it if it runs longer than $timeout seconds. If it terminates before the timeout, it will have the program output in $output and the exit status in $return_value.

    I have tested it and it seems to work well. Hopefully you can adapt it to your needs.

    <?php
    
    $command = 'echo Hello; sleep 30'; // the command to execute
    $timeout = 5; // terminate process if it goes longer than this time in seconds
    
    $cwd = '/tmp';  // working directory of executing process
    $env = null;    // environment variables to set, null to use same as PHP
    
    $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("file", "/tmp/error-output.txt", "a") // stderr is a file to write to
    );
    
    // start the process
    $process    = proc_open($command, $descriptorspec, $pipes, $cwd, $env);
    $startTime  = time();
    $terminated = false;
    $output     = '';
    
    if (is_resource($process)) {
        // process was started
        // $pipes now looks like this:
        // 0 => writeable handle connected to child stdin
        // 1 => readable handle connected to child stdout
        // Any error output will be appended to /tmp/error-output.txt
    
        // loop infinitely until timeout, or process finishes
        for(;;) {
            usleep(100000); // dont consume too many resources
    
            $stat = proc_get_status($process); // get info on process
    
            if ($stat['running']) { // still running
                if (time() - $startTime > $timeout) { // check for timeout
                    // close descriptors
                    fclose($pipes[1]);
                    fclose($pipes[0]);
                    proc_terminate($process); // terminate process
                    $return_value = proc_close($process); // get return value
                    $terminated   = true;
                    break;
                }
            } else {
                // process finished before timeout
                $output = stream_get_contents($pipes[1]); // get output of command
                // close descriptors
                fclose($pipes[1]);
                fclose($pipes[0]);
    
                proc_close($process); // close process
                $return_value = $stat['exitcode']; // set exit code
                break;
            }
        }
    
        if (!$terminated) {
            echo $output;
        }
    
        echo "command returned $return_value
    ";
        if ($terminated) echo "Process was terminated due to long execution
    ";
    } else {
        echo "Failed to start process!
    ";
    }
    

    References: proc_open(), proc_close(), proc_get_status(), proc_terminate()

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

悬赏问题

  • ¥15 微信公众号自制会员卡没有收款渠道啊
  • ¥15 stable diffusion
  • ¥100 Jenkins自动化部署—悬赏100元
  • ¥15 关于#python#的问题:求帮写python代码
  • ¥20 MATLAB画图图形出现上下震荡的线条
  • ¥15 关于#windows#的问题:怎么用WIN 11系统的电脑 克隆WIN NT3.51-4.0系统的硬盘
  • ¥15 perl MISA分析p3_in脚本出错
  • ¥15 k8s部署jupyterlab,jupyterlab保存不了文件
  • ¥15 ubuntu虚拟机打包apk错误
  • ¥199 rust编程架构设计的方案 有偿