exec() with timeout
Asked Answered
S

7

10

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.)

Spurge answered 23/2, 2012 at 18:39 Comment(1)
start timer. put process in infinite loop, checking the timer, time-out when needed.Logistics
S
29

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.

Spear answered 10/4, 2013 at 9:1 Comment(1)
An elegant solution thank you. Default time is seconds and user can optionally change the unit.Garrett
T
6

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); 
} 
?>
Tootsie answered 23/2, 2012 at 18:48 Comment(1)
This looks like a reasonable approach, but how can one get the return code in this case?Spurge
A
3

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;
}
Altheta answered 8/1, 2014 at 9:54 Comment(2)
I've found there is one problem with this code. Although it is true that calls to stream_get_contents($pipes[1]) will return immediately because that pipe was set to non-blocking, the LAST call which is stream_get_contents($pipes[2]) will actually block until the whole process exits because the stderr pipe was never set to non blocking. This defeats the whole purpose of being able to return control to the caller after the timeout. The solution is simple: Add stream_set_blocking($pipes[2], 0); after the first one aboveQuinone
@juanra: I'm not having any problems using timeout {$time} command in our PHP script. What's the problem you saw? Maybe something like an inadequate max_execution_time was causing problems?Background
C
2

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.

Charmainecharmane answered 23/2, 2012 at 18:45 Comment(2)
Are you referring to the pcntl_ functions here?Spurge
@NikiC: Yes, I think that's what they are called in PHP.Charmainecharmane
O
2

(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 :(
Osric answered 6/5, 2013 at 21:42 Comment(1)
But manual says proc_get_status and proc_terminate works with resource returned by proc_open only, not popen?Utter
H
2

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");
Hardener answered 6/1, 2018 at 7:49 Comment(0)
I
2

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;
}
Interracial answered 5/11, 2018 at 9:13 Comment(1)
for slightly more precision, you could replace time() with microtime(true) and also sleep to some smaller interval like usleep(100000) for 100ms of sleepSubmariner

© 2022 - 2024 — McMap. All rights reserved.