8000 merged branch schmittjoh/processIdleTimeout (PR #8651) · helmer/symfony@e4da195 · GitHub
[go: up one dir, main page]

Skip to content

Commit e4da195

Browse files
committed
merged branch schmittjoh/processIdleTimeout (PR symfony#8651)
This PR was merged into the master branch. Discussion ---------- adds ability to define an idle timeout This adds the ability to define an idle timeout which in contrast to the current timeout considers only the time since the last output was produced by a process. It also adds a special exception for timeout cases. Commits ------- b922ba2 adds ability to define an idle timeout
2 parents 89b42ba + b922ba2 commit e4da195

File tree

3 files changed

+171
-15
lines changed

3 files changed

+171
-15
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Process\Exception;
13+
14+
use Symfony\Component\Process\Process;
15+
16+
/**
17+
* Exception that is thrown when a process times out.
18+
*
19+
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
20+
*/
21+
class ProcessTimedOutException extends RuntimeException
22+
{
23+
const TYPE_GENERAL = 1;
24+
const TYPE_IDLE = 2;
25+
26+
private $process;
27+
private $timeoutType;
28+
29+
public function __construct(Process $process, $timeoutType)
30+
{
31+
$this->process = $process;
32+
$this->timeoutType = $timeoutType;
33+
34+
parent::__construct(sprintf(
35+
'The process "%s" exceeded the timeout of %s seconds.',
36+
$process->getCommandLine(),
37+
$this->getExceededTimeout()
38+
));
39+
}
40+
41+
public function getProcess()
42+
{
43+
return $this->process;
44+
}
45+
46+
public function isGeneralTimeout()
47+
{
48+
return $this->timeoutType === self::TYPE_GENERAL;
49+
}
50+
51+
public function isIdleTimeout()
52+
{
53+
return $this->timeoutType === self::TYPE_IDLE;
54+
}
55+
56+
public function getExceededTimeout()
57+
{
58+
switch ($this->timeoutType) {
59+
case self::TYPE_GENERAL:
60+
return $this->process->getTimeout();
61+
62+
case self::TYPE_IDLE:
63+
return $this->process->getIdleTimeout();
64+
65+
default:
66+
throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType));
67+
}
68+
}
69+
}

src/Symfony/Component/Process/Process.php

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Process\Exception\InvalidArgumentException;
1515
use Symfony\Component\Process\Exception\LogicException;
16+
use Symfony\Component\Process\Exception\ProcessTimedOutException;
1617
use Symfony\Component\Process\Exception\RuntimeException;
1718

1819
/**
@@ -44,7 +45,9 @@ class Process
4445
private $env;
4546
private $stdin;
4647
private $starttime;
48+
private $lastOutputTime;
4749
private $timeout;
50+
private $idleTimeout;
4851
private $options;
4952
private $exitcode;
5053
private $fallbackExitcode;
@@ -231,7 +234,7 @@ public function start($callback = null)
231234
throw new RuntimeException('Process is already running');
232235
}
233236

234-
$this->starttime = microtime(true);
237+
$this->starttime = $this->lastOutputTime = microtime(true);
235238
$this->stdout = '';
236239
$this->stderr = '';
237240
$this->incrementalOutputOffset = 0;
@@ -795,6 +798,7 @@ public function stop($timeout = 10, $signal = null)
795798
*/
796799
public function addOutput($line)
797800
{
801+
$this->lastOutputTime = microtime(true);
798802
$this->stdout .= $line;
799803
}
800804

@@ -805,6 +809,7 @@ public function addOutput($line)
805809
*/
806810
public function addErrorOutput($line)
807811
{
812+
$this->lastOutputTime = microtime(true);
808813
$this->stderr .= $line;
809814
}
810815

@@ -835,39 +840,53 @@ public function setCommandLine($commandline)
835840
/**
836841
* Gets the process timeout.
837842
*
838-
* @return integer|null The timeout in seconds or null if it's disabled
843+
* @return float|null The timeout in seconds or null if it's disabled
839844
*/
840845
public function getTimeout()
841846
{
842847
return $this->timeout;
843848
}
844849

850+
/**
851+
* Gets the process idle timeout.
852+
*
853+
* @return float|null
854+
*/
855+
public function getIdleTimeout()
856+
{
857+
return $this->idleTimeout;
858+
}
859+
845860
/**
846861
* Sets the process timeout.
847862
*
848863
* To disable the timeout, set this value to null.
849864
*
850-
* @param float|null $timeout The timeout in seconds
865+
* @param integer|float|null $timeout The timeout in seconds
851866
*
852867
* @return self The current Process instance
853868
*
854869
* @throws InvalidArgumentException if the timeout is negative
855870
*/
856871
public function setTimeout($timeout)
857872
{
858-
if (null === $timeout) {
859-
$this->timeout = null;
860-
861-
return $this;
862-
}
863-
864-
$timeout = (float) $timeout;
873+
$this->timeout = $this->validateTimeout($timeout);
865874

866-
if ($timeout < 0) {
867-
throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
868-
}
875+
return $this;
876+
}
869877

870-
$this->timeout = $timeout;
878+
/**
879+
* Sets the process idle timeout.
880+
*
881+
* @param integer|float|null $timeout
882+
*
883+
* @return self The current Process instance.
884+
*
885+
* @throws InvalidArgumentException if the timeout is negative
886+
*/
887+
public function setIdleTimeout($timeout)
888+
{
889+
$this->idleTimeout = $this->validateTimeout($timeout);
871890

872891
return $this;
873892
}
@@ -1078,7 +1097,13 @@ public function checkTimeout()
10781097
if (0 < $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
10791098
$this->stop(0);
10801099

1081-
throw new RuntimeException('The process timed-out.');
1100+
throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL);
1101+
}
1102+
1103+
if (0 < $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) {
1104+
$this->stop(0);
1105+
1106+
throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE);
10821107
}
10831108
}
10841109

@@ -1253,4 +1278,26 @@ private function hasSystemCallBeenInterrupted()
12531278
// stream_select returns false when the `select` system call is interrupted by an incoming signal
12541279
return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call');
12551280
}
1281+
1282+
/**
1283+
* Validates and returns the filtered timeout.
1284+
*
1285+
* @param integer|float|null $timeout
1286+
*
1287+
* @return float|null
1288+
*/
1289+
private function validateTimeout($timeout)
1290+
{
1291+
if (null === $timeout) {
1292+
return null;
1293+
}
1294+
1295+
$timeout = (float) $timeout;
1296+
1297+
if ($timeout < 0) {
1298+
throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
1299+
}
1300+
1301+
return $timeout;
1302+
}
12561303
}

src/Symfony/Component/Process/Tests/AbstractProcessTest.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\Process\Tests;
1313

14+
use Symfony\Component\Process\Exception\ProcessTimedOutException;
1415
use Symfony\Component\Process\Process;
1516
use Symfony\Component\Process\Exception\RuntimeException;
1617

@@ -429,6 +430,45 @@ public function testCheckTimeoutOnStartedProcess()
429430
$this->assertLessThan($timeout + $precision, $duration);
430431
}
431432

433+
/**
434+
* @group idle-timeout
435+
*/
436+
public function testIdleTimeout()
437+
{
438+
$process = $this->getProcess('sleep 3');
439+
$process->setTimeout(10);
440+
$process->setIdleTimeout(1);
441+
442+
try {
443+
$process->run();
444+
445+
$this->fail('A timeout exception was expected.');
446+
} catch (ProcessTimedOutException $ex) {
447+
$this->assertTrue($ex->isIdleTimeout());
448+
$this->assertFalse($ex->isGeneralTimeout());
449+
$this->assertEquals(1.0, $ex->getExceededTimeout());
450+
}
451+
}
452+
453+
/**
454+
* @group idle-timeout
455+
*/
456+
public function testIdleTimeoutNotExceededWhenOutputIsSent()
457+
{
458+
$process = $this->getProcess('echo "foo"; sleep 1; echo "foo"; sleep 1; echo "foo"; sleep 1; echo "foo"; sleep 5;');
459+
$process->setTimeout(5);
460+
$process->setIdleTimeout(3);
461+
462+
try {
463+
$process->run();
464+
$this->fail('A timeout exception was expected.');
465+
} catch (ProcessTimedOutException $ex) {
466+
$this->assertTrue($ex->isGeneralTimeout());
467+
$this->assertFalse($ex->isIdleTimeout());
468+
$this->assertEquals(5.0, $ex->getExceededTimeout());
469+
}
470+
}
471+
432472
public function testGetPid()
433473
{
434474
$process = $this->getProcess('php -r "sleep(1);"');

0 commit comments

Comments
 (0)
0