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;
61 private $options = array('suppress_errors' => true);
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.
91 public static $exitCodes = array(
94 2 => 'Misuse of shell builtins',
96 126 => 'Invoked command cannot execute',
97 127 => 'Command not found',
98 128 => 'Invalid exit argument',
103 131 => 'Quit and dump core',
104 132 => 'Illegal instruction',
105 133 => 'Trace/breakpoint trap',
106 134 => 'Process aborted',
107 135 => 'Bus error: "access to undefined portion of memory object"',
108 136 => 'Floating point exception: "erroneous arithmetic operation"',
109 137 => 'Kill (terminate immediately)',
110 138 => 'User-defined 1',
111 139 => 'Segmentation violation',
112 140 => 'User-defined 2',
113 141 => 'Write to pipe with no one reading',
114 142 => 'Signal raised by alarm',
115 143 => 'Termination (request to terminate)',
117 145 => 'Child process terminated, stopped (or continued*)',
118 146 => 'Continue if stopped',
119 147 => 'Stop executing temporarily',
120 148 => 'Terminal stop signal',
121 149 => 'Background process attempting to read from tty ("in")',
122 150 => 'Background process attempting to write to tty ("out")',
123 151 => 'Urgent data available on socket',
124 152 => 'CPU time limit exceeded',
125 153 => 'File size limit exceeded',
126 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"',
127 155 => 'Profiling timer expired',
129 157 => 'Pollable event',
131 159 => 'Bad syscall',
135 * @param string|array $commandline The command line to run
136 * @param string|null $cwd The working directory or null to use the working dir of the current PHP process
137 * @param array|null $env The environment variables or null to use the same environment as the current PHP process
138 * @param mixed|null $input The input as stream resource, scalar or \Traversable, or null for no input
139 * @param int|float|null $timeout The timeout in seconds or null to disable
140 * @param array $options An array of options for proc_open
142 * @throws RuntimeException When proc_open is not installed
144 public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = null)
146 if (!function_exists('proc_open')) {
147 throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.');
150 $this->commandline
= $commandline;
153 // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started
154 // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected
155 // @see : https://bugs.php.net/bug.php?id=51800
156 // @see : https://bugs.php.net/bug.php?id=50524
157 if (null === $this->cwd
&& (defined('ZEND_THREAD_SAFE') ||
'\\' === DIRECTORY_SEPARATOR
)) {
158 $this->cwd
= getcwd();
164 $this->setInput($input);
165 $this->setTimeout($timeout);
166 $this->useFileHandles
= '\\' === DIRECTORY_SEPARATOR
;
168 $this->enhanceSigchildCompatibility
= '\\' !== DIRECTORY_SEPARATOR
&& $this->isSigchildEnabled();
169 if (null !== $options) {
170 @trigger_error
(sprintf('The $options parameter of the %s constructor is deprecated since Symfony 3.3 and will be removed in 4.0.', __CLASS__
), E_USER_DEPRECATED
);
171 $this->options
= array_replace($this->options
, $options);
175 public function __destruct()
180 public function __clone()
182 $this->resetProcessData();
188 * The callback receives the type of output (out or err) and
189 * some bytes from the output in real-time. It allows to have feedback
190 * from the independent process during execution.
192 * The STDOUT and STDERR are also available after the process is finished
193 * via the getOutput() and getErrorOutput() methods.
195 * @param callable|null $callback A PHP callback to run whenever there is some
196 * output available on STDOUT or STDERR
197 * @param array $env An array of additional env vars to set when running the process
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 * @final since version 3.3
207 public function run($callback = null/*, array $env = array()*/)
209 $env = 1 < func_num_args() ?
func_get_arg(1) : null;
210 $this->start($callback, $env);
212 return $this->wait();
218 * This is identical to run() except that an exception is thrown if the process
219 * exits with a non-zero exit code.
221 * @param callable|null $callback
222 * @param array $env An array of additional env vars to set when running the process
226 * @throws RuntimeException if PHP was compiled with --enable-sigchild and the enhanced sigchild compatibility mode is not enabled
227 * @throws ProcessFailedException if the process didn't terminate successfully
229 * @final since version 3.3
231 public function mustRun(callable
$callback = null/*, array $env = array()*/)
233 if (!$this->enhanceSigchildCompatibility
&& $this->isSigchildEnabled()) {
234 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
236 $env = 1 < func_num_args() ?
func_get_arg(1) : null;
238 if (0 !== $this->run($callback, $env)) {
239 throw new ProcessFailedException($this);
246 * Starts the process and returns after writing the input to STDIN.
248 * This method blocks until all STDIN data is sent to the process then it
249 * returns while the process runs in the background.
251 * The termination of the process can be awaited with wait().
253 * The callback receives the type of output (out or err) and some bytes from
254 * the output in real-time while writing the standard input to the process.
255 * It allows to have feedback from the independent process during execution.
257 * @param callable|null $callback A PHP callback to run whenever there is some
258 * output available on STDOUT or STDERR
259 * @param array $env An array of additional env vars to set when running the process
261 * @throws RuntimeException When process can't be launched
262 * @throws RuntimeException When process is already running
263 * @throws LogicException In case a callback is provided and output has been disabled
265 public function start(callable
$callback = null/*, array $env = array()*/)
267 if ($this->isRunning()) {
268 throw new RuntimeException('Process is already running');
270 if (2 <= func_num_args()) {
271 $env = func_get_arg(1);
273 if (__CLASS__
!== static::class) {
274 $r = new \
ReflectionMethod($this, __FUNCTION__
);
275 if (__CLASS__
!== $r->getDeclaringClass()->getName() && (2 > $r->getNumberOfParameters() ||
'env' !== $r->getParameters()[0]->name
)) {
276 @trigger_error
(sprintf('The %s::start() method expects a second "$env" argument since Symfony 3.3. It will be made mandatory in 4.0.', static::class), E_USER_DEPRECATED
);
282 $this->resetProcessData();
283 $this->starttime
= $this->lastOutputTime
= microtime(true);
284 $this->callback
= $this->buildCallback($callback);
285 $this->hasCallback
= null !== $callback;
286 $descriptors = $this->getDescriptors();
287 $inheritEnv = $this->inheritEnv
;
289 if (is_array($commandline = $this->commandline
)) {
290 $commandline = implode(' ', array_map(array($this, 'escapeArgument'), $commandline));
292 if ('\\' !== DIRECTORY_SEPARATOR
) {
293 // exec is mandatory to deal with sending a signal to the process
294 $commandline = 'exec '.$commandline;
307 if (null !== $env && $inheritEnv) {
308 $env +
= $this->getDefaultEnv();
309 } elseif (null !== $env) {
310 @trigger_error
('Not inheriting environment variables is deprecated since Symfony 3.3 and will always happen in 4.0. Set "Process::inheritEnvironmentVariables()" to true instead.', E_USER_DEPRECATED
);
312 $env = $this->getDefaultEnv();
314 if ('\\' === DIRECTORY_SEPARATOR
&& $this->enhanceWindowsCompatibility
) {
315 $this->options
['bypass_shell'] = true;
316 $commandline = $this->prepareWindowsCommandLine($commandline, $env);
317 } elseif (!$this->useFileHandles
&& $this->enhanceSigchildCompatibility
&& $this->isSigchildEnabled()) {
318 // last exit code is output on the fourth pipe and caught to work around --enable-sigchild
319 $descriptors[3] = array('pipe', 'w');
321 // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input
322 $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;';
323 $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code';
325 // Workaround for the bug, when PTS functionality is enabled.
326 // @see : https://bugs.php.net/69442
327 $ptsWorkaround = fopen(__FILE__
, 'r');
329 if (defined('HHVM_VERSION')) {
333 foreach ($env as $k => $v) {
335 $envPairs[] = $k.'='.$v;
340 if (!is_dir($this->cwd
)) {
341 @trigger_error
('The provided cwd does not exist. Command is currently ran against getcwd(). This behavior is deprecated since Symfony 3.4 and will be removed in 4.0.', E_USER_DEPRECATED
);
344 $this->process
= proc_open($commandline, $descriptors, $this->processPipes
->pipes
, $this->cwd
, $envPairs, $this->options
);
346 if (!is_resource($this->process
)) {
347 throw new RuntimeException('Unable to launch a new process.');
349 $this->status
= self
::STATUS_STARTED
;
351 if (isset($descriptors[3])) {
352 $this->fallbackStatus
['pid'] = (int) fgets($this->processPipes
->pipes
[3]);
359 $this->updateStatus(false);
360 $this->checkTimeout();
364 * Restarts the process.
366 * Be warned that the process is cloned before being started.
368 * @param callable|null $callback A PHP callback to run whenever there is some
369 * output available on STDOUT or STDERR
370 * @param array $env An array of additional env vars to set when running the process
374 * @throws RuntimeException When process can't be launched
375 * @throws RuntimeException When process is already running
379 * @final since version 3.3
381 public function restart(callable
$callback = null/*, array $env = array()*/)
383 if ($this->isRunning()) {
384 throw new RuntimeException('Process is already running');
386 $env = 1 < func_num_args() ?
func_get_arg(1) : null;
388 $process = clone $this;
389 $process->start($callback, $env);
395 * Waits for the process to terminate.
397 * The callback receives the type of output (out or err) and some bytes
398 * from the output in real-time while writing the standard input to the process.
399 * It allows to have feedback from the independent process during execution.
401 * @param callable|null $callback A valid PHP callback
403 * @return int The exitcode of the process
405 * @throws RuntimeException When process timed out
406 * @throws RuntimeException When process stopped after receiving signal
407 * @throws LogicException When process is not yet started
409 public function wait(callable
$callback = null)
411 $this->requireProcessIsStarted(__FUNCTION__
);
413 $this->updateStatus(false);
415 if (null !== $callback) {
416 if (!$this->processPipes
->haveReadSupport()) {
418 throw new \
LogicException('Pass the callback to the Process::start method or enableOutput to use a callback with Process::wait');
420 $this->callback
= $this->buildCallback($callback);
424 $this->checkTimeout();
425 $running = '\\' === DIRECTORY_SEPARATOR ?
$this->isRunning() : $this->processPipes
->areOpen();
426 $this->readPipes($running, '\\' !== DIRECTORY_SEPARATOR ||
!$running);
429 while ($this->isRunning()) {
433 if ($this->processInformation
['signaled'] && $this->processInformation
['termsig'] !== $this->latestSignal
) {
434 throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation
['termsig']));
437 return $this->exitcode
;
441 * Returns the Pid (process identifier), if applicable.
443 * @return int|null The process id if running, null otherwise
445 public function getPid()
447 return $this->isRunning() ?
$this->processInformation
['pid'] : null;
451 * Sends a POSIX signal to the process.
453 * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php)
457 * @throws LogicException In case the process is not running
458 * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
459 * @throws RuntimeException In case of failure
461 public function signal($signal)
463 $this->doSignal($signal, true);
469 * Disables fetching output and error output from the underlying process.
473 * @throws RuntimeException In case the process is already running
474 * @throws LogicException if an idle timeout is set
476 public function disableOutput()
478 if ($this->isRunning()) {
479 throw new RuntimeException('Disabling output while the process is running is not possible.');
481 if (null !== $this->idleTimeout
) {
482 throw new LogicException('Output can not be disabled while an idle timeout is set.');
485 $this->outputDisabled
= true;
491 * Enables fetching output and error output from the underlying process.
495 * @throws RuntimeException In case the process is already running
497 public function enableOutput()
499 if ($this->isRunning()) {
500 throw new RuntimeException('Enabling output while the process is running is not possible.');
503 $this->outputDisabled
= false;
509 * Returns true in case the output is disabled, false otherwise.
513 public function isOutputDisabled()
515 return $this->outputDisabled
;
519 * Returns the current output of the process (STDOUT).
521 * @return string The process output
523 * @throws LogicException in case the output has been disabled
524 * @throws LogicException In case the process is not started
526 public function getOutput()
528 $this->readPipesForOutput(__FUNCTION__
);
530 if (false === $ret = stream_get_contents($this->stdout
, -1, 0)) {
538 * Returns the output incrementally.
540 * In comparison with the getOutput method which always return the whole
541 * output, this one returns the new output since the last call.
543 * @return string The process output since the last call
545 * @throws LogicException in case the output has been disabled
546 * @throws LogicException In case the process is not started
548 public function getIncrementalOutput()
550 $this->readPipesForOutput(__FUNCTION__
);
552 $latest = stream_get_contents($this->stdout
, -1, $this->incrementalOutputOffset
);
553 $this->incrementalOutputOffset
= ftell($this->stdout
);
555 if (false === $latest) {
563 * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR).
565 * @param int $flags A bit field of Process::ITER_* flags
567 * @throws LogicException in case the output has been disabled
568 * @throws LogicException In case the process is not started
572 public function getIterator($flags = 0)
574 $this->readPipesForOutput(__FUNCTION__
, false);
576 $clearOutput = !(self
::ITER_KEEP_OUTPUT
& $flags);
577 $blocking = !(self
::ITER_NON_BLOCKING
& $flags);
578 $yieldOut = !(self
::ITER_SKIP_OUT
& $flags);
579 $yieldErr = !(self
::ITER_SKIP_ERR
& $flags);
581 while (null !== $this->callback ||
($yieldOut && !feof($this->stdout
)) ||
($yieldErr && !feof($this->stderr
))) {
583 $out = stream_get_contents($this->stdout
, -1, $this->incrementalOutputOffset
);
585 if (isset($out[0])) {
587 $this->clearOutput();
589 $this->incrementalOutputOffset
= ftell($this->stdout
);
592 yield self
::OUT
=> $out;
597 $err = stream_get_contents($this->stderr
, -1, $this->incrementalErrorOutputOffset
);
599 if (isset($err[0])) {
601 $this->clearErrorOutput();
603 $this->incrementalErrorOutputOffset
= ftell($this->stderr
);
606 yield self
::ERR
=> $err;
610 if (!$blocking && !isset($out[0]) && !isset($err[0])) {
611 yield self
::OUT
=> '';
614 $this->checkTimeout();
615 $this->readPipesForOutput(__FUNCTION__
, $blocking);
620 * Clears the process output.
624 public function clearOutput()
626 ftruncate($this->stdout
, 0);
627 fseek($this->stdout
, 0);
628 $this->incrementalOutputOffset
= 0;
634 * Returns the current error output of the process (STDERR).
636 * @return string The process error output
638 * @throws LogicException in case the output has been disabled
639 * @throws LogicException In case the process is not started
641 public function getErrorOutput()
643 $this->readPipesForOutput(__FUNCTION__
);
645 if (false === $ret = stream_get_contents($this->stderr
, -1, 0)) {
653 * Returns the errorOutput incrementally.
655 * In comparison with the getErrorOutput method which always return the
656 * whole error output, this one returns the new error output since the last
659 * @return string The process error output since the last call
661 * @throws LogicException in case the output has been disabled
662 * @throws LogicException In case the process is not started
664 public function getIncrementalErrorOutput()
666 $this->readPipesForOutput(__FUNCTION__
);
668 $latest = stream_get_contents($this->stderr
, -1, $this->incrementalErrorOutputOffset
);
669 $this->incrementalErrorOutputOffset
= ftell($this->stderr
);
671 if (false === $latest) {
679 * Clears the process output.
683 public function clearErrorOutput()
685 ftruncate($this->stderr
, 0);
686 fseek($this->stderr
, 0);
687 $this->incrementalErrorOutputOffset
= 0;
693 * Returns the exit code returned by the process.
695 * @return null|int The exit status code, null if the Process is not terminated
697 * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled
699 public function getExitCode()
701 if (!$this->enhanceSigchildCompatibility
&& $this->isSigchildEnabled()) {
702 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
705 $this->updateStatus(false);
707 return $this->exitcode
;
711 * Returns a string representation for the exit code returned by the process.
713 * This method relies on the Unix exit code status standardization
714 * and might not be relevant for other operating systems.
716 * @return null|string A string representation for the exit status code, null if the Process is not terminated
718 * @see http://tldp.org/LDP/abs/html/exitcodes.html
719 * @see http://en.wikipedia.org/wiki/Unix_signal
721 public function getExitCodeText()
723 if (null === $exitcode = $this->getExitCode()) {
727 return isset(self
::$exitCodes[$exitcode]) ? self
::$exitCodes[$exitcode] : 'Unknown error';
731 * Checks if the process ended successfully.
733 * @return bool true if the process ended successfully, false otherwise
735 public function isSuccessful()
737 return 0 === $this->getExitCode();
741 * Returns true if the child process has been terminated by an uncaught signal.
743 * It always returns false on Windows.
747 * @throws RuntimeException In case --enable-sigchild is activated
748 * @throws LogicException In case the process is not terminated
750 public function hasBeenSignaled()
752 $this->requireProcessIsTerminated(__FUNCTION__
);
754 if (!$this->enhanceSigchildCompatibility
&& $this->isSigchildEnabled()) {
755 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
758 return $this->processInformation
['signaled'];
762 * Returns the number of the signal that caused the child process to terminate its execution.
764 * It is only meaningful if hasBeenSignaled() returns true.
768 * @throws RuntimeException In case --enable-sigchild is activated
769 * @throws LogicException In case the process is not terminated
771 public function getTermSignal()
773 $this->requireProcessIsTerminated(__FUNCTION__
);
775 if ($this->isSigchildEnabled() && (!$this->enhanceSigchildCompatibility ||
-1 === $this->processInformation
['termsig'])) {
776 throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
779 return $this->processInformation
['termsig'];
783 * Returns true if the child process has been stopped by a signal.
785 * It always returns false on Windows.
789 * @throws LogicException In case the process is not terminated
791 public function hasBeenStopped()
793 $this->requireProcessIsTerminated(__FUNCTION__
);
795 return $this->processInformation
['stopped'];
799 * Returns the number of the signal that caused the child process to stop its execution.
801 * It is only meaningful if hasBeenStopped() returns true.
805 * @throws LogicException In case the process is not terminated
807 public function getStopSignal()
809 $this->requireProcessIsTerminated(__FUNCTION__
);
811 return $this->processInformation
['stopsig'];
815 * Checks if the process is currently running.
817 * @return bool true if the process is currently running, false otherwise
819 public function isRunning()
821 if (self
::STATUS_STARTED
!== $this->status
) {
825 $this->updateStatus(false);
827 return $this->processInformation
['running'];
831 * Checks if the process has been started with no regard to the current state.
833 * @return bool true if status is ready, false otherwise
835 public function isStarted()
837 return self
::STATUS_READY
!= $this->status
;
841 * Checks if the process is terminated.
843 * @return bool true if process is terminated, false otherwise
845 public function isTerminated()
847 $this->updateStatus(false);
849 return self
::STATUS_TERMINATED
== $this->status
;
853 * Gets the process status.
855 * The status is one of: ready, started, terminated.
857 * @return string The current process status
859 public function getStatus()
861 $this->updateStatus(false);
863 return $this->status
;
869 * @param int|float $timeout The timeout in seconds
870 * @param int $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9)
872 * @return int The exit-code of the process
874 public function stop($timeout = 10, $signal = null)
876 $timeoutMicro = microtime(true) +
$timeout;
877 if ($this->isRunning()) {
878 // given `SIGTERM` may not be defined and that `proc_terminate` uses the constant value and not the constant itself, we use the same here
879 $this->doSignal(15, false);
882 } while ($this->isRunning() && microtime(true) < $timeoutMicro);
884 if ($this->isRunning()) {
885 // Avoid exception here: process is supposed to be running, but it might have stopped just
886 // after this line. In any case, let's silently discard the error, we cannot do anything.
887 $this->doSignal($signal ?
: 9, false);
891 if ($this->isRunning()) {
892 if (isset($this->fallbackStatus
['pid'])) {
893 unset($this->fallbackStatus
['pid']);
895 return $this->stop(0, $signal);
900 return $this->exitcode
;
904 * Adds a line to the STDOUT stream.
908 * @param string $line The line to append
910 public function addOutput($line)
912 $this->lastOutputTime
= microtime(true);
914 fseek($this->stdout
, 0, SEEK_END
);
915 fwrite($this->stdout
, $line);
916 fseek($this->stdout
, $this->incrementalOutputOffset
);
920 * Adds a line to the STDERR stream.
924 * @param string $line The line to append
926 public function addErrorOutput($line)
928 $this->lastOutputTime
= microtime(true);
930 fseek($this->stderr
, 0, SEEK_END
);
931 fwrite($this->stderr
, $line);
932 fseek($this->stderr
, $this->incrementalErrorOutputOffset
);
936 * Gets the command line to be executed.
938 * @return string The command to execute
940 public function getCommandLine()
942 return is_array($this->commandline
) ?
implode(' ', array_map(array($this, 'escapeArgument'), $this->commandline
)) : $this->commandline
;
946 * Sets the command line to be executed.
948 * @param string|array $commandline The command to execute
950 * @return self The current Process instance
952 public function setCommandLine($commandline)
954 $this->commandline
= $commandline;
960 * Gets the process timeout (max. runtime).
962 * @return float|null The timeout in seconds or null if it's disabled
964 public function getTimeout()
966 return $this->timeout
;
970 * Gets the process idle timeout (max. time since last output).
972 * @return float|null The timeout in seconds or null if it's disabled
974 public function getIdleTimeout()
976 return $this->idleTimeout
;
980 * Sets the process timeout (max. runtime).
982 * To disable the timeout, set this value to null.
984 * @param int|float|null $timeout The timeout in seconds
986 * @return self The current Process instance
988 * @throws InvalidArgumentException if the timeout is negative
990 public function setTimeout($timeout)
992 $this->timeout
= $this->validateTimeout($timeout);
998 * Sets the process idle timeout (max. time since last output).
1000 * To disable the timeout, set this value to null.
1002 * @param int|float|null $timeout The timeout in seconds
1004 * @return self The current Process instance
1006 * @throws LogicException if the output is disabled
1007 * @throws InvalidArgumentException if the timeout is negative
1009 public function setIdleTimeout($timeout)
1011 if (null !== $timeout && $this->outputDisabled
) {
1012 throw new LogicException('Idle timeout can not be set while the output is disabled.');
1015 $this->idleTimeout
= $this->validateTimeout($timeout);
1021 * Enables or disables the TTY mode.
1023 * @param bool $tty True to enabled and false to disable
1025 * @return self The current Process instance
1027 * @throws RuntimeException In case the TTY mode is not supported
1029 public function setTty($tty)
1031 if ('\\' === DIRECTORY_SEPARATOR
&& $tty) {
1032 throw new RuntimeException('TTY mode is not supported on Windows platform.');
1035 static $isTtySupported;
1037 if (null === $isTtySupported) {
1038 $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);
1041 if (!$isTtySupported) {
1042 throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.');
1046 $this->tty
= (bool) $tty;
1052 * Checks if the TTY mode is enabled.
1054 * @return bool true if the TTY mode is enabled, false otherwise
1056 public function isTty()
1068 public function setPty($bool)
1070 $this->pty
= (bool) $bool;
1076 * Returns PTY state.
1080 public function isPty()
1086 * Gets the working directory.
1088 * @return string|null The current working directory or null on failure
1090 public function getWorkingDirectory()
1092 if (null === $this->cwd
) {
1093 // getcwd() will return false if any one of the parent directories does not have
1094 // the readable or search mode set, even if the current directory does
1095 return getcwd() ?
: null;
1102 * Sets the current working directory.
1104 * @param string $cwd The new working directory
1106 * @return self The current Process instance
1108 public function setWorkingDirectory($cwd)
1116 * Gets the environment variables.
1118 * @return array The current environment variables
1120 public function getEnv()
1126 * Sets the environment variables.
1128 * Each environment variable value should be a string.
1129 * If it is an array, the variable is ignored.
1130 * If it is false or null, it will be removed when
1131 * env vars are otherwise inherited.
1133 * That happens in PHP when 'argv' is registered into
1134 * the $_ENV array for instance.
1136 * @param array $env The new environment variables
1138 * @return self The current Process instance
1140 public function setEnv(array $env)
1142 // Process can not handle env values that are arrays
1143 $env = array_filter($env, function ($value) {
1144 return !is_array($value);
1153 * Gets the Process input.
1155 * @return resource|string|\Iterator|null The Process input
1157 public function getInput()
1159 return $this->input
;
1165 * This content will be passed to the underlying process standard input.
1167 * @param string|int|float|bool|resource|\Traversable|null $input The content
1169 * @return self The current Process instance
1171 * @throws LogicException In case the process is running
1173 public function setInput($input)
1175 if ($this->isRunning()) {
1176 throw new LogicException('Input can not be set while the process is running.');
1179 $this->input
= ProcessUtils
::validateInput(__METHOD__
, $input);
1185 * Gets the options for proc_open.
1187 * @return array The current options
1189 * @deprecated since version 3.3, to be removed in 4.0.
1191 public function getOptions()
1193 @trigger_error
(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0.', __METHOD__
), E_USER_DEPRECATED
);
1195 return $this->options
;
1199 * Sets the options for proc_open.
1201 * @param array $options The new options
1203 * @return self The current Process instance
1205 * @deprecated since version 3.3, to be removed in 4.0.
1207 public function setOptions(array $options)
1209 @trigger_error
(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0.', __METHOD__
), E_USER_DEPRECATED
);
1211 $this->options
= $options;
1217 * Gets whether or not Windows compatibility is enabled.
1219 * This is true by default.
1223 * @deprecated since version 3.3, to be removed in 4.0. Enhanced Windows compatibility will always be enabled.
1225 public function getEnhanceWindowsCompatibility()
1227 @trigger_error
(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Enhanced Windows compatibility will always be enabled.', __METHOD__
), E_USER_DEPRECATED
);
1229 return $this->enhanceWindowsCompatibility
;
1233 * Sets whether or not Windows compatibility is enabled.
1235 * @param bool $enhance
1237 * @return self The current Process instance
1239 * @deprecated since version 3.3, to be removed in 4.0. Enhanced Windows compatibility will always be enabled.
1241 public function setEnhanceWindowsCompatibility($enhance)
1243 @trigger_error
(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Enhanced Windows compatibility will always be enabled.', __METHOD__
), E_USER_DEPRECATED
);
1245 $this->enhanceWindowsCompatibility
= (bool) $enhance;
1251 * Returns whether sigchild compatibility mode is activated or not.
1255 * @deprecated since version 3.3, to be removed in 4.0. Sigchild compatibility will always be enabled.
1257 public function getEnhanceSigchildCompatibility()
1259 @trigger_error
(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Sigchild compatibility will always be enabled.', __METHOD__
), E_USER_DEPRECATED
);
1261 return $this->enhanceSigchildCompatibility
;
1265 * Activates sigchild compatibility mode.
1267 * Sigchild compatibility mode is required to get the exit code and
1268 * determine the success of a process when PHP has been compiled with
1269 * the --enable-sigchild option
1271 * @param bool $enhance
1273 * @return self The current Process instance
1275 * @deprecated since version 3.3, to be removed in 4.0.
1277 public function setEnhanceSigchildCompatibility($enhance)
1279 @trigger_error
(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Sigchild compatibility will always be enabled.', __METHOD__
), E_USER_DEPRECATED
);
1281 $this->enhanceSigchildCompatibility
= (bool) $enhance;
1287 * Sets whether environment variables will be inherited or not.
1289 * @param bool $inheritEnv
1291 * @return self The current Process instance
1293 public function inheritEnvironmentVariables($inheritEnv = true)
1296 @trigger_error
('Not inheriting environment variables is deprecated since Symfony 3.3 and will always happen in 4.0. Set "Process::inheritEnvironmentVariables()" to true instead.', E_USER_DEPRECATED
);
1299 $this->inheritEnv
= (bool) $inheritEnv;
1305 * Returns whether environment variables will be inherited or not.
1309 * @deprecated since version 3.3, to be removed in 4.0. Environment variables will always be inherited.
1311 public function areEnvironmentVariablesInherited()
1313 @trigger_error
(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0. Environment variables will always be inherited.', __METHOD__
), E_USER_DEPRECATED
);
1315 return $this->inheritEnv
;
1319 * Performs a check between the timeout definition and the time the process started.
1321 * In case you run a background process (with the start method), you should
1322 * trigger this method regularly to ensure the process timeout
1324 * @throws ProcessTimedOutException In case the timeout was reached
1326 public function checkTimeout()
1328 if (self
::STATUS_STARTED
!== $this->status
) {
1332 if (null !== $this->timeout
&& $this->timeout
< microtime(true) - $this->starttime
) {
1335 throw new ProcessTimedOutException($this, ProcessTimedOutException
::TYPE_GENERAL
);
1338 if (null !== $this->idleTimeout
&& $this->idleTimeout
< microtime(true) - $this->lastOutputTime
) {
1341 throw new ProcessTimedOutException($this, ProcessTimedOutException
::TYPE_IDLE
);
1346 * Returns whether PTY is supported on the current operating system.
1350 public static function isPtySupported()
1354 if (null !== $result) {
1358 if ('\\' === DIRECTORY_SEPARATOR
) {
1359 return $result = false;
1362 return $result = (bool) @proc_open
('echo 1 >/dev/null', array(array('pty'), array('pty'), array('pty')), $pipes);
1366 * Creates the descriptors needed by the proc_open.
1370 private function getDescriptors()
1372 if ($this->input
instanceof \Iterator
) {
1373 $this->input
->rewind();
1375 if ('\\' === DIRECTORY_SEPARATOR
) {
1376 $this->processPipes
= new WindowsPipes($this->input
, !$this->outputDisabled ||
$this->hasCallback
);
1378 $this->processPipes
= new UnixPipes($this->isTty(), $this->isPty(), $this->input
, !$this->outputDisabled ||
$this->hasCallback
);
1381 return $this->processPipes
->getDescriptors();
1385 * Builds up the callback used by wait().
1387 * The callbacks adds all occurred output to the specific buffer and calls
1388 * the user callback (if present) with the received output.
1390 * @param callable|null $callback The user defined PHP callback
1392 * @return \Closure A PHP closure
1394 protected function buildCallback(callable
$callback = null)
1396 if ($this->outputDisabled
) {
1397 return function ($type, $data) use ($callback) {
1398 if (null !== $callback) {
1399 call_user_func($callback, $type, $data);
1406 return function ($type, $data) use ($callback, $out) {
1407 if ($out == $type) {
1408 $this->addOutput($data);
1410 $this->addErrorOutput($data);
1413 if (null !== $callback) {
1414 call_user_func($callback, $type, $data);
1420 * Updates the status of the process, reads pipes.
1422 * @param bool $blocking Whether to use a blocking read call
1424 protected function updateStatus($blocking)
1426 if (self
::STATUS_STARTED
!== $this->status
) {
1430 $this->processInformation
= proc_get_status($this->process
);
1431 $running = $this->processInformation
['running'];
1433 $this->readPipes($running && $blocking, '\\' !== DIRECTORY_SEPARATOR ||
!$running);
1435 if ($this->fallbackStatus
&& $this->enhanceSigchildCompatibility
&& $this->isSigchildEnabled()) {
1436 $this->processInformation
= $this->fallbackStatus +
$this->processInformation
;
1445 * Returns whether PHP has been compiled with the '--enable-sigchild' option or not.
1449 protected function isSigchildEnabled()
1451 if (null !== self
::$sigchild) {
1452 return self
::$sigchild;
1455 if (!function_exists('phpinfo') ||
defined('HHVM_VERSION')) {
1456 return self
::$sigchild = false;
1460 phpinfo(INFO_GENERAL
);
1462 return self
::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
1466 * Reads pipes for the freshest output.
1468 * @param string $caller The name of the method that needs fresh outputs
1469 * @param bool $blocking Whether to use blocking calls or not
1471 * @throws LogicException in case output has been disabled or process is not started
1473 private function readPipesForOutput($caller, $blocking = false)
1475 if ($this->outputDisabled
) {
1476 throw new LogicException('Output has been disabled.');
1479 $this->requireProcessIsStarted($caller);
1481 $this->updateStatus($blocking);
1485 * Validates and returns the filtered timeout.
1487 * @param int|float|null $timeout
1489 * @return float|null
1491 * @throws InvalidArgumentException if the given timeout is a negative number
1493 private function validateTimeout($timeout)
1495 $timeout = (float) $timeout;
1497 if (0.0 === $timeout) {
1499 } elseif ($timeout < 0) {
1500 throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
1507 * Reads pipes, executes callback.
1509 * @param bool $blocking Whether to use blocking calls or not
1510 * @param bool $close Whether to close file handles or not
1512 private function readPipes($blocking, $close)
1514 $result = $this->processPipes
->readAndWrite($blocking, $close);
1516 $callback = $this->callback
;
1517 foreach ($result as $type => $data) {
1519 $callback(self
::STDOUT
=== $type ? self
::OUT
: self
::ERR
, $data);
1520 } elseif (!isset($this->fallbackStatus
['signaled'])) {
1521 $this->fallbackStatus
['exitcode'] = (int) $data;
1527 * Closes process resource, closes file handles, sets the exitcode.
1529 * @return int The exitcode
1531 private function close()
1533 $this->processPipes
->close();
1534 if (is_resource($this->process
)) {
1535 proc_close($this->process
);
1537 $this->exitcode
= $this->processInformation
['exitcode'];
1538 $this->status
= self
::STATUS_TERMINATED
;
1540 if (-1 === $this->exitcode
) {
1541 if ($this->processInformation
['signaled'] && 0 < $this->processInformation
['termsig']) {
1542 // if process has been signaled, no exitcode but a valid termsig, apply Unix convention
1543 $this->exitcode
= 128 +
$this->processInformation
['termsig'];
1544 } elseif ($this->enhanceSigchildCompatibility
&& $this->isSigchildEnabled()) {
1545 $this->processInformation
['signaled'] = true;
1546 $this->processInformation
['termsig'] = -1;
1550 // Free memory from self-reference callback created by buildCallback
1551 // Doing so in other contexts like __destruct or by garbage collector is ineffective
1552 // Now pipes are closed, so the callback is no longer necessary
1553 $this->callback
= null;
1555 return $this->exitcode
;
1559 * Resets data related to the latest run of the process.
1561 private function resetProcessData()
1563 $this->starttime
= null;
1564 $this->callback
= null;
1565 $this->exitcode
= null;
1566 $this->fallbackStatus
= array();
1567 $this->processInformation
= null;
1568 $this->stdout
= fopen('php://temp/maxmemory:'.(1024 * 1024), 'wb+');
1569 $this->stderr
= fopen('php://temp/maxmemory:'.(1024 * 1024), 'wb+');
1570 $this->process
= null;
1571 $this->latestSignal
= null;
1572 $this->status
= self
::STATUS_READY
;
1573 $this->incrementalOutputOffset
= 0;
1574 $this->incrementalErrorOutputOffset
= 0;
1578 * Sends a POSIX signal to the process.
1580 * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php)
1581 * @param bool $throwException Whether to throw exception in case signal failed
1583 * @return bool True if the signal was sent successfully, false otherwise
1585 * @throws LogicException In case the process is not running
1586 * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
1587 * @throws RuntimeException In case of failure
1589 private function doSignal($signal, $throwException)
1591 if (null === $pid = $this->getPid()) {
1592 if ($throwException) {
1593 throw new LogicException('Can not send signal on a non running process.');
1599 if ('\\' === DIRECTORY_SEPARATOR
) {
1600 exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode);
1601 if ($exitCode && $this->isRunning()) {
1602 if ($throwException) {
1603 throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output)));
1609 if (!$this->enhanceSigchildCompatibility ||
!$this->isSigchildEnabled()) {
1610 $ok = @proc_terminate
($this->process
, $signal);
1611 } elseif (function_exists('posix_kill')) {
1612 $ok = @posix_kill
($pid, $signal);
1613 } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), array(2 => array('pipe', 'w')), $pipes)) {
1614 $ok = false === fgets($pipes[2]);
1617 if ($throwException) {
1618 throw new RuntimeException(sprintf('Error while sending signal `%s`.', $signal));
1625 $this->latestSignal
= (int) $signal;
1626 $this->fallbackStatus
['signaled'] = true;
1627 $this->fallbackStatus
['exitcode'] = -1;
1628 $this->fallbackStatus
['termsig'] = $this->latestSignal
;
1633 private function prepareWindowsCommandLine($cmd, array &$env)
1635 $uid = uniqid('', true);
1637 $varCache = array();
1638 $cmd = preg_replace_callback(
1642 (?: !LF! | "(?:\^[%!^])?+" )
1646 function ($m) use (&$env, &$varCache, &$varCount, $uid) {
1647 if (!isset($m[1])) {
1650 if (isset($varCache[$m[0]])) {
1651 return $varCache[$m[0]];
1653 if (false !== strpos($value = $m[1], "\0")) {
1654 $value = str_replace("\0", '?', $value);
1656 if (false === strpbrk($value, "\"%!\n")) {
1657 return '"'.$value.'"';
1660 $value = str_replace(array('!LF!', '"^!"', '"^%"', '"^^"', '""'), array("\n", '!', '%', '^', '"'), $value);
1661 $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"';
1662 $var = $uid.++
$varCount;
1664 $env[$var] = $value;
1666 return $varCache[$m[0]] = '!'.$var.'!';
1671 $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')';
1672 foreach ($this->processPipes
->getFiles() as $offset => $filename) {
1673 $cmd .= ' '.$offset.'>"'.$filename.'"';
1680 * Ensures the process is running or terminated, throws a LogicException if the process has a not started.
1682 * @param string $functionName The function name that was called
1684 * @throws LogicException if the process has not run
1686 private function requireProcessIsStarted($functionName)
1688 if (!$this->isStarted()) {
1689 throw new LogicException(sprintf('Process must be started before calling %s.', $functionName));
1694 * Ensures the process is terminated, throws a LogicException if the process has a status different than `terminated`.
1696 * @param string $functionName The function name that was called
1698 * @throws LogicException if the process is not yet terminated
1700 private function requireProcessIsTerminated($functionName)
1702 if (!$this->isTerminated()) {
1703 throw new LogicException(sprintf('Process must be terminated before calling %s.', $functionName));
1708 * Escapes a string to be used as a shell argument.
1710 * @param string $argument The argument that will be escaped
1712 * @return string The escaped argument
1714 private function escapeArgument($argument)
1716 if ('\\' !== DIRECTORY_SEPARATOR
) {
1717 return "'".str_replace("'", "'\\''", $argument)."'";
1719 if ('' === $argument = (string) $argument) {
1722 if (false !== strpos($argument, "\0")) {
1723 $argument = str_replace("\0", '?', $argument);
1725 if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) {
1728 $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument);
1730 return '"'.str_replace(array('"', '^', '%', '!', "\n"), array('""', '"^^"', '"^%"', '"^!"', '!LF!'), $argument).'"';
1733 private function getDefaultEnv()
1737 foreach ($_SERVER as $k => $v) {
1738 if (is_string($v) && false !== $v = getenv($k)) {
1743 foreach ($_ENV as $k => $v) {
1744 if (is_string($v)) {