* @author Fabien Potencier <fabien@symfony.com>
* @author Romain Neutron <imprec@gmail.com>
*/
-class Process
+class Process implements \IteratorAggregate
{
const ERR = 'err';
const OUT = 'out';
// Timeout Precision in seconds.
const TIMEOUT_PRECISION = 0.2;
+ const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking
+ const ITER_KEEP_OUTPUT = 2; // By default, outputs are cleared while iterating, use this flag to keep them in memory
+ const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating
+ const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating
+
private $callback;
+ private $hasCallback = false;
private $commandline;
private $cwd;
private $env;
private $incrementalErrorOutputOffset = 0;
private $tty;
private $pty;
+ private $inheritEnv = false;
private $useFileHandles = false;
/** @var PipesInterface */
* @param string $commandline The command line to run
* @param string|null $cwd The working directory or null to use the working dir of the current PHP process
* @param array|null $env The environment variables or null to use the same environment as the current PHP process
- * @param string|null $input The input
+ * @param mixed|null $input The input as stream resource, scalar or \Traversable, or null for no input
* @param int|float|null $timeout The timeout in seconds or null to disable
* @param array $options An array of options for proc_open
*
* @throws RuntimeException if PHP was compiled with --enable-sigchild and the enhanced sigchild compatibility mode is not enabled
* @throws ProcessFailedException if the process didn't terminate successfully
*/
- public function mustRun($callback = null)
+ public function mustRun(callable $callback = null)
{
if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
* @throws RuntimeException When process is already running
* @throws LogicException In case a callback is provided and output has been disabled
*/
- public function start($callback = null)
+ public function start(callable $callback = null)
{
if ($this->isRunning()) {
throw new RuntimeException('Process is already running');
}
- if ($this->outputDisabled && null !== $callback) {
- throw new LogicException('Output has been disabled, enable it to allow the use of a callback.');
- }
$this->resetProcessData();
$this->starttime = $this->lastOutputTime = microtime(true);
$this->callback = $this->buildCallback($callback);
+ $this->hasCallback = null !== $callback;
$descriptors = $this->getDescriptors();
+ $inheritEnv = $this->inheritEnv;
$commandline = $this->commandline;
+ $env = $this->env;
+ $envBackup = array();
+ if (null !== $env && $inheritEnv) {
+ if ('\\' === DIRECTORY_SEPARATOR && !empty($this->options['bypass_shell']) && !$this->enhanceWindowsCompatibility) {
+ throw new LogicException('The "bypass_shell" option must be false to inherit environment variables while enhanced Windows compatibility is off');
+ }
+
+ foreach ($env as $k => $v) {
+ $envBackup[$k] = getenv($k);
+ putenv(false === $v || null === $v ? $k : "$k=$v");
+ }
+ $env = null;
+ }
if ('\\' === DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) {
$commandline = 'cmd /V:ON /E:ON /D /C "('.$commandline.')';
foreach ($this->processPipes->getFiles() as $offset => $filename) {
$ptsWorkaround = fopen(__FILE__, 'r');
}
- $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options);
+ $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $env, $this->options);
+
+ foreach ($envBackup as $k => $v) {
+ putenv(false === $v ? $k : "$k=$v");
+ }
if (!is_resource($this->process)) {
throw new RuntimeException('Unable to launch a new process.');
*
* @see start()
*/
- public function restart($callback = null)
+ public function restart(callable $callback = null)
{
if ($this->isRunning()) {
throw new RuntimeException('Process is already running');
* @throws RuntimeException When process stopped after receiving signal
* @throws LogicException When process is not yet started
*/
- public function wait($callback = null)
+ public function wait(callable $callback = null)
{
$this->requireProcessIsStarted(__FUNCTION__);
$this->updateStatus(false);
+
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');
+ }
$this->callback = $this->buildCallback($callback);
}
return $latest;
}
+ /**
+ * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR).
+ *
+ * @param int $flags A bit field of Process::ITER_* flags
+ *
+ * @throws LogicException in case the output has been disabled
+ * @throws LogicException In case the process is not started
+ *
+ * @return \Generator
+ */
+ public function getIterator($flags = 0)
+ {
+ $this->readPipesForOutput(__FUNCTION__, false);
+
+ $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags);
+ $blocking = !(self::ITER_NON_BLOCKING & $flags);
+ $yieldOut = !(self::ITER_SKIP_OUT & $flags);
+ $yieldErr = !(self::ITER_SKIP_ERR & $flags);
+
+ while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) {
+ if ($yieldOut) {
+ $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
+
+ if (isset($out[0])) {
+ if ($clearOutput) {
+ $this->clearOutput();
+ } else {
+ $this->incrementalOutputOffset = ftell($this->stdout);
+ }
+
+ yield self::OUT => $out;
+ }
+ }
+
+ if ($yieldErr) {
+ $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
+
+ if (isset($err[0])) {
+ if ($clearOutput) {
+ $this->clearErrorOutput();
+ } else {
+ $this->incrementalErrorOutputOffset = ftell($this->stderr);
+ }
+
+ yield self::ERR => $err;
+ }
+ }
+
+ if (!$blocking && !isset($out[0]) && !isset($err[0])) {
+ yield self::OUT => '';
+ }
+
+ $this->checkTimeout();
+ $this->readPipesForOutput(__FUNCTION__, $blocking);
+ }
+ }
+
/**
* Clears the process output.
*
return !is_array($value);
});
- $this->env = array();
- foreach ($env as $key => $value) {
- $this->env[$key] = (string) $value;
- }
+ $this->env = $env;
return $this;
}
- /**
- * Gets the contents of STDIN.
- *
- * @return string|null The current contents
- *
- * @deprecated since version 2.5, to be removed in 3.0.
- * Use setInput() instead.
- * This method is deprecated in favor of getInput.
- */
- public function getStdin()
- {
- @trigger_error('The '.__METHOD__.' method is deprecated since version 2.5 and will be removed in 3.0. Use the getInput() method instead.', E_USER_DEPRECATED);
-
- return $this->getInput();
- }
-
/**
* Gets the Process input.
*
- * @return null|string The Process input
+ * @return resource|string|\Iterator|null The Process input
*/
public function getInput()
{
return $this->input;
}
- /**
- * Sets the contents of STDIN.
- *
- * @param string|null $stdin The new contents
- *
- * @return self The current Process instance
- *
- * @deprecated since version 2.5, to be removed in 3.0.
- * Use setInput() instead.
- *
- * @throws LogicException In case the process is running
- * @throws InvalidArgumentException In case the argument is invalid
- */
- public function setStdin($stdin)
- {
- @trigger_error('The '.__METHOD__.' method is deprecated since version 2.5 and will be removed in 3.0. Use the setInput() method instead.', E_USER_DEPRECATED);
-
- return $this->setInput($stdin);
- }
-
/**
* Sets the input.
*
* This content will be passed to the underlying process standard input.
*
- * @param mixed $input The content
+ * @param resource|scalar|\Traversable|null $input The content
*
* @return self The current Process instance
*
* @throws LogicException In case the process is running
- *
- * Passing an object as an input is deprecated since version 2.5 and will be removed in 3.0.
*/
public function setInput($input)
{
return $this;
}
+ /**
+ * Sets whether environment variables will be inherited or not.
+ *
+ * @param bool $inheritEnv
+ *
+ * @return self The current Process instance
+ */
+ public function inheritEnvironmentVariables($inheritEnv = true)
+ {
+ $this->inheritEnv = (bool) $inheritEnv;
+
+ return $this;
+ }
+
+ /**
+ * Returns whether environment variables will be inherited or not.
+ *
+ * @return bool
+ */
+ public function areEnvironmentVariablesInherited()
+ {
+ return $this->inheritEnv;
+ }
+
/**
* Performs a check between the timeout definition and the time the process started.
*
*/
private function getDescriptors()
{
+ if ($this->input instanceof \Iterator) {
+ $this->input->rewind();
+ }
if ('\\' === DIRECTORY_SEPARATOR) {
- $this->processPipes = WindowsPipes::create($this, $this->input);
+ $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $this->hasCallback);
} else {
- $this->processPipes = UnixPipes::create($this, $this->input);
+ $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->hasCallback);
}
return $this->processPipes->getDescriptors();
*
* @return \Closure A PHP closure
*/
- protected function buildCallback($callback)
+ protected function buildCallback(callable $callback = null)
{
- $that = $this;
+ if ($this->outputDisabled) {
+ return function ($type, $data) use ($callback) {
+ if (null !== $callback) {
+ call_user_func($callback, $type, $data);
+ }
+ };
+ }
+
$out = self::OUT;
- $callback = function ($type, $data) use ($that, $callback, $out) {
+
+ return function ($type, $data) use ($callback, $out) {
if ($out == $type) {
- $that->addOutput($data);
+ $this->addOutput($data);
} else {
- $that->addErrorOutput($data);
+ $this->addErrorOutput($data);
}
if (null !== $callback) {
call_user_func($callback, $type, $data);
}
};
-
- return $callback;
}
/**
/**
* Reads pipes for the freshest output.
*
- * @param $caller The name of the method that needs fresh outputs
+ * @param string $caller The name of the method that needs fresh outputs
+ * @param bool $blocking Whether to use blocking calls or not
*
* @throws LogicException in case output has been disabled or process is not started
*/
- private function readPipesForOutput($caller)
+ private function readPipesForOutput($caller, $blocking = false)
{
if ($this->outputDisabled) {
throw new LogicException('Output has been disabled.');
$this->requireProcessIsStarted($caller);
- $this->updateStatus(false);
+ $this->updateStatus($blocking);
}
/**