Создание PHP Online Grading System в Linux: поведение exec, идентификаторы процессов и grep

Фон

Я пишу простой онлайн-судья (система оценки кода) с использованием PHP и MySQL. Он принимает представленные коды на C++ и Java, компилирует их и тестирует их.

Это Apache, использующий PHP 5.2 на старой версии Ubuntu.

Чем я сейчас занимаюсь

У меня есть php-программа, которая зацикливается бесконечно, вызывая другую php-программу

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

каждую десятую секунды. Давайте назовем первый looper.php и второй grade.php, (Контрольно-пропускной пункт: grade.php должен полностью завершить работу до продолжения цикла for, верно?)

grade.php извлекает самый ранний представленный код, который должен быть оценен из базы данных MySQL, помещает этот код в файл (test.[cpp/java]), и вызывает 2 другие программы php по имени compile.php а также test.php, вот так:

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

(Контрольно-пропускной пункт: compile.php должен полностью завершить работу до вызова цикла for test.php даже заводится, правильно?)

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 должен захватывать идентификатор процесса javac в $out[0],

Настоящая проблема

Я хочу прекратить компиляцию, если по какой-то причине компиляция занимает более 10 секунд, и я хочу закончить compile.php если программа перестает компилироваться до 10 секунд. Так как exec("javac... Я назвал выше, это фоновый процесс (или это?), Я не могу знать, когда он завершился, не глядя на идентификатор процесса, который должен был храниться в $out ранее. Сразу после, в compile.phpЯ делаю это с 10-секундным вызовом цикла exec("ps ax | grep [pid].*javac"); и посмотреть, если пид все еще существует:

//...
$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.php начинает работать до javac даже останавливается, в результате чего test.php не в состоянии найти основной класс, когда он пытается запустить программу Java. Я думаю, что цикл по какой-то причине обойден, хотя это может быть и не так. В других случаях вся система оценок работает как задумано.

В то же время, test.php также использует ту же стратегию (с X-секундным циклом и grep) при запуске программы в определенный промежуток времени, и она имеет аналогичную ошибку.

Я думаю, что ошибка заключается в grep не найти javacпид даже когда javac все еще работает, в результате чего 10-секундный цикл прерывается рано. Можете ли вы заметить очевидную ошибку? Более сдержанный баг? Есть ли проблема с моим использованием exec? Есть ли проблема с $out? Или происходит что-то совершенно другое?

Спасибо за чтение моего длинного вопроса. Вся помощь приветствуется.

1 ответ

Решение

Я только что придумал этот код, который будет запускать процесс и завершать его, если он выполняется дольше, чем $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 ()

Другие вопросы по тегам