Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
adds ability to define an idle timeout
  • Loading branch information
schmittjoh committed Aug 2, 2013
1 parent b788094 commit b922ba2
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 15 deletions.
@@ -0,0 +1,69 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Process\Exception;

use Symfony\Component\Process\Process;

/**
* Exception that is thrown when a process times out.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ProcessTimedOutException extends RuntimeException
{
const TYPE_GENERAL = 1;
const TYPE_IDLE = 2;

private $process;
private $timeoutType;

public function __construct(Process $process, $timeoutType)
{
$this->process = $process;
$this->timeoutType = $timeoutType;

parent::__construct(sprintf(
'The process "%s" exceeded the timeout of %s seconds.',
$process->getCommandLine(),
$this->getExceededTimeout()
));
}

public function getProcess()
{
return $this->process;
}

public function isGeneralTimeout()
{
return $this->timeoutType === self::TYPE_GENERAL;
}

public function isIdleTimeout()
{
return $this->timeoutType === self::TYPE_IDLE;
}

public function getExceededTimeout()
{
switch ($this->timeoutType) {
case self::TYPE_GENERAL:
return $this->process->getTimeout();

case self::TYPE_IDLE:
return $this->process->getIdleTimeout();

default:
throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType));
}
}
}
77 changes: 62 additions & 15 deletions src/Symfony/Component/Process/Process.php
Expand Up @@ -13,6 +13,7 @@

use Symfony\Component\Process\Exception\InvalidArgumentException;
use Symfony\Component\Process\Exception\LogicException;
use Symfony\Component\Process\Exception\ProcessTimedOutException;
use Symfony\Component\Process\Exception\RuntimeException;

/**
Expand Down Expand Up @@ -44,7 +45,9 @@ class Process
private $env;
private $stdin;
private $starttime;
private $lastOutputTime;
private $timeout;
private $idleTimeout;
private $options;
private $exitcode;
private $fallbackExitcode;
Expand Down Expand Up @@ -231,7 +234,7 @@ public function start($callback = null)
throw new RuntimeException('Process is already running');
}

$this->starttime = microtime(true);
$this->starttime = $this->lastOutputTime = microtime(true);
$this->stdout = '';
$this->stderr = '';
$this->incrementalOutputOffset = 0;
Expand Down Expand Up @@ -795,6 +798,7 @@ public function stop($timeout = 10, $signal = null)
*/
public function addOutput($line)
{
$this->lastOutputTime = microtime(true);
$this->stdout .= $line;
}

Expand All @@ -805,6 +809,7 @@ public function addOutput($line)
*/
public function addErrorOutput($line)
{
$this->lastOutputTime = microtime(true);
$this->stderr .= $line;
}

Expand Down Expand Up @@ -835,39 +840,53 @@ public function setCommandLine($commandline)
/**
* Gets the process timeout.
*
* @return integer|null The timeout in seconds or null if it's disabled
* @return float|null The timeout in seconds or null if it's disabled
*/
public function getTimeout()
{
return $this->timeout;
}

/**
* Gets the process idle timeout.
*
* @return float|null
*/
public function getIdleTimeout()
{
return $this->idleTimeout;
}

/**
* Sets the process timeout.
*
* To disable the timeout, set this value to null.
*
* @param float|null $timeout The timeout in seconds
* @param integer|float|null $timeout The timeout in seconds
*
* @return self The current Process instance
*
* @throws InvalidArgumentException if the timeout is negative
*/
public function setTimeout($timeout)
{
if (null === $timeout) {
$this->timeout = null;

return $this;
}

$timeout = (float) $timeout;
$this->timeout = $this->validateTimeout($timeout);

if ($timeout < 0) {
throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
}
return $this;
}

$this->timeout = $timeout;
/**
* Sets the process idle timeout.
*
* @param integer|float|null $timeout
*
* @return self The current Process instance.
*
* @throws InvalidArgumentException if the timeout is negative
*/
public function setIdleTimeout($timeout)
{
$this->idleTimeout = $this->validateTimeout($timeout);

return $this;
}
Expand Down Expand Up @@ -1078,7 +1097,13 @@ public function checkTimeout()
if (0 < $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
$this->stop(0);

throw new RuntimeException('The process timed-out.');
throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL);
}

if (0 < $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) {
$this->stop(0);

throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE);
}
}

Expand Down Expand Up @@ -1253,4 +1278,26 @@ private function hasSystemCallBeenInterrupted()
// stream_select returns false when the `select` system call is interrupted by an incoming signal
return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call');
}

/**
* Validates and returns the filtered timeout.
*
* @param integer|float|null $timeout
*
* @return float|null
*/
private function validateTimeout($timeout)
{
if (null === $timeout) {
return null;
}

$timeout = (float) $timeout;

if ($timeout < 0) {
throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
}

return $timeout;
}
}
40 changes: 40 additions & 0 deletions src/Symfony/Component/Process/Tests/AbstractProcessTest.php
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Process\Tests;

use Symfony\Component\Process\Exception\ProcessTimedOutException;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\RuntimeException;

Expand Down Expand Up @@ -429,6 +430,45 @@ public function testCheckTimeoutOnStartedProcess()
$this->assertLessThan($timeout + $precision, $duration);
}

/**
* @group idle-timeout
*/
public function testIdleTimeout()
{
$process = $this->getProcess('sleep 3');
$process->setTimeout(10);
$process->setIdleTimeout(1);

try {
$process->run();

$this->fail('A timeout exception was expected.');
} catch (ProcessTimedOutException $ex) {
$this->assertTrue($ex->isIdleTimeout());
$this->assertFalse($ex->isGeneralTimeout());
$this->assertEquals(1.0, $ex->getExceededTimeout());
}
}

/**
* @group idle-timeout
*/
public function testIdleTimeoutNotExceededWhenOutputIsSent()
{
$process = $this->getProcess('echo "foo"; sleep 1; echo "foo"; sleep 1; echo "foo"; sleep 1; echo "foo"; sleep 5;');
$process->setTimeout(5);
$process->setIdleTimeout(3);

try {
$process->run();
$this->fail('A timeout exception was expected.');
} catch (ProcessTimedOutException $ex) {
$this->assertTrue($ex->isGeneralTimeout());
$this->assertFalse($ex->isIdleTimeout());
$this->assertEquals(5.0, $ex->getExceededTimeout());
}
}

public function testGetPid()
{
$process = $this->getProcess('php -r "sleep(1);"');
Expand Down

0 comments on commit b922ba2

Please sign in to comment.