Правильное выполнение оболочки в PHP
Эта проблема
Я использовал функцию, которая использовала proc_open()
вызывать команды оболочки. Кажется, что я делал STDIO неправильно, иногда это приводило к блокировке PHP или целевой команды. Это оригинальный код:
function execute($cmd, $stdin=null){
$proc=proc_open($cmd,array(0=>array('pipe','r'),1=>array('pipe','w'),2=>array('pipe','w')),$pipes);
fwrite($pipes[0],$stdin); fclose($pipes[0]);
$stdout=stream_get_contents($pipes[1]); fclose($pipes[1]);
$stderr=stream_get_contents($pipes[2]); fclose($pipes[2]);
return array( 'stdout'=>$stdout, 'stderr'=>$stderr, 'return'=>proc_close($proc) );
}
Это работает большую часть времени, но этого недостаточно, я хочу, чтобы это работало всегда.
Вопрос заключается в stream_get_contents()
блокировка, если буферы STDIO превышают 4 КБ данных.
Прецедент
function out($data){
file_put_contents('php://stdout',$data);
}
function err($data){
file_put_contents('php://stderr',$data);
}
if(isset($argc)){
// RUN CLI TESTCASE
out(str_repeat('o',1030);
err(str_repeat('e',1030);
out(str_repeat('O',1030);
err(str_repeat('E',1030);
die(128); // to test return error code
}else{
// RUN EXECUTION TEST CASE
$res=execute('php -f '.escapeshellarg(__FILE__));
}
Мы выводим строку дважды в STDERR и STDOUT с общей длиной 4120 байт (более 4k). Это заставляет PHP блокироваться с обеих сторон.
Решение
По-видимому, stream_select()
это путь У меня есть следующий код:
function execute($cmd,$stdin=null,$timeout=20000){
$proc=proc_open($cmd,array(0=>array('pipe','r'),1=>array('pipe','w'),2=>array('pipe','w')),$pipes);
$write = array($pipes[0]);
$read = array($pipes[1], $pipes[2]);
$except = null;
$stdout = '';
$stderr = '';
while($r = stream_select($read, $write, $except, null, $timeout)){
foreach($read as $stream){
// handle STDOUT
if($stream===$pipes[1])
/*...*/ $stdout.=stream_get_contents($stream);
// handle STDERR
if($stream===$pipes[2])
/*...*/ $stderr.=stream_get_contents($stream);
}
// Handle STDIN (???)
if(isset($write[0])) ;
// the following code is temporary
$n=isset($n) ? $n+1 : 0; if($n>10)break; // break while loop after 10 iterations
}
}
Единственным оставшимся кусочком головоломки является обработка STDIN (см. Строку, помеченную (???)
). Я понял, что STDIN должен быть получен от того, что вызывает мою функцию, execute()
, Но что, если я вообще не хочу использовать STDIN? В моем тестовом примере выше, я не просил ввода, но я должен что-то сделать с STDIN.
Тем не менее, вышеупомянутый подход все еще зависает в stream_get_contents()
, Я совершенно не уверен, что делать / попробовать дальше.
кредиты
Решение было предложено Якобом Труэльсеном, а также обнаружил оригинальную проблему. Совет 4k был также его идеей. До этого я был озадачен, почему функция работала нормально (не знал, что все зависит от размера буфера).
4 ответа
Ну, кажется, прошел год и забыл, что эта вещь еще в ожидании!
Тем не менее, я обернул этот беспорядок в хороший класс PHP, который вы можете найти на Github.
Основная остающаяся проблема заключается в том, что чтение STDERR приводит к блокировке сценария PHP, поэтому он был отключен.
С другой стороны, благодаря событиям и некоторому хорошему кодированию (я надеюсь!), Можно реально взаимодействовать с исполняемым процессом (отсюда и название класса, InterExec
). Таким образом, вы можете иметь поведение в стиле бота в PHP.
Вы пропустили это примечание в руководстве по PHP для stream_select():
Когда stream_select () возвращается, массивы read, write и кроме изменяются, чтобы указать, какой ресурс (ы) потока фактически изменил статус.
Вам нужно заново создавать массивы перед каждым вызовом stream_select ().
В зависимости от процесса, который вы открываете, возможно, ваш пример все еще блокируется.
while($r = stream_select($read, $write, $except, null, $timeout)){
Насколько я знаю, это установит $ r в число измененных потоков, которое может быть 0, и цикл больше не будет продолжаться. Я бы лично перекодировал это, как описано в руководстве по PHP:
while(false !== ($r = stream_select($read, $write, $except, null, $timeout))){
Что касается вашего STDIN, если ваш процесс не является интерактивным, тогда STDIN может не понадобиться. Какой процесс вы выполняете?
Вся проблема с зависанием в stream_get_contents заключается в том, как создается процесс. Правильный способ - открыть STDOUT с режимом чтения / записи канала, например:
$descriptor = array (0 => array ("pipe", "r"), 1 => array ("pipe", "rw"), 2 => array ("pipe", "rw"));
//Open the resource to execute $command
$t->pref = proc_open($command,$descriptor,$t->pipes);
//Set STDOUT and STDERR to non-blocking
stream_set_blocking ($t->pipes[0], 0);
stream_set_blocking ($t->pipes[1], 0);
Это очевидно, что когда stream_get_contents хочет прочитать канал STDOUT, ему нужен режим чтения. Та же самая ошибка с зависанием / заморозкой / блокировкой находится в этом хорошем классе https://gist.github.com/Arbow/982320
Тогда блокировка исчезает. Но читать не читает ничего.