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


person Christian    schedule 16.05.2011    source источник
comment
Если вы не планируете отправлять какие-либо данные, вам вообще не нужно ничего делать со STDIN. Как и $except, вы можете просто установить $write равным нулю.   -  person Zoredache    schedule 05.08.2011
comment
@Zoredache Но моя проблема не в STDIN, а в STDOUT. Кроме того, это та же самая причина, по которой я сказал Джею (ниже).   -  person Christian    schedule 05.08.2011


Ответы (4)


Ну вроде год прошел и забыл, что это дело все еще впереди!

Однако я завершил этот беспорядок в хорошем PHP-классе, который вы можете найти на Github.

Основная оставшаяся проблема заключается в том, что чтение STDERR вызывает блокировку PHP-скрипта, поэтому он был отключен.

С другой стороны, благодаря событиям и хорошему коду (надеюсь!), можно реально взаимодействовать с выполняемым процессом (отсюда и название класса InterExec). Таким образом, вы можете иметь поведение в стиле бота в PHP.

person Christian    schedule 17.12.2012

Вы пропустили это примечание в руководстве по PHP для stream_select():

Когда функция stream_select() возвращает значение, массивы чтения, записи и исключения модифицируются, чтобы указать, какие потоковые ресурсы фактически изменили свое состояние.

Вам нужно заново создавать массивы перед вызовом stream_select() каждый раз.

В зависимости от процесса, который вы открываете, это может быть причиной того, что ваш пример все еще блокируется.

person David Anderson    schedule 06.11.2013
comment
Я проверю это, как только у меня будет время, но это действительно правдоподобное объяснение. Спасибо! - person Christian; 07.11.2013

while($r = stream_select($read, $write, $except, null, $timeout)){

Насколько я знаю, это установит $r равным количеству измененных потоков, которое может быть равно 0, и цикл больше не будет продолжаться. Я бы лично перекодировал это, как описано в руководстве по PHP:

while(false !== ($r = stream_select($read, $write, $except, null, $timeout))){

Что касается вашего STDIN, если ваш процесс не является интерактивным, то STDIN может не понадобиться. Какой процесс вы выполняете?

person Jay    schedule 18.05.2011
comment
Это общая функция. Я хотел бы поддерживать STDIN для пользователей, которые могут захотеть использовать STDIN. :). Что касается того, что $r равно 0, для меня это не имеет большого значения. Он по-прежнему зависает в stream_get_contents(). - person Christian; 18.05.2011
comment
Вы пытались увидеть, что он примет? Например, такой процесс, как telnet, вы можете открыть, а затем передать «o», за которым следует новая строка, затем адрес x.x.x.x:xx, а затем новая строка и записать полученный STDOUT и посмотреть, работает ли он :) - person Jay; 19.05.2011
comment
Джей: Приведенный выше тестовый код делает именно это, выполняя стандартный ввод-вывод, как и любая другая программа. - person Christian; 19.05.2011

Вся проблема с зависанием в 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

Затем блокировка исчезает. Но читать ничего не читает.

person OSP    schedule 20.04.2013