4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\Process;
14 use Symfony\Component\Process\Exception\InvalidArgumentException;
15 use Symfony\Component\Process\Exception\LogicException;
16 use Symfony\Component\Process\Exception\ProcessFailedException;
17 use Symfony\Component\Process\Exception\ProcessTimedOutException;
18 use Symfony\Component\Process\Exception\RuntimeException;
19 use Symfony\Component\Process\Pipes\PipesInterface;
20 use Symfony\Component\Process\Pipes\UnixPipes;
21 use Symfony\Component\Process\Pipes\WindowsPipes;
24 * Process is a thin wrapper around proc_* functions to easily
25 * start independent PHP processes.
27 * @author Fabien Potencier <fabien@symfony.com>
28 * @author Romain Neutron <imprec@gmail.com>
30 class Process implements \IteratorAggregate
35 const STATUS_READY = 'ready';
36 const STATUS_STARTED = 'started';
37 const STATUS_TERMINATED = 'terminated';
43 // Timeout Precision in seconds.
44 const TIMEOUT_PRECISION = 0.2;
46 const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking
47 const ITER_KEEP_OUTPUT = 2; // By default, outputs are cleared while iterating, use this flag to keep them in memory
48 const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating
49 const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating
52 private $hasCallback = false;
58 private $lastOutputTime;
63 private $fallbackStatus = array();
64 private $processInformation;
65 private $outputDisabled = false;
68 private $enhanceWindowsCompatibility = true;
69 private $enhanceSigchildCompatibility;
71 private $status = self::STATUS_READY;
72 private $incrementalOutputOffset = 0;
73 private $incrementalErrorOutputOffset = 0;
76 private $inheritEnv = false;
78 private $useFileHandles = false;
79 /** @var PipesInterface */
80 private $processPipes;
82 private $latestSignal;
84 private static $sigchild;
87 * Exit codes translation table.
89 * User-defined errors must use exit codes in the 64-113 range.
93 public static $exitCodes = array(
96 2 => 'Misuse of shell builtins',
98 126 => 'Invoked command cannot execute',
99 127 => 'Command not found',
100 128 => 'Invalid exit argument',
105 131 => 'Quit and dump core',
106 132 => 'Illegal instruction',
107 133 => 'Trace/breakpoint trap',
108 134 => 'Process aborted',
109 135 => 'Bus error: "access to undefined portion of memory object"',
110 136 => 'Floating point exception: "erroneous arithmetic operation"',
111 137 => 'Kill (terminate immediately)',
112 138 => 'User-defined 1',
113 139 => 'Segmentation violation',
114 140 => 'User-defined 2',
115 141 => 'Write to pipe with no one reading',
116 142 => 'Signal raised by alarm',
117 143 => 'Termination (request to terminate)',
119 145 => 'Child process terminated, stopped (or continued*)',
120 146 => 'Continue if stopped',
121 147 => 'Stop executing temporarily',
122 148 => 'Terminal stop signal',
123 149 => 'Background process attempting to read from tty ("in")',
124 150 => 'Background process attempting to write to tty ("out")',
125 151 => 'Urgent data available on socket',
126 152 => 'CPU time limit exceeded',
127 153 => 'File size limit exceeded',
128 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"',
129 155 => 'Profiling timer expired',
131 157 => 'Pollable event',
133 159 => 'Bad syscall',
139 * @param string $commandline The command line to run
140 * @param string|null $cwd The working directory or null to use the working dir of the current PHP process
141 * @param array|null $env The environment variables or null to use the same environment as the current PHP process
142 * @param mixed|null $input The input as stream resource, scalar or \Traversable, or null for no input
143 * @param int|float|null $timeout The timeout in seconds or null to disable
144 * @param array $options An array of options for proc_open
146 * @throws RuntimeException When proc_open is not installed
148 public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array())
150 if (!function_exists('proc_open')) {
151 throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.');
154 $this->commandline = $commandline;
157 // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started
158 // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected
159 // @see : https://bugs.php.net/bug.php?id=51800
160 // @see : https://bugs.php.net/bug.php?id=50524
161 if (null === $this->cwd && (defined('ZEND_THREAD_SAFE') || '\\' === DIRECTORY_SEPARATOR)) {
162 $this->cwd = getcwd();
168 $this->setInput($input);
169 $this->setTimeout($timeout);
170 $this->useFileHandles = '\\' === DIRECTORY_SEPARATOR;
172 $this->enhanceSigchildCompatibility = '\\' !== DIRECTORY_SEPARATOR && $this->isSigchildEnabled();
173 $this->options = array_replace(array('suppress_errors' => true, 'binary_pipes' => true), $options);
176 public function __destruct()
181 public function __clone()
183 $this->resetProcessData();
189 * The callback receives the type of output (out or err) and
190 * some bytes from the output in real-time. It allows to have feedback
191 * from the independent process during execution.
193 * The STDOUT and STDERR are also available after the process is finished
194 * via the getOutput() and getErrorOutput() methods.
196 * @param callable|null $callback A PHP callback to run whenever there is some
197 * output available on STDOUT or STDERR
199 * @return int The exit status code
201 * @throws RuntimeException When process can't be launched
202 * @throws RuntimeException When process stopped after receiving signal
203 * @throws LogicException In case a callback is provided and output has been disabled
205 public function run($callback = null)
207 $this->start($callback);
209 return $this->wait();
215 * This is identical to run() except that an exception is thrown if the process
216 * exits with a non-zero exit code.
218 * @param callable|null $callback
222 * @throws RuntimeException if PHP was compiled with --enable-sigchild and the enhanced sigchild compatibility mode is not enabled
223 * @throws ProcessFailedException if the process didn't terminate successfully
225 public function mustRun(callable $callback = null)
227 if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
228 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
231 if (0 !== $this->run($callback)) {
232 throw new ProcessFailedException($this);
239 * Starts the process and returns after writing the input to STDIN.
241 * This method blocks until all STDIN data is sent to the process then it
242 * returns while the process runs in the background.
244 * The termination of the process can be awaited with wait().
246 * The callback receives the type of output (out or err) and some bytes from
247 * the output in real-time while writing the standard input to the process.
248 * It allows to have feedback from the independent process during execution.
250 * @param callable|null $callback A PHP callback to run whenever there is some
251 * output available on STDOUT or STDERR
253 * @throws RuntimeException When process can't be launched
254 * @throws RuntimeException When process is already running
255 * @throws LogicException In case a callback is provided and output has been disabled
257 public function start(callable $callback = null)
259 if ($this->isRunning()) {
260 throw new RuntimeException('Process is already running');
263 $this->resetProcessData();
264 $this->starttime = $this->lastOutputTime = microtime(true);
265 $this->callback = $this->buildCallback($callback);
266 $this->hasCallback = null !== $callback;
267 $descriptors = $this->getDescriptors();
268 $inheritEnv = $this->inheritEnv;
270 $commandline = $this->commandline;
273 $envBackup = array();
274 if (null !== $env && $inheritEnv) {
275 if ('\\' === DIRECTORY_SEPARATOR && !empty($this->options['bypass_shell']) && !$this->enhanceWindowsCompatibility) {
276 throw new LogicException('The "bypass_shell" option must be false to inherit environment variables while enhanced Windows compatibility is off');
279 foreach ($env as $k => $v) {
280 $envBackup[$k] = getenv($k);
281 putenv(false === $v || null === $v ? $k : "$k=$v");
285 if ('\\' === DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) {
286 $commandline = 'cmd /V:ON /E:ON /D /C "('.$commandline.')';
287 foreach ($this->processPipes->getFiles() as $offset => $filename) {
288 $commandline .= ' '.$offset.'>'.ProcessUtils::escapeArgument($filename);
292 if (!isset($this->options['bypass_shell'])) {
293 $this->options['bypass_shell'] = true;
295 } elseif (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
296 // last exit code is output on the fourth pipe and caught to work around --enable-sigchild
297 $descriptors[3] = array('pipe', 'w');
299 // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input
300 $commandline = '{ ('.$this->commandline.') <&3 3<&- 3>/dev/null & } 3<&0;';
301 $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code';
303 // Workaround for the bug, when PTS functionality is enabled.
304 // @see : https://bugs.php.net/69442
305 $ptsWorkaround = fopen(__FILE__, 'r');
308 $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $env, $this->options);
310 foreach ($envBackup as $k => $v) {
311 putenv(false === $v ? $k : "$k=$v");
314 if (!is_resource($this->process)) {
315 throw new RuntimeException('Unable to launch a new process.');
317 $this->status = self::STATUS_STARTED;
319 if (isset($descriptors[3])) {
320 $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]);
327 $this->updateStatus(false);
328 $this->checkTimeout();
332 * Restarts the process.
334 * Be warned that the process is cloned before being started.
336 * @param callable|null $callback A PHP callback to run whenever there is some
337 * output available on STDOUT or STDERR
341 * @throws RuntimeException When process can't be launched
342 * @throws RuntimeException When process is already running
346 public function restart(callable $callback = null)
348 if ($this->isRunning()) {
349 throw new RuntimeException('Process is already running');
352 $process = clone $this;
353 $process->start($callback);
359 * Waits for the process to terminate.
361 * The callback receives the type of output (out or err) and some bytes
362 * from the output in real-time while writing the standard input to the process.
363 * It allows to have feedback from the independent process during execution.
365 * @param callable|null $callback A valid PHP callback
367 * @return int The exitcode of the process
369 * @throws RuntimeException When process timed out
370 * @throws RuntimeException When process stopped after receiving signal
371 * @throws LogicException When process is not yet started
373 public function wait(callable $callback = null)
375 $this->requireProcessIsStarted(__FUNCTION__);
377 $this->updateStatus(false);
379 if (null !== $callback) {
380 if (!$this->processPipes->haveReadSupport()) {
382 throw new \LogicException('Pass the callback to the Process::start method or enableOutput to use a callback with Process::wait');
384 $this->callback = $this->buildCallback($callback);
388 $this->checkTimeout();
389 $running = '\\' === DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
390 $this->readPipes($running, '\\' !== DIRECTORY_SEPARATOR || !$running);
393 while ($this->isRunning()) {
397 if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) {
398 throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig']));
401 return $this->exitcode;
405 * Returns the Pid (process identifier), if applicable.
407 * @return int|null The process id if running, null otherwise
409 public function getPid()
411 return $this->isRunning() ? $this->processInformation['pid'] : null;
415 * Sends a POSIX signal to the process.
417 * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php)
421 * @throws LogicException In case the process is not running
422 * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
423 * @throws RuntimeException In case of failure
425 public function signal($signal)
427 $this->doSignal($signal, true);
433 * Disables fetching output and error output from the underlying process.
437 * @throws RuntimeException In case the process is already running
438 * @throws LogicException if an idle timeout is set
440 public function disableOutput()
442 if ($this->isRunning()) {
443 throw new RuntimeException('Disabling output while the process is running is not possible.');
445 if (null !== $this->idleTimeout) {
446 throw new LogicException('Output can not be disabled while an idle timeout is set.');
449 $this->outputDisabled = true;
455 * Enables fetching output and error output from the underlying process.
459 * @throws RuntimeException In case the process is already running
461 public function enableOutput()
463 if ($this->isRunning()) {
464 throw new RuntimeException('Enabling output while the process is running is not possible.');
467 $this->outputDisabled = false;
473 * Returns true in case the output is disabled, false otherwise.
477 public function isOutputDisabled()
479 return $this->outputDisabled;
483 * Returns the current output of the process (STDOUT).
485 * @return string The process output
487 * @throws LogicException in case the output has been disabled
488 * @throws LogicException In case the process is not started
490 public function getOutput()
492 $this->readPipesForOutput(__FUNCTION__);
494 if (false === $ret = stream_get_contents($this->stdout, -1, 0)) {
502 * Returns the output incrementally.
504 * In comparison with the getOutput method which always return the whole
505 * output, this one returns the new output since the last call.
507 * @return string The process output since the last call
509 * @throws LogicException in case the output has been disabled
510 * @throws LogicException In case the process is not started
512 public function getIncrementalOutput()
514 $this->readPipesForOutput(__FUNCTION__);
516 $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
517 $this->incrementalOutputOffset = ftell($this->stdout);
519 if (false === $latest) {
527 * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR).
529 * @param int $flags A bit field of Process::ITER_* flags
531 * @throws LogicException in case the output has been disabled
532 * @throws LogicException In case the process is not started
536 public function getIterator($flags = 0)
538 $this->readPipesForOutput(__FUNCTION__, false);
540 $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags);
541 $blocking = !(self::ITER_NON_BLOCKING & $flags);
542 $yieldOut = !(self::ITER_SKIP_OUT & $flags);
543 $yieldErr = !(self::ITER_SKIP_ERR & $flags);
545 while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) {
547 $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
549 if (isset($out[0])) {
551 $this->clearOutput();
553 $this->incrementalOutputOffset = ftell($this->stdout);
556 yield self::OUT => $out;
561 $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
563 if (isset($err[0])) {
565 $this->clearErrorOutput();
567 $this->incrementalErrorOutputOffset = ftell($this->stderr);
570 yield self::ERR => $err;
574 if (!$blocking && !isset($out[0]) && !isset($err[0])) {
575 yield self::OUT => '';
578 $this->checkTimeout();
579 $this->readPipesForOutput(__FUNCTION__, $blocking);
584 * Clears the process output.
588 public function clearOutput()
590 ftruncate($this->stdout, 0);
591 fseek($this->stdout, 0);
592 $this->incrementalOutputOffset = 0;
598 * Returns the current error output of the process (STDERR).
600 * @return string The process error output
602 * @throws LogicException in case the output has been disabled
603 * @throws LogicException In case the process is not started
605 public function getErrorOutput()
607 $this->readPipesForOutput(__FUNCTION__);
609 if (false === $ret = stream_get_contents($this->stderr, -1, 0)) {
617 * Returns the errorOutput incrementally.
619 * In comparison with the getErrorOutput method which always return the
620 * whole error output, this one returns the new error output since the last
623 * @return string The process error output since the last call
625 * @throws LogicException in case the output has been disabled
626 * @throws LogicException In case the process is not started
628 public function getIncrementalErrorOutput()
630 $this->readPipesForOutput(__FUNCTION__);
632 $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
633 $this->incrementalErrorOutputOffset = ftell($this->stderr);
635 if (false === $latest) {
643 * Clears the process output.
647 public function clearErrorOutput()
649 ftruncate($this->stderr, 0);
650 fseek($this->stderr, 0);
651 $this->incrementalErrorOutputOffset = 0;
657 * Returns the exit code returned by the process.
659 * @return null|int The exit status code, null if the Process is not terminated
661 * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled
663 public function getExitCode()
665 if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
666 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
669 $this->updateStatus(false);
671 return $this->exitcode;
675 * Returns a string representation for the exit code returned by the process.
677 * This method relies on the Unix exit code status standardization
678 * and might not be relevant for other operating systems.
680 * @return null|string A string representation for the exit status code, null if the Process is not terminated
682 * @see http://tldp.org/LDP/abs/html/exitcodes.html
683 * @see http://en.wikipedia.org/wiki/Unix_signal
685 public function getExitCodeText()
687 if (null === $exitcode = $this->getExitCode()) {
691 return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error';
695 * Checks if the process ended successfully.
697 * @return bool true if the process ended successfully, false otherwise
699 public function isSuccessful()
701 return 0 === $this->getExitCode();
705 * Returns true if the child process has been terminated by an uncaught signal.
707 * It always returns false on Windows.
711 * @throws RuntimeException In case --enable-sigchild is activated
712 * @throws LogicException In case the process is not terminated
714 public function hasBeenSignaled()
716 $this->requireProcessIsTerminated(__FUNCTION__);
718 if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
719 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
722 return $this->processInformation['signaled'];
726 * Returns the number of the signal that caused the child process to terminate its execution.
728 * It is only meaningful if hasBeenSignaled() returns true.
732 * @throws RuntimeException In case --enable-sigchild is activated
733 * @throws LogicException In case the process is not terminated
735 public function getTermSignal()
737 $this->requireProcessIsTerminated(__FUNCTION__);
739 if ($this->isSigchildEnabled() && (!$this->enhanceSigchildCompatibility || -1 === $this->processInformation['termsig'])) {
740 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
743 return $this->processInformation['termsig'];
747 * Returns true if the child process has been stopped by a signal.
749 * It always returns false on Windows.
753 * @throws LogicException In case the process is not terminated
755 public function hasBeenStopped()
757 $this->requireProcessIsTerminated(__FUNCTION__);
759 return $this->processInformation['stopped'];
763 * Returns the number of the signal that caused the child process to stop its execution.
765 * It is only meaningful if hasBeenStopped() returns true.
769 * @throws LogicException In case the process is not terminated
771 public function getStopSignal()
773 $this->requireProcessIsTerminated(__FUNCTION__);
775 return $this->processInformation['stopsig'];
779 * Checks if the process is currently running.
781 * @return bool true if the process is currently running, false otherwise
783 public function isRunning()
785 if (self::STATUS_STARTED !== $this->status) {
789 $this->updateStatus(false);
791 return $this->processInformation['running'];
795 * Checks if the process has been started with no regard to the current state.
797 * @return bool true if status is ready, false otherwise
799 public function isStarted()
801 return $this->status != self::STATUS_READY;
805 * Checks if the process is terminated.
807 * @return bool true if process is terminated, false otherwise
809 public function isTerminated()
811 $this->updateStatus(false);
813 return $this->status == self::STATUS_TERMINATED;
817 * Gets the process status.
819 * The status is one of: ready, started, terminated.
821 * @return string The current process status
823 public function getStatus()
825 $this->updateStatus(false);
827 return $this->status;
833 * @param int|float $timeout The timeout in seconds
834 * @param int $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9)
836 * @return int The exit-code of the process
838 public function stop($timeout = 10, $signal = null)
840 $timeoutMicro = microtime(true) + $timeout;
841 if ($this->isRunning()) {
842 // given `SIGTERM` may not be defined and that `proc_terminate` uses the constant value and not the constant itself, we use the same here
843 $this->doSignal(15, false);
846 } while ($this->isRunning() && microtime(true) < $timeoutMicro);
848 if ($this->isRunning()) {
849 // Avoid exception here: process is supposed to be running, but it might have stopped just
850 // after this line. In any case, let's silently discard the error, we cannot do anything.
851 $this->doSignal($signal ?: 9, false);
855 if ($this->isRunning()) {
856 if (isset($this->fallbackStatus['pid'])) {
857 unset($this->fallbackStatus['pid']);
859 return $this->stop(0, $signal);
864 return $this->exitcode;
868 * Adds a line to the STDOUT stream.
872 * @param string $line The line to append
874 public function addOutput($line)
876 $this->lastOutputTime = microtime(true);
878 fseek($this->stdout, 0, SEEK_END);
879 fwrite($this->stdout, $line);
880 fseek($this->stdout, $this->incrementalOutputOffset);
884 * Adds a line to the STDERR stream.
888 * @param string $line The line to append
890 public function addErrorOutput($line)
892 $this->lastOutputTime = microtime(true);
894 fseek($this->stderr, 0, SEEK_END);
895 fwrite($this->stderr, $line);
896 fseek($this->stderr, $this->incrementalErrorOutputOffset);
900 * Gets the command line to be executed.
902 * @return string The command to execute
904 public function getCommandLine()
906 return $this->commandline;
910 * Sets the command line to be executed.
912 * @param string $commandline The command to execute
914 * @return self The current Process instance
916 public function setCommandLine($commandline)
918 $this->commandline = $commandline;
924 * Gets the process timeout (max. runtime).
926 * @return float|null The timeout in seconds or null if it's disabled
928 public function getTimeout()
930 return $this->timeout;
934 * Gets the process idle timeout (max. time since last output).
936 * @return float|null The timeout in seconds or null if it's disabled
938 public function getIdleTimeout()
940 return $this->idleTimeout;
944 * Sets the process timeout (max. runtime).
946 * To disable the timeout, set this value to null.
948 * @param int|float|null $timeout The timeout in seconds
950 * @return self The current Process instance
952 * @throws InvalidArgumentException if the timeout is negative
954 public function setTimeout($timeout)
956 $this->timeout = $this->validateTimeout($timeout);
962 * Sets the process idle timeout (max. time since last output).
964 * To disable the timeout, set this value to null.
966 * @param int|float|null $timeout The timeout in seconds
968 * @return self The current Process instance
970 * @throws LogicException if the output is disabled
971 * @throws InvalidArgumentException if the timeout is negative
973 public function setIdleTimeout($timeout)
975 if (null !== $timeout && $this->outputDisabled) {
976 throw new LogicException('Idle timeout can not be set while the output is disabled.');
979 $this->idleTimeout = $this->validateTimeout($timeout);
985 * Enables or disables the TTY mode.
987 * @param bool $tty True to enabled and false to disable
989 * @return self The current Process instance
991 * @throws RuntimeException In case the TTY mode is not supported
993 public function setTty($tty)
995 if ('\\' === DIRECTORY_SEPARATOR && $tty) {
996 throw new RuntimeException('TTY mode is not supported on Windows platform.');
999 static $isTtySupported;
1001 if (null === $isTtySupported) {
1002 $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', array(array('file', '/dev/tty', 'r'), array('file', '/dev/tty', 'w'), array('file', '/dev/tty', 'w')), $pipes);
1005 if (!$isTtySupported) {
1006 throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.');
1010 $this->tty = (bool) $tty;
1016 * Checks if the TTY mode is enabled.
1018 * @return bool true if the TTY mode is enabled, false otherwise
1020 public function isTty()
1032 public function setPty($bool)
1034 $this->pty = (bool) $bool;
1040 * Returns PTY state.
1044 public function isPty()
1050 * Gets the working directory.
1052 * @return string|null The current working directory or null on failure
1054 public function getWorkingDirectory()
1056 if (null === $this->cwd) {
1057 // getcwd() will return false if any one of the parent directories does not have
1058 // the readable or search mode set, even if the current directory does
1059 return getcwd() ?: null;
1066 * Sets the current working directory.
1068 * @param string $cwd The new working directory
1070 * @return self The current Process instance
1072 public function setWorkingDirectory($cwd)
1080 * Gets the environment variables.
1082 * @return array The current environment variables
1084 public function getEnv()
1090 * Sets the environment variables.
1092 * An environment variable value should be a string.
1093 * If it is an array, the variable is ignored.
1095 * That happens in PHP when 'argv' is registered into
1096 * the $_ENV array for instance.
1098 * @param array $env The new environment variables
1100 * @return self The current Process instance
1102 public function setEnv(array $env)
1104 // Process can not handle env values that are arrays
1105 $env = array_filter($env, function ($value) {
1106 return !is_array($value);
1115 * Gets the Process input.
1117 * @return resource|string|\Iterator|null The Process input
1119 public function getInput()
1121 return $this->input;
1127 * This content will be passed to the underlying process standard input.
1129 * @param resource|scalar|\Traversable|null $input The content
1131 * @return self The current Process instance
1133 * @throws LogicException In case the process is running
1135 public function setInput($input)
1137 if ($this->isRunning()) {
1138 throw new LogicException('Input can not be set while the process is running.');
1141 $this->input = ProcessUtils::validateInput(__METHOD__, $input);
1147 * Gets the options for proc_open.
1149 * @return array The current options
1151 public function getOptions()
1153 return $this->options;
1157 * Sets the options for proc_open.
1159 * @param array $options The new options
1161 * @return self The current Process instance
1163 public function setOptions(array $options)
1165 $this->options = $options;
1171 * Gets whether or not Windows compatibility is enabled.
1173 * This is true by default.
1177 public function getEnhanceWindowsCompatibility()
1179 return $this->enhanceWindowsCompatibility;
1183 * Sets whether or not Windows compatibility is enabled.
1185 * @param bool $enhance
1187 * @return self The current Process instance
1189 public function setEnhanceWindowsCompatibility($enhance)
1191 $this->enhanceWindowsCompatibility = (bool) $enhance;
1197 * Returns whether sigchild compatibility mode is activated or not.
1201 public function getEnhanceSigchildCompatibility()
1203 return $this->enhanceSigchildCompatibility;
1207 * Activates sigchild compatibility mode.
1209 * Sigchild compatibility mode is required to get the exit code and
1210 * determine the success of a process when PHP has been compiled with
1211 * the --enable-sigchild option
1213 * @param bool $enhance
1215 * @return self The current Process instance
1217 public function setEnhanceSigchildCompatibility($enhance)
1219 $this->enhanceSigchildCompatibility = (bool) $enhance;
1225 * Sets whether environment variables will be inherited or not.
1227 * @param bool $inheritEnv
1229 * @return self The current Process instance
1231 public function inheritEnvironmentVariables($inheritEnv = true)
1233 $this->inheritEnv = (bool) $inheritEnv;
1239 * Returns whether environment variables will be inherited or not.
1243 public function areEnvironmentVariablesInherited()
1245 return $this->inheritEnv;
1249 * Performs a check between the timeout definition and the time the process started.
1251 * In case you run a background process (with the start method), you should
1252 * trigger this method regularly to ensure the process timeout
1254 * @throws ProcessTimedOutException In case the timeout was reached
1256 public function checkTimeout()
1258 if ($this->status !== self::STATUS_STARTED) {
1262 if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
1265 throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL);
1268 if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) {
1271 throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE);
1276 * Returns whether PTY is supported on the current operating system.
1280 public static function isPtySupported()
1284 if (null !== $result) {
1288 if ('\\' === DIRECTORY_SEPARATOR) {
1289 return $result = false;
1292 return $result = (bool) @proc_open('echo 1 >/dev/null', array(array('pty'), array('pty'), array('pty')), $pipes);
1296 * Creates the descriptors needed by the proc_open.
1300 private function getDescriptors()
1302 if ($this->input instanceof \Iterator) {
1303 $this->input->rewind();
1305 if ('\\' === DIRECTORY_SEPARATOR) {
1306 $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $this->hasCallback);
1308 $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->hasCallback);
1311 return $this->processPipes->getDescriptors();
1315 * Builds up the callback used by wait().
1317 * The callbacks adds all occurred output to the specific buffer and calls
1318 * the user callback (if present) with the received output.
1320 * @param callable|null $callback The user defined PHP callback
1322 * @return \Closure A PHP closure
1324 protected function buildCallback(callable $callback = null)
1326 if ($this->outputDisabled) {
1327 return function ($type, $data) use ($callback) {
1328 if (null !== $callback) {
1329 call_user_func($callback, $type, $data);
1336 return function ($type, $data) use ($callback, $out) {
1337 if ($out == $type) {
1338 $this->addOutput($data);
1340 $this->addErrorOutput($data);
1343 if (null !== $callback) {
1344 call_user_func($callback, $type, $data);
1350 * Updates the status of the process, reads pipes.
1352 * @param bool $blocking Whether to use a blocking read call
1354 protected function updateStatus($blocking)
1356 if (self::STATUS_STARTED !== $this->status) {
1360 $this->processInformation = proc_get_status($this->process);
1361 $running = $this->processInformation['running'];
1363 $this->readPipes($running && $blocking, '\\' !== DIRECTORY_SEPARATOR || !$running);
1365 if ($this->fallbackStatus && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
1366 $this->processInformation = $this->fallbackStatus + $this->processInformation;
1375 * Returns whether PHP has been compiled with the '--enable-sigchild' option or not.
1379 protected function isSigchildEnabled()
1381 if (null !== self::$sigchild) {
1382 return self::$sigchild;
1385 if (!function_exists('phpinfo') || defined('HHVM_VERSION')) {
1386 return self::$sigchild = false;
1390 phpinfo(INFO_GENERAL);
1392 return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
1396 * Reads pipes for the freshest output.
1398 * @param string $caller The name of the method that needs fresh outputs
1399 * @param bool $blocking Whether to use blocking calls or not
1401 * @throws LogicException in case output has been disabled or process is not started
1403 private function readPipesForOutput($caller, $blocking = false)
1405 if ($this->outputDisabled) {
1406 throw new LogicException('Output has been disabled.');
1409 $this->requireProcessIsStarted($caller);
1411 $this->updateStatus($blocking);
1415 * Validates and returns the filtered timeout.
1417 * @param int|float|null $timeout
1419 * @return float|null
1421 * @throws InvalidArgumentException if the given timeout is a negative number
1423 private function validateTimeout($timeout)
1425 $timeout = (float) $timeout;
1427 if (0.0 === $timeout) {
1429 } elseif ($timeout < 0) {
1430 throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
1437 * Reads pipes, executes callback.
1439 * @param bool $blocking Whether to use blocking calls or not
1440 * @param bool $close Whether to close file handles or not
1442 private function readPipes($blocking, $close)
1444 $result = $this->processPipes->readAndWrite($blocking, $close);
1446 $callback = $this->callback;
1447 foreach ($result as $type => $data) {
1449 $callback($type === self::STDOUT ? self::OUT : self::ERR, $data);
1450 } elseif (!isset($this->fallbackStatus['signaled'])) {
1451 $this->fallbackStatus['exitcode'] = (int) $data;
1457 * Closes process resource, closes file handles, sets the exitcode.
1459 * @return int The exitcode
1461 private function close()
1463 $this->processPipes->close();
1464 if (is_resource($this->process)) {
1465 proc_close($this->process);
1467 $this->exitcode = $this->processInformation['exitcode'];
1468 $this->status = self::STATUS_TERMINATED;
1470 if (-1 === $this->exitcode) {
1471 if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) {
1472 // if process has been signaled, no exitcode but a valid termsig, apply Unix convention
1473 $this->exitcode = 128 + $this->processInformation['termsig'];
1474 } elseif ($this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
1475 $this->processInformation['signaled'] = true;
1476 $this->processInformation['termsig'] = -1;
1480 // Free memory from self-reference callback created by buildCallback
1481 // Doing so in other contexts like __destruct or by garbage collector is ineffective
1482 // Now pipes are closed, so the callback is no longer necessary
1483 $this->callback = null;
1485 return $this->exitcode;
1489 * Resets data related to the latest run of the process.
1491 private function resetProcessData()
1493 $this->starttime = null;
1494 $this->callback = null;
1495 $this->exitcode = null;
1496 $this->fallbackStatus = array();
1497 $this->processInformation = null;
1498 $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'wb+');
1499 $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'wb+');
1500 $this->process = null;
1501 $this->latestSignal = null;
1502 $this->status = self::STATUS_READY;
1503 $this->incrementalOutputOffset = 0;
1504 $this->incrementalErrorOutputOffset = 0;
1508 * Sends a POSIX signal to the process.
1510 * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php)
1511 * @param bool $throwException Whether to throw exception in case signal failed
1513 * @return bool True if the signal was sent successfully, false otherwise
1515 * @throws LogicException In case the process is not running
1516 * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
1517 * @throws RuntimeException In case of failure
1519 private function doSignal($signal, $throwException)
1521 if (null === $pid = $this->getPid()) {
1522 if ($throwException) {
1523 throw new LogicException('Can not send signal on a non running process.');
1529 if ('\\' === DIRECTORY_SEPARATOR) {
1530 exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode);
1531 if ($exitCode && $this->isRunning()) {
1532 if ($throwException) {
1533 throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output)));
1539 if (!$this->enhanceSigchildCompatibility || !$this->isSigchildEnabled()) {
1540 $ok = @proc_terminate($this->process, $signal);
1541 } elseif (function_exists('posix_kill')) {
1542 $ok = @posix_kill($pid, $signal);
1543 } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), array(2 => array('pipe', 'w')), $pipes)) {
1544 $ok = false === fgets($pipes[2]);
1547 if ($throwException) {
1548 throw new RuntimeException(sprintf('Error while sending signal `%s`.', $signal));
1555 $this->latestSignal = (int) $signal;
1556 $this->fallbackStatus['signaled'] = true;
1557 $this->fallbackStatus['exitcode'] = -1;
1558 $this->fallbackStatus['termsig'] = $this->latestSignal;
1564 * Ensures the process is running or terminated, throws a LogicException if the process has a not started.
1566 * @param string $functionName The function name that was called
1568 * @throws LogicException If the process has not run.
1570 private function requireProcessIsStarted($functionName)
1572 if (!$this->isStarted()) {
1573 throw new LogicException(sprintf('Process must be started before calling %s.', $functionName));
1578 * Ensures the process is terminated, throws a LogicException if the process has a status different than `terminated`.
1580 * @param string $functionName The function name that was called
1582 * @throws LogicException If the process is not yet terminated.
1584 private function requireProcessIsTerminated($functionName)
1586 if (!$this->isTerminated()) {
1587 throw new LogicException(sprintf('Process must be terminated before calling %s.', $functionName));