merged branch romainneutron/process-timeout-2 (PR #7583)
This PR was merged into the 2.1 branch. Discussion ---------- [2.1][Process] Fix Process timeout | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #6976 | License | MIT This PR fixes timeout in the process component. Timeout can now be float values. The precision of the timeout is given by the `Process::TIMEOUT_PRECISION` constant. Note : The timeout works when running the process with the `Process::run` method. When using the `Process::start` method, the `Process::checkTimeout` method should be regularly triggered to perform a check. example : ```php $process->start(); while ($condition) { // some stuff $process->checkTimeout(); // throws an Symfony\Process\Exception\RuntimeException if timeout is reached usleep(1000); } ``` Commits -------bf4a9b0
Round stream_select fifth argument up.3780fdb
Fix Process timeout
This commit is contained in:
commit
72b4f86d09
@ -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, ceil(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, ceil(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().
|
||||
*
|
||||
|
@ -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;
|
||||
|
@ -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(
|
||||
|
Reference in New Issue
Block a user