Fix Process timeout
This commit is contained in:
parent
7221d25fc4
commit
3780fdb214
@ -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().
|
||||||
*
|
*
|
||||||
|
@ -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;
|
||||||
|
@ -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(
|
||||||
|
Reference in New Issue
Block a user