exec() с тайм-аутом

Я ищу способ запустить процесс PHP с тайм-аутом. В настоящее время я просто использую exec(), но он не предоставляет возможность тайм-аута.

Я также пытался открыть процесс с помощью proc_open() и с помощью stream_set_timeout() в полученном канале, но это тоже не сработало.

Итак, есть ли способ запустить команду (точнее, команду PHP) с тайм-аутом? (PS: Это для случаев, когда предел max_execution_time не работает, поэтому нет необходимости предлагать это.)

(Кстати, мне также нужно получить код возврата процесса.)


person NikiC    schedule 23.02.2012    source источник
comment
запустить таймер. поместить процесс в бесконечный цикл, проверить таймер, тайм-аут при необходимости.   -  person    schedule 23.02.2012


Ответы (7)


Я немного поискал по этой теме и пришел к выводу, что в некоторых случаях (если вы используете Linux) вы можете использовать команду «тайм-аут». Это довольно гибко

Usage: timeout [OPTION] DURATION COMMAND [ARG]...
  or:  timeout [OPTION]

в моем конкретном случае я пытаюсь запустить индексатор sphinx из PHP, вроде скрипта данных миграции, поэтому мне нужно переиндексировать мои документы sphinx

exec("timeout {$time} indexer --rotate --all", $output);

Затем я проанализирую вывод и решу сделать еще одну попытку или создать исключение и выйти из сценария.

person Davyd Dzhahaiev    schedule 10.04.2013
comment
Отлично работает :) - person Sunny Patial; 14.07.2017

Я нашел это на php.net, и я думаю, что он может делать то, что вы хотите

<?php 
function PsExecute($command, $timeout = 60, $sleep = 2) { 
    // First, execute the process, get the process ID 

    $pid = PsExec($command); 

    if( $pid === false ) 
        return false; 

    $cur = 0; 
    // Second, loop for $timeout seconds checking if process is running 
    while( $cur < $timeout ) { 
        sleep($sleep); 
        $cur += $sleep; 
        // If process is no longer running, return true; 

       echo "\n ---- $cur ------ \n"; 

        if( !PsExists($pid) ) 
            return true; // Process must have exited, success! 
    } 

    // If process is still running after timeout, kill the process and return false 
    PsKill($pid); 
    return false; 
} 

function PsExec($commandJob) { 

    $command = $commandJob.' > /dev/null 2>&1 & echo $!'; 
    exec($command ,$op); 
    $pid = (int)$op[0]; 

    if($pid!="") return $pid; 

    return false; 
} 

function PsExists($pid) { 

    exec("ps ax | grep $pid 2>&1", $output); 

    while( list(,$row) = each($output) ) { 

            $row_array = explode(" ", $row); 
            $check_pid = $row_array[0]; 

            if($pid == $check_pid) { 
                    return true; 
            } 

    } 

    return false; 
} 

function PsKill($pid) { 
    exec("kill -9 $pid", $output); 
} 
?>
person romo    schedule 23.02.2012
comment
Это выглядит разумным подходом, но как в этом случае получить код возврата? - person NikiC; 24.02.2012

Вы можете fork(), а затем exec() в одном процессе и wait() неблокировать в другом. Также следите за тайм-аутом и kill() другим процессом, если он не завершится вовремя.

person Florian    schedule 23.02.2012
comment
Вы имеете в виду функции pcntl_ здесь? - person NikiC; 23.02.2012
comment
@NikiC: Да, я думаю, так они называются в PHP. - person Florian; 23.02.2012

(Отказ от ответственности: я был удивлен, не найдя хорошего решения для этого, затем я просмотрел документацию proc и нашел ее довольно прямолинейной. Итак, вот простой ответ proc, который использует собственные функции таким образом, чтобы обеспечить согласованные результаты. Вы также можете по-прежнему ловите вывод для целей ведения журнала.)

Строка функций proc имеет proc_terminate ( process-handler ), которая в сочетании с proc_get_status ( process-handler ) получает «работающий» ключ, вы можете выполнять цикл со сном, чтобы выполнить синхронный вызов exec с тайм-аутом.

Итак, в основном:

$ps = popen('cmd');
$timeout = 5; //5 seconds
$starttime = time();
while(time() < $starttime + $timeout) //until the current time is greater than our start time, plus the timeout
{
    $status = proc_get_status($ps);
    if($status['running'])
        sleep(1);
    else
        return true; //command completed :)
}

proc_terminate($ps);
return false; //command timed out :(
person Jason McCarrell    schedule 06.05.2013
comment
Но в руководстве сказано, что proc_get_status и proc_terminate работают только с ресурсом, возвращаемым proc_open, а не с popen? - person Yuri Gor; 05.06.2014

Решение timeout {$time} command не работает должным образом, когда оно вызывается из скрипта PHP. В моем случае с командой ssh ​​на неправильный сервер (ключ rsa не найден, и сервер запрашивает пароль), процесс все еще жив после определенного тайм-аута.

Однако я нашел функцию, которая здесь отлично работает:

http://blog.dubbelboer.com/2012/08/24/execute-with-timeout.html

К&П:

/**
 * Execute a command and return it's output. Either wait until the command exits or the timeout has expired.
 *
 * @param string $cmd     Command to execute.
 * @param number $timeout Timeout in seconds.
 * @return string Output of the command.
 * @throws \Exception
 */
function exec_timeout($cmd, $timeout) {
  // File descriptors passed to the process.
  $descriptors = array(
    0 => array('pipe', 'r'),  // stdin
    1 => array('pipe', 'w'),  // stdout
    2 => array('pipe', 'w')   // stderr
  );

  // Start the process.
  $process = proc_open('exec ' . $cmd, $descriptors, $pipes);

  if (!is_resource($process)) {
    throw new \Exception('Could not execute process');
  }

  // Set the stdout stream to none-blocking.
  stream_set_blocking($pipes[1], 0);

  // Turn the timeout into microseconds.
  $timeout = $timeout * 1000000;

  // Output buffer.
  $buffer = '';

  // While we have time to wait.
  while ($timeout > 0) {
    $start = microtime(true);

    // Wait until we have output or the timer expired.
    $read  = array($pipes[1]);
    $other = array();
    stream_select($read, $other, $other, 0, $timeout);

    // Get the status of the process.
    // Do this before we read from the stream,
    // this way we can't lose the last bit of output if the process dies between these     functions.
    $status = proc_get_status($process);

    // Read the contents from the buffer.
    // This function will always return immediately as the stream is none-blocking.
    $buffer .= stream_get_contents($pipes[1]);

    if (!$status['running']) {
      // Break from this loop if the process exited before the timeout.
      break;
    }

    // Subtract the number of microseconds that we waited.
    $timeout -= (microtime(true) - $start) * 1000000;
  }

  // Check if there were any errors.
  $errors = stream_get_contents($pipes[2]);

  if (!empty($errors)) {
    throw new \Exception($errors);
  }

  // Kill the process in case the timeout expired and it's still running.
  // If the process already exited this won't do anything.
  proc_terminate($process, 9);

  // Close all streams.
  fclose($pipes[0]);
  fclose($pipes[1]);
  fclose($pipes[2]);

  proc_close($process);

  return $buffer;
}
person juanra    schedule 08.01.2014
comment
Я обнаружил, что с этим кодом есть одна проблема. Хотя верно, что вызовы stream_get_contents($pipes[1]) вернутся немедленно, потому что этот канал был настроен на неблокирование, ПОСЛЕДНИЙ вызов, который имеет значение stream_get_contents($pipes[2]), фактически будет блокироваться до тех пор, пока весь процесс не завершится, потому что канал stderr никогда не был настроен на неблокирование. Это лишает возможности вернуть управление вызывающей стороне после тайм-аута. Решение простое: добавьте stream_set_blocking($pipes[2], 0); после первого выше. - person Danny; 14.01.2019
comment
@juanra: у меня нет проблем с использованием timeout {$time} command в нашем PHP-скрипте. В чем проблема, которую вы видели? Может быть, что-то вроде неадекватного max_execution_time вызывало проблемы? - person rinogo; 25.02.2021

Я столкнулся с той же проблемой, что и все ответы выше, но сервер Windows не может работать ни с одним из них, возможно, это моя глупость.

Моим последним рабочим решением для Windows является выполнение командного файла,

timeout.bat

::param 1 is timeout seconds, param 2 is executable
echo "running %2 with timeout %1"
start %2
set time=0

:check
tasklist /FI "IMAGENAME eq %2" 2>NUL | find /I /N "%2">NUL
::time limit exceed
if "%time%"=="%1" goto kill
::program is running
if "%ERRORLEVEL%"=="0" ( ping 127.0.0.1 -n 2 >nul & set /a time=%time%+1 & goto check) else ( goto end)

:kill
echo "terminate"
taskkill /im %2 /f

:end
echo "end"

php-команда

exec("timeout.bat {$time} your_program.exe");
person Dipsy    schedule 06.01.2018

Улучшая другие решения, я придумал следующее:

function exec_timeout($cmd,$timeout=60){
        $start=time();
        $outfile=uniqid('/tmp/out',1);
        $pid=trim(shell_exec("$cmd >$outfile 2>&1 & echo $!"));
        if(empty($pid)) return false;
        while(1){
                if(time()-$start>$timeout){
                        exec("kill -9 $pid",$null);
                        break;
                }
                $exists=trim(shell_exec("ps -p $pid -o pid="));
                if(empty($exists)) break;
                sleep(1);
        }
        $output=file_get_contents($outfile);
        unlink($outfile);
        return $output;
}
person dw1    schedule 05.11.2018
comment
для немного большей точности вы можете заменить time () на microtime (true), а также засыпать на некоторый меньший интервал, например usleep (100000) на 100 мс сна - person Phil Glau; 06.11.2018