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

发布于 2025-01-06 19:18:10 字数 2837 浏览 0 评论 0原文

背景

我正在使用 PHP 和 MySQL 编写一个简单的在线法官(代码评分系统)。它接受提交的 C++ 和 Java 代码,对其进行编译并测试。

这是在旧版本 Ubuntu 上运行 PHP 5.2 的 Apache。

我目前正在做的事情

是我有一个无限循环的php程序,每隔

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

十分之一秒调用另一个php程序。我们将第一个命名为 looper.php,将第二个命名为 grade.php。 (检查点:grade.php 应该在“for”循环继续之前完全完成运行,对吗?)

grade.php 从MySQL 数据库,将该代码放入文件 (test.[cpp/java]) 中,并连续调用另外 2 个 php 程序,名为 compile.phptest.php,如下所示:

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

(检查点:compile.php 应该在调用 test.php 的“for”循环开始之前完全完成运行,对吗?)

compile.php然后在 test.[cpp/java] 中编译该程序作为后台进程。现在,我们假设它正在编译一个 Java 程序,并且 test.java 位于子目录中。我现在已经

//...
//$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);
//...

compile.php 中了。它正在重定向 javac 的输出,因此 javac 应该作为后台进程运行......而且看起来它可以工作。 $out 应该获取 $out[0]javac 的进程 ID。

真正的问题是,

如果由于某种原因编译需要超过 10 秒,我想停止编译,如果程序在 10 秒之前停止编译,我想结束 compile.php。由于我上面调用的 exec("javac... 是一个后台进程(或者是吗?),如果不查看进程 ID,我无法知道它何时完成,进程 ID 应该有之前已存储在 $out 中,之后在 compile.php 中,我使用 10 秒的循环调用 exec("ps ax | grep [pid] ].*javac"); 并查看是否pid 仍然存在:

//...
$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);
//...

... 至少在某些时候,发生的情况是 test.phpjavac 之前开始运行。甚至停止,导致 test.php 在尝试运行 java 程序时无法找到主类,我认为循环由于某种原因被绕过,尽管情况可能并非如此。在其他时候,整个评分系统按预期工作

test.php 也使用相同的策略(使用 X 秒循环和 grep)在一定的时间限制内运行程序,并且它有类似的错误,

我认为错误在于 。即使 javac 仍在运行,grep 也找不到 javac 的 pid,导致 10 秒循环提前中断。你能发现一个明显的错误吗?一个更谨慎的错误?我的 exec 使用有问题吗? $out有问题吗?或者正在发生完全不同的事情?

感谢您阅读我的长问题。感谢所有帮助。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

过气美图社 2025-01-13 19:18:10

我刚刚想出了这段代码,它将运行一个进程,并在运行时间超过 $timeout 秒时终止它。如果它在超时之前终止,则会在 $output 中包含程序输出,并在 $return_value 中包含退出状态。

我已经测试过了,看起来效果很好。希望您能根据您的需要进行调整。

<?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\n";
    if ($terminated) echo "Process was terminated due to long execution\n";
} else {
    echo "Failed to start process!\n";
}

参考文献:proc_open()proc_close(), proc_get_status(), proc_terminate()

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\n";
    if ($terminated) echo "Process was terminated due to long execution\n";
} else {
    echo "Failed to start process!\n";
}

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

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文