From b7da10615586eb7639afaa6b5d34e72d29a14512 Mon Sep 17 00:00:00 2001 From: boombatower Date: Sat, 6 Aug 2016 22:32:33 -0500 Subject: [PATCH] [Process] Provide interactive input. Instead of always closing the input pipe after exhausting the input buffer or resource leave the input pipe open. - setInputInteractive(): controls interactivity - appendInputBuffer(): convenience method for writing additional input - setInput(): now allows setting new input if interactive --- .../Component/Process/Pipes/AbstractPipes.php | 43 ++++++++++-- src/Symfony/Component/Process/Process.php | 70 ++++++++++++++++++- .../Component/Process/Tests/ProcessTest.php | 14 ++++ 3 files changed, 117 insertions(+), 10 deletions(-) diff --git a/src/Symfony/Component/Process/Pipes/AbstractPipes.php b/src/Symfony/Component/Process/Pipes/AbstractPipes.php index 4c67d5b82c31..795546545bc4 100644 --- a/src/Symfony/Component/Process/Pipes/AbstractPipes.php +++ b/src/Symfony/Component/Process/Pipes/AbstractPipes.php @@ -23,6 +23,8 @@ abstract class AbstractPipes implements PipesInterface /** @var array */ public $pipes = array(); + /** @var bool */ + protected $inputInteractive = false; /** @var string */ private $inputBuffer = ''; /** @var resource|scalar|\Iterator|null */ @@ -31,6 +33,25 @@ abstract class AbstractPipes implements PipesInterface private $blocked = true; public function __construct($input) + { + $this->setInput($input); + } + + /** + * {@inheritdoc} + */ + public function close() + { + foreach ($this->pipes as $pipe) { + fclose($pipe); + } + $this->pipes = array(); + } + + /** + * @see Process::setInput() + */ + public function setInput($input) { if (is_resource($input) || $input instanceof \Iterator) { $this->input = $input; @@ -42,14 +63,22 @@ public function __construct($input) } /** - * {@inheritdoc} + * @see Process::setInputInteractive() */ - public function close() + public function setInputInteractive($interactive) { - foreach ($this->pipes as $pipe) { - fclose($pipe); - } - $this->pipes = array(); + $this->inputInteractive = $interactive; + } + + /** + * @see Process::appendInputBuffer() + */ + public function appendInputBuffer($buffer) + { + // An interesting alternative would be to provide a loopback stream + // resource that could be give as $input and simply written to + // repeatedly, but this is non-trivial to achieve. + $this->inputBuffer .= $buffer; } /** @@ -158,7 +187,7 @@ protected function write() } // no input to read on resource, buffer is empty - if (!isset($this->inputBuffer[0]) && !($this->input instanceof \Iterator ? $this->input->valid() : $this->input)) { + if (!$this->inputInteractive && !isset($this->inputBuffer[0]) && !($this->input instanceof \Iterator ? $this->input->valid() : $this->input)) { $this->input = null; fclose($this->pipes[0]); unset($this->pipes[0]); diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index 39493d72e01a..22e073ab495c 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -54,6 +54,7 @@ class Process implements \IteratorAggregate private $cwd; private $env; private $input; + private $inputInteractive = false; private $starttime; private $lastOutputTime; private $timeout; @@ -1122,15 +1123,77 @@ public function getInput() * * @return self The current Process instance * - * @throws LogicException In case the process is running + * @throws LogicException In case the process is running and not interactive */ public function setInput($input) { + $input = ProcessUtils::validateInput(__METHOD__, $input); + if ($this->isRunning()) { - throw new LogicException('Input can not be set while the process is running.'); + if ($this->inputInteractive) { + // If process is already running $this->input will not be read, + // thus the input must be set directly on processPipes. + $this->processPipes->setInput($input); + } else { + throw new LogicException('Input can not be set while the process is running.'); + } + } else { + // Only used for before process is running. + $this->input = $input; + } + + return $this; + } + + /** + * Set the input interactive flag. + * + * True indicates that the input pipe will not be closed when the given + * input has been written to the pipe. Defaults to false which means the + * pipe will be closed after the initial input has been exhausted. + * + * @param bool $interactive The interactive flag + * + * @return self The current Process instance + * + * @throws LogicException In case the process is running + */ + public function setInputInteractive($interactive) + { + if ($this->isRunning()) { + throw new LogicException('Input interactivity can not be set while the process is running.'); + } + + $this->inputInteractive = $interactive; + + return $this; + } + + /** + * Appends the input buffer (string/scalar only). + * + * This content will be passed to the underlying process standard input. + * + * @param mixed $buffer The content + * + * @return self The current Process instance + * + * @throws LogicException In case the process is not running or not interactive + * @throws LogicException In case the buffer is a resource + */ + public function appendInputBuffer($buffer) + { + if (!$this->inputInteractive || !$this->isRunning()) { + throw new LogicException('Input buffer can not be appended if not in interactive mode or while the process is not running.'); + } + + if (is_resource($buffer)) { + throw new LogicException('Input buffer may not be a resource.'); } - $this->input = ProcessUtils::validateInput(__METHOD__, $input); + // Validate looks for scalar or string and returns string. + $buffer = ProcessUtils::validateInput(__METHOD__, $buffer); + $this->processPipes->appendInputBuffer($buffer); return $this; } @@ -1299,6 +1362,7 @@ private function getDescriptors() } else { $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->hasCallback); } + $this->processPipes->setInputInteractive($this->inputInteractive); return $this->processPipes->getDescriptors(); } diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php index 3e7c2da31610..8e692bc99116 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -1396,6 +1396,20 @@ public function testInheritEnvDisabled() $this->assertSame($expected, $env); } + public function testInteractiveInput() + { + $p = $this->getProcess('read && read && echo done'); + $p->setInputInteractive(true); + $p->start(); + $p->setInput(PHP_EOL); + $this->assertTrue($p->isRunning()); // trigger read/write + $p->appendInputBuffer(PHP_EOL); + $this->assertTrue($p->isRunning()); // trigger read/write + usleep(100000); // allow a tenth of a second to complete + $this->assertFalse($p->isRunning()); + $this->assertTrue(false !== strpos($p->getOutput(), 'done')); + } + /** * @param string $commandline * @param null|string $cwd