Skip to content

[Process] Provide interactive input. #19558

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions src/Symfony/Component/Process/Pipes/AbstractPipes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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;
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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]);
Expand Down
70 changes: 67 additions & 3 deletions src/Symfony/Component/Process/Process.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class Process implements \IteratorAggregate
private $cwd;
private $env;
private $input;
private $inputInteractive = false;
private $starttime;
private $lastOutputTime;
private $timeout;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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();
}
Expand Down
14 changes: 14 additions & 0 deletions src/Symfony/Component/Process/Tests/ProcessTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down