exec() with timeout

2019-01-14 09:08发布

问题:

I'm looking for a way to run a PHP process with a timeout. Currently I'm simply using exec(), but it does not provide a timeout option.

What I also tried is opening the process using proc_open() and using stream_set_timeout() on the resulting pipe, but that didn't work either.

So, is there any way to run a command (a PHP command to be precise) with a timeout? (PS: This is for cases where the max_execution_time limit fails, so no need to suggest that.)

(By the way, I also need to retrieve the return code of the process.)

回答1:

I've searched a bit on this topic and came to conclusion that in some case (if you are using linux) you can use 'timeout' command. It's pretty flexible

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

in my particular case I'm trying to run sphinx indexer from PHP, kinda migration data script so I need to reindex my sphinx documents

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

Then I'm going to analyze output and decide to give it one more try, or throw an exception and quit my script.



回答2:

I found this on php.net that I think can do what you want

<?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); 
} 
?>


回答3:

You could fork() and then exec() in one process and wait() non-blocking in the other. Also keep track of the timeout and kill() the other process if it does not finish in time.



回答4:

The timeout {$time} command solution does not work properly when it's called from a PHP script. In my case, with a ssh command to a wrong server (rsa key not found, and the server ask for a password), the process still alive after the defined timeout.

However I've found a function that works fine here:

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

C&P:

/**
 * 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;
}


回答5:

(Disclaimer: I was surprised to find no good solution for this, then I browsed the proc documentation and found it pretty straight forward. So here is a simple proc answer, that uses native functions in a way that provides consistent results. You can also still catch the output for logging purposes.)

The proc line of functions has proc_terminate ( process-handler ), which combined with proc_get_status ( process-handler ) getting the "running" key, you can while loop with sleep to do a synchronous exec call with a timeout.

So basically:

$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 :(


回答6:

I am facing the same problem that I have tried all the answers above, but windows server cant work with any of these, maybe it is my stupidity.

My final working solution for windows is executing a batch file,

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"

the php command

exec("timeout.bat {$time} your_program.exe");


回答7:

Improving on other solutions I came up with this:

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;
}


标签: php timeout exec