feature #27742 [Process] Add feature "wait until callback" to process class (Nek-)

This PR was squashed before being merged into the 4.2-dev branch (closes #27742).

Discussion
----------

[Process] Add feature "wait until callback" to process class

I often see code like the following:

```php
$process->start();
// wait for the process to be ready
sleep(3);
```

Many times in tests, sometimes in other places. There is a problem with this kind of code because if for any reason the process starts slower than the usual... Your code may fail after that. Also if it's faster, you're losing time for waiting.

So here is my proposal:

```php
$process->start();
$process->waitUntil(function($type, $output) {
    // check the output and return true to stop waiting when you got what you wait
});
```

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | none
| License       | MIT
| Doc PR        | Waiting for feedbacks

Commits
-------

27eaf83b63 [Process] Add feature \"wait until callback\" to process class
This commit is contained in:
Fabien Potencier 2018-10-11 10:22:46 -07:00
commit 4a4fda5679
4 changed files with 97 additions and 3 deletions

View File

@ -7,6 +7,8 @@ CHANGELOG
* added the `Process::fromShellCommandline()` to run commands in a shell wrapper
* deprecated passing a command as string when creating a `Process` instance
* deprecated the `Process::setCommandline()` and the `PhpProcess::setPhpBinary()` methods
* added the `Process::waitUntil()` method to wait for the process only for a
specific output, then continue the normal execution of your application
4.1.0
-----

View File

@ -407,7 +407,7 @@ class Process implements \IteratorAggregate
if (null !== $callback) {
if (!$this->processPipes->haveReadSupport()) {
$this->stop(0);
throw new \LogicException('Pass the callback to the Process::start method or enableOutput to use a callback with Process::wait');
throw new \LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait"');
}
$this->callback = $this->buildCallback($callback);
}
@ -429,6 +429,45 @@ class Process implements \IteratorAggregate
return $this->exitcode;
}
/**
* Waits until the callback returns true.
*
* The callback receives the type of output (out or err) and some bytes
* from the output in real-time while writing the standard input to the process.
* It allows to have feedback from the independent process during execution.
*
* @param callable $callback
*
* @throws RuntimeException When process timed out
* @throws LogicException When process is not yet started
*/
public function waitUntil(callable $callback)
{
$this->requireProcessIsStarted(__FUNCTION__);
$this->updateStatus(false);
if (!$this->processPipes->haveReadSupport()) {
$this->stop(0);
throw new \LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::waitUntil".');
}
$callback = $this->buildCallback($callback);
$wait = true;
do {
$this->checkTimeout();
$running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
$output = $this->processPipes->readAndWrite($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
foreach ($output as $type => $data) {
if (3 !== $type) {
$wait = !$callback(self::STDOUT === $type ? self::OUT : self::ERR, $data);
} elseif (!isset($this->fallbackStatus['signaled'])) {
$this->fallbackStatus['exitcode'] = (int) $data;
}
}
} while ($wait);
}
/**
* Returns the Pid (process identifier), if applicable.
*
@ -1264,7 +1303,7 @@ class Process implements \IteratorAggregate
if ($this->outputDisabled) {
return function ($type, $data) use ($callback) {
if (null !== $callback) {
\call_user_func($callback, $type, $data);
return \call_user_func($callback, $type, $data);
}
};
}
@ -1279,7 +1318,7 @@ class Process implements \IteratorAggregate
}
if (null !== $callback) {
\call_user_func($callback, $type, $data);
return \call_user_func($callback, $type, $data);
}
};
}

View File

@ -0,0 +1,26 @@
<?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.
*/
$outputs = array(
'First iteration output',
'Second iteration output',
'One more iteration output',
'This took more time',
'This one was sooooo slow',
);
$iterationTime = 10000;
foreach ($outputs as $output) {
usleep($iterationTime);
$iterationTime *= 10;
echo $output."\n";
}

View File

@ -133,6 +133,33 @@ class ProcessTest extends TestCase
$this->assertLessThan(15, microtime(true) - $start);
}
public function testWaitUntilSpecificOutput()
{
$p = $this->getProcess(array(self::$phpBin, __DIR__.'/KillableProcessWithOutput.php'));
$p->start();
$start = microtime(true);
$completeOutput = '';
$p->waitUntil(function ($type, $output) use (&$completeOutput) {
$completeOutput .= $output;
if (false !== strpos($output, 'One more')) {
return true;
}
return false;
});
$p->stop();
if ('\\' === \DIRECTORY_SEPARATOR) {
// Windows is slower
$this->assertLessThan(15, microtime(true) - $start);
} else {
$this->assertLessThan(2, microtime(true) - $start);
}
$this->assertEquals("First iteration output\nSecond iteration output\nOne more iteration output\n", $completeOutput);
}
public function testAllOutputIsActuallyReadOnTermination()
{
// this code will result in a maximum of 2 reads of 8192 bytes by calling