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 STDERR = 2;
// Timeout Precision in seconds.
CONST TIMEOUT_PRECISION = 0.2;
private $commandline;
private $cwd;
private $env;
private $stdin;
private $starttime;
private $timeout;
private $options;
private $exitcode;
@ -211,6 +215,7 @@ class Process
throw new \RuntimeException('Process is already running');
}
$this->starttime = microtime(true);
$this->stdout = '';
$this->stderr = '';
$callback = $this->buildCallback($callback);
@ -285,7 +290,7 @@ class Process
$w = $writePipes;
$e = null;
$n = @stream_select($r, $w, $e, $this->timeout);
$n = @stream_select($r, $w, $e, 0, static::TIMEOUT_PRECISION * 1E6);
if (false === $n) {
break;
@ -318,6 +323,8 @@ class Process
unset($this->pipes[$type]);
}
}
$this->checkTimeout();
}
$this->updateStatus();
@ -345,13 +352,15 @@ class Process
if (defined('PHP_WINDOWS_VERSION_BUILD') && $this->fileHandles) {
$this->processFileHandles($callback, !$this->pipes);
}
$this->checkTimeout();
if ($this->pipes) {
$r = $this->pipes;
$w = 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();
// stream_select returns false when the `select` system call is interrupted by an incoming signal
@ -361,10 +370,11 @@ class Process
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) {
@ -675,7 +685,7 @@ class Process
*
* 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
*/
@ -687,10 +697,10 @@ class Process
return;
}
$timeout = (integer) $timeout;
$timeout = (float) $timeout;
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;
@ -829,6 +839,23 @@ class Process
$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().
*

View File

@ -86,7 +86,7 @@ class ProcessBuilder
*
* To disable the timeout, set this value to null.
*
* @param integer|null
* @param float|null
*/
public function setTimeout($timeout)
{
@ -96,10 +96,10 @@ class ProcessBuilder
return $this;
}
$timeout = (integer) $timeout;
$timeout = (float) $timeout;
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;

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Process\Tests;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\RuntimeException;
/**
* @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
}
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()
{
return array(