Создание 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 ()