Fix Process timeout

This commit is contained in:
Romain Neutron 2013-04-06 20:51:55 +02:00
parent 7221d25fc4
commit 3780fdb214
3 changed files with 80 additions and 11 deletions

View File

@ -34,10 +34,14 @@ class Process
const STDOUT = 1; const STDOUT = 1;
const STDERR = 2; const STDERR = 2;
// Timeout Precision in seconds.
CONST TIMEOUT_PRECISION = 0.2;
private $commandline; private $commandline;
private $cwd; private $cwd;
private $env; private $env;
private $stdin; private $stdin;
private $starttime;
private $timeout; private $timeout;
private $options; private $options;
private $exitcode; private $exitcode;
@ -211,6 +215,7 @@ class Process
throw new \RuntimeException('Process is already running'); throw new \RuntimeException('Process is already running');
} }
$this->starttime = microtime(true);
$this->stdout = ''; $this->stdout = '';
$this->stderr = ''; $this->stderr = '';
$callback = $this->buildCallback($callback); $callback = $this->buildCallback($callback);
@ -285,7 +290,7 @@ class Process
$w = $writePipes; $w = $writePipes;
$e = null; $e = null;
$n = @stream_select($r, $w, $e, $this->timeout); $n = @stream_select($r, $w, $e, 0, static::TIMEOUT_PRECISION * 1E6);
if (false === $n) { if (false === $n) {
break; break;
@ -318,6 +323,8 @@ class Process
unset($this->pipes[$type]); unset($this->pipes[$type]);
} }
} }
$this->checkTimeout();
} }
$this->updateStatus(); $this->updateStatus();
@ -345,13 +352,15 @@ class Process
if (defined('PHP_WINDOWS_VERSION_BUILD') && $this->fileHandles) { if (defined('PHP_WINDOWS_VERSION_BUILD') && $this->fileHandles) {
$this->processFileHandles($callback, !$this->pipes); $this->processFileHandles($callback, !$this->pipes);
} }
$this->checkTimeout();
if ($this->pipes) { if ($this->pipes) {
$r = $this->pipes; $r = $this->pipes;
$w = null; $w = null;
$e = null; $e = null;
if (false === $n = @stream_select($r, $w, $e, $this->timeout)) { // let's have a look if something changed in streams
if (false === $n = @stream_select($r, $w, $e, 0, static::TIMEOUT_PRECISION * 1E6)) {
$lastError = error_get_last(); $lastError = error_get_last();
// stream_select returns false when the `select` system call is interrupted by an incoming signal // stream_select returns false when the `select` system call is interrupted by an incoming signal
@ -361,10 +370,11 @@ class Process
continue; continue;
} }
if (0 === $n) {
proc_terminate($this->process);
throw new \RuntimeException('The process timed out.');
// nothing has changed
if (0 === $n) {
continue;
} }
foreach ($r as $pipe) { foreach ($r as $pipe) {
@ -675,7 +685,7 @@ class Process
* *
* To disable the timeout, set this value to null. * To disable the timeout, set this value to null.
* *
* @param integer|null $timeout The timeout in seconds * @param float|null $timeout The timeout in seconds
* *
* @throws \InvalidArgumentException if the timeout is negative * @throws \InvalidArgumentException if the timeout is negative
*/ */
@ -687,10 +697,10 @@ class Process
return; return;
} }
$timeout = (integer) $timeout; $timeout = (float) $timeout;
if ($timeout < 0) { if ($timeout < 0) {
throw new \InvalidArgumentException('The timeout value must be a valid positive integer.'); throw new \InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
} }
$this->timeout = $timeout; $this->timeout = $timeout;
@ -829,6 +839,23 @@ class Process
$this->enhanceSigchildCompatibility = (Boolean) $enhance; $this->enhanceSigchildCompatibility = (Boolean) $enhance;
} }
/**
* Performs a check between the timeout definition and the time the process
* started
*
* In case you run a background process (with the start method), you should
* trigger this method regularly to ensure the process timeout
*
* @throws RuntimeException In case the timeout was reached
*/
public function checkTimeout()
{
if (0 < $this->timeout && $this->timeout < (microtime(true) - $this->starttime)) {
$this->stop(0);
throw new RuntimeException('Process timed-out.');
}
}
/** /**
* Builds up the callback used by wait(). * Builds up the callback used by wait().
* *

View File

@ -86,7 +86,7 @@ class ProcessBuilder
* *
* To disable the timeout, set this value to null. * To disable the timeout, set this value to null.
* *
* @param integer|null * @param float|null
*/ */
public function setTimeout($timeout) public function setTimeout($timeout)
{ {
@ -96,10 +96,10 @@ class ProcessBuilder
return $this; return $this;
} }
$timeout = (integer) $timeout; $timeout = (float) $timeout;
if ($timeout < 0) { if ($timeout < 0) {
throw new \InvalidArgumentException('The timeout value must be a valid positive integer.'); throw new \InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
} }
$this->timeout = $timeout; $this->timeout = $timeout;

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Process\Tests; namespace Symfony\Component\Process\Tests;
use Symfony\Component\Process\Process; use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\RuntimeException;
/** /**
* @author Robert Schönthal <seroscho@googlemail.com> * @author Robert Schönthal <seroscho@googlemail.com>
@ -255,6 +256,47 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase
// PHP will deadlock when it tries to cleanup $process // PHP will deadlock when it tries to cleanup $process
} }
public function testRunProcessWithTimeout()
{
$timeout = 0.5;
$process = $this->getProcess('sleep 3');
$process->setTimeout($timeout);
$start = microtime(true);
try {
$process->run();
$this->fail('A RuntimeException should have been raised');
} catch (RuntimeException $e) {
}
$duration = microtime(true) - $start;
$this->assertLessThan($timeout + Process::TIMEOUT_PRECISION, $duration);
}
public function testCheckTimeoutOnStartedProcess()
{
$timeout = 0.5;
$precision = 100000;
$process = $this->getProcess('sleep 3');
$process->setTimeout($timeout);
$start = microtime(true);
$process->start();
try {
while ($process->isRunning()) {
$process->checkTimeout();
usleep($precision);
}
$this->fail('A RuntimeException should have been raised');
} catch (RuntimeException $e) {
}
$duration = microtime(true) - $start;
$this->assertLessThan($timeout + $precision, $duration);
}
public function responsesCodeProvider() public function responsesCodeProvider()
{ {
return array( return array(