Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Process; Chris@0: Chris@0: use Symfony\Component\Process\Exception\InvalidArgumentException; Chris@0: use Symfony\Component\Process\Exception\LogicException; Chris@0: use Symfony\Component\Process\Exception\ProcessFailedException; Chris@0: use Symfony\Component\Process\Exception\ProcessTimedOutException; Chris@0: use Symfony\Component\Process\Exception\RuntimeException; Chris@0: use Symfony\Component\Process\Pipes\PipesInterface; Chris@0: use Symfony\Component\Process\Pipes\UnixPipes; Chris@0: use Symfony\Component\Process\Pipes\WindowsPipes; Chris@0: Chris@0: /** Chris@0: * Process is a thin wrapper around proc_* functions to easily Chris@0: * start independent PHP processes. Chris@0: * Chris@0: * @author Fabien Potencier Chris@0: * @author Romain Neutron Chris@0: */ Chris@0: class Process implements \IteratorAggregate Chris@0: { Chris@0: const ERR = 'err'; Chris@0: const OUT = 'out'; Chris@0: Chris@0: const STATUS_READY = 'ready'; Chris@0: const STATUS_STARTED = 'started'; Chris@0: const STATUS_TERMINATED = 'terminated'; Chris@0: Chris@0: const STDIN = 0; Chris@0: const STDOUT = 1; Chris@0: const STDERR = 2; Chris@0: Chris@0: // Timeout Precision in seconds. Chris@0: const TIMEOUT_PRECISION = 0.2; Chris@0: Chris@0: const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking Chris@0: const ITER_KEEP_OUTPUT = 2; // By default, outputs are cleared while iterating, use this flag to keep them in memory Chris@0: const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating Chris@0: const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating Chris@0: Chris@0: private $callback; Chris@0: private $hasCallback = false; Chris@0: private $commandline; Chris@0: private $cwd; Chris@0: private $env; Chris@0: private $input; Chris@0: private $starttime; Chris@0: private $lastOutputTime; Chris@0: private $timeout; Chris@0: private $idleTimeout; Chris@17: private $options = ['suppress_errors' => true]; Chris@0: private $exitcode; Chris@17: private $fallbackStatus = []; Chris@0: private $processInformation; Chris@0: private $outputDisabled = false; Chris@0: private $stdout; Chris@0: private $stderr; Chris@0: private $enhanceWindowsCompatibility = true; Chris@0: private $enhanceSigchildCompatibility; Chris@0: private $process; Chris@0: private $status = self::STATUS_READY; Chris@0: private $incrementalOutputOffset = 0; Chris@0: private $incrementalErrorOutputOffset = 0; Chris@0: private $tty; Chris@0: private $pty; Chris@0: private $inheritEnv = false; Chris@0: Chris@0: private $useFileHandles = false; Chris@0: /** @var PipesInterface */ Chris@0: private $processPipes; Chris@0: Chris@0: private $latestSignal; Chris@0: Chris@0: private static $sigchild; Chris@0: Chris@0: /** Chris@0: * Exit codes translation table. Chris@0: * Chris@0: * User-defined errors must use exit codes in the 64-113 range. Chris@0: */ Chris@17: public static $exitCodes = [ Chris@0: 0 => 'OK', Chris@0: 1 => 'General error', Chris@0: 2 => 'Misuse of shell builtins', Chris@0: Chris@0: 126 => 'Invoked command cannot execute', Chris@0: 127 => 'Command not found', Chris@0: 128 => 'Invalid exit argument', Chris@0: Chris@0: // signals Chris@0: 129 => 'Hangup', Chris@0: 130 => 'Interrupt', Chris@0: 131 => 'Quit and dump core', Chris@0: 132 => 'Illegal instruction', Chris@0: 133 => 'Trace/breakpoint trap', Chris@0: 134 => 'Process aborted', Chris@0: 135 => 'Bus error: "access to undefined portion of memory object"', Chris@0: 136 => 'Floating point exception: "erroneous arithmetic operation"', Chris@0: 137 => 'Kill (terminate immediately)', Chris@0: 138 => 'User-defined 1', Chris@0: 139 => 'Segmentation violation', Chris@0: 140 => 'User-defined 2', Chris@0: 141 => 'Write to pipe with no one reading', Chris@0: 142 => 'Signal raised by alarm', Chris@0: 143 => 'Termination (request to terminate)', Chris@0: // 144 - not defined Chris@0: 145 => 'Child process terminated, stopped (or continued*)', Chris@0: 146 => 'Continue if stopped', Chris@0: 147 => 'Stop executing temporarily', Chris@0: 148 => 'Terminal stop signal', Chris@0: 149 => 'Background process attempting to read from tty ("in")', Chris@0: 150 => 'Background process attempting to write to tty ("out")', Chris@0: 151 => 'Urgent data available on socket', Chris@0: 152 => 'CPU time limit exceeded', Chris@0: 153 => 'File size limit exceeded', Chris@0: 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', Chris@0: 155 => 'Profiling timer expired', Chris@0: // 156 - not defined Chris@0: 157 => 'Pollable event', Chris@0: // 158 - not defined Chris@0: 159 => 'Bad syscall', Chris@17: ]; Chris@0: Chris@0: /** Chris@14: * @param string|array $commandline The command line to run Chris@0: * @param string|null $cwd The working directory or null to use the working dir of the current PHP process Chris@0: * @param array|null $env The environment variables or null to use the same environment as the current PHP process Chris@0: * @param mixed|null $input The input as stream resource, scalar or \Traversable, or null for no input Chris@0: * @param int|float|null $timeout The timeout in seconds or null to disable Chris@0: * @param array $options An array of options for proc_open Chris@0: * Chris@0: * @throws RuntimeException When proc_open is not installed Chris@0: */ Chris@14: public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = null) Chris@0: { Chris@17: if (!\function_exists('proc_open')) { Chris@0: throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.'); Chris@0: } Chris@0: Chris@0: $this->commandline = $commandline; Chris@0: $this->cwd = $cwd; Chris@0: Chris@0: // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started Chris@0: // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected Chris@0: // @see : https://bugs.php.net/bug.php?id=51800 Chris@0: // @see : https://bugs.php.net/bug.php?id=50524 Chris@17: if (null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR)) { Chris@0: $this->cwd = getcwd(); Chris@0: } Chris@0: if (null !== $env) { Chris@0: $this->setEnv($env); Chris@0: } Chris@0: Chris@0: $this->setInput($input); Chris@0: $this->setTimeout($timeout); Chris@17: $this->useFileHandles = '\\' === \DIRECTORY_SEPARATOR; Chris@0: $this->pty = false; Chris@17: $this->enhanceSigchildCompatibility = '\\' !== \DIRECTORY_SEPARATOR && $this->isSigchildEnabled(); Chris@14: if (null !== $options) { Chris@14: @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); Chris@14: $this->options = array_replace($this->options, $options); Chris@14: } Chris@0: } Chris@0: Chris@0: public function __destruct() Chris@0: { Chris@0: $this->stop(0); Chris@0: } Chris@0: Chris@0: public function __clone() Chris@0: { Chris@0: $this->resetProcessData(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Runs the process. Chris@0: * Chris@0: * The callback receives the type of output (out or err) and Chris@0: * some bytes from the output in real-time. It allows to have feedback Chris@0: * from the independent process during execution. Chris@0: * Chris@0: * The STDOUT and STDERR are also available after the process is finished Chris@0: * via the getOutput() and getErrorOutput() methods. Chris@0: * Chris@0: * @param callable|null $callback A PHP callback to run whenever there is some Chris@0: * output available on STDOUT or STDERR Chris@14: * @param array $env An array of additional env vars to set when running the process Chris@0: * Chris@0: * @return int The exit status code Chris@0: * Chris@0: * @throws RuntimeException When process can't be launched Chris@0: * @throws RuntimeException When process stopped after receiving signal Chris@0: * @throws LogicException In case a callback is provided and output has been disabled Chris@14: * Chris@14: * @final since version 3.3 Chris@0: */ Chris@17: public function run($callback = null/*, array $env = []*/) Chris@0: { Chris@17: $env = 1 < \func_num_args() ? func_get_arg(1) : null; Chris@14: $this->start($callback, $env); Chris@0: Chris@0: return $this->wait(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Runs the process. Chris@0: * Chris@0: * This is identical to run() except that an exception is thrown if the process Chris@0: * exits with a non-zero exit code. Chris@0: * Chris@0: * @param callable|null $callback Chris@14: * @param array $env An array of additional env vars to set when running the process Chris@0: * Chris@0: * @return self Chris@0: * Chris@0: * @throws RuntimeException if PHP was compiled with --enable-sigchild and the enhanced sigchild compatibility mode is not enabled Chris@0: * @throws ProcessFailedException if the process didn't terminate successfully Chris@14: * Chris@14: * @final since version 3.3 Chris@0: */ Chris@17: public function mustRun(callable $callback = null/*, array $env = []*/) Chris@0: { Chris@0: if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { Chris@0: throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); Chris@0: } Chris@17: $env = 1 < \func_num_args() ? func_get_arg(1) : null; Chris@0: Chris@14: if (0 !== $this->run($callback, $env)) { Chris@0: throw new ProcessFailedException($this); Chris@0: } Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Starts the process and returns after writing the input to STDIN. Chris@0: * Chris@0: * This method blocks until all STDIN data is sent to the process then it Chris@0: * returns while the process runs in the background. Chris@0: * Chris@0: * The termination of the process can be awaited with wait(). Chris@0: * Chris@0: * The callback receives the type of output (out or err) and some bytes from Chris@0: * the output in real-time while writing the standard input to the process. Chris@0: * It allows to have feedback from the independent process during execution. Chris@0: * Chris@0: * @param callable|null $callback A PHP callback to run whenever there is some Chris@0: * output available on STDOUT or STDERR Chris@14: * @param array $env An array of additional env vars to set when running the process Chris@0: * Chris@0: * @throws RuntimeException When process can't be launched Chris@0: * @throws RuntimeException When process is already running Chris@0: * @throws LogicException In case a callback is provided and output has been disabled Chris@0: */ Chris@17: public function start(callable $callback = null/*, array $env = [*/) Chris@0: { Chris@0: if ($this->isRunning()) { Chris@0: throw new RuntimeException('Process is already running'); Chris@0: } Chris@17: if (2 <= \func_num_args()) { Chris@14: $env = func_get_arg(1); Chris@14: } else { Chris@14: if (__CLASS__ !== static::class) { Chris@14: $r = new \ReflectionMethod($this, __FUNCTION__); Chris@17: if (__CLASS__ !== $r->getDeclaringClass()->getName() && (2 > $r->getNumberOfParameters() || 'env' !== $r->getParameters()[1]->name)) { Chris@14: @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); Chris@14: } Chris@14: } Chris@14: $env = null; Chris@14: } Chris@0: Chris@0: $this->resetProcessData(); Chris@0: $this->starttime = $this->lastOutputTime = microtime(true); Chris@0: $this->callback = $this->buildCallback($callback); Chris@0: $this->hasCallback = null !== $callback; Chris@0: $descriptors = $this->getDescriptors(); Chris@0: $inheritEnv = $this->inheritEnv; Chris@0: Chris@17: if (\is_array($commandline = $this->commandline)) { Chris@17: $commandline = implode(' ', array_map([$this, 'escapeArgument'], $commandline)); Chris@0: Chris@17: if ('\\' !== \DIRECTORY_SEPARATOR) { Chris@14: // exec is mandatory to deal with sending a signal to the process Chris@14: $commandline = 'exec '.$commandline; Chris@14: } Chris@14: } Chris@14: Chris@14: if (null === $env) { Chris@14: $env = $this->env; Chris@14: } else { Chris@14: if ($this->env) { Chris@14: $env += $this->env; Chris@14: } Chris@14: $inheritEnv = true; Chris@14: } Chris@14: Chris@0: if (null !== $env && $inheritEnv) { Chris@14: $env += $this->getDefaultEnv(); Chris@14: } elseif (null !== $env) { Chris@14: @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); Chris@14: } else { Chris@14: $env = $this->getDefaultEnv(); Chris@0: } Chris@17: if ('\\' === \DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) { Chris@14: $this->options['bypass_shell'] = true; Chris@14: $commandline = $this->prepareWindowsCommandLine($commandline, $env); Chris@0: } elseif (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { Chris@0: // last exit code is output on the fourth pipe and caught to work around --enable-sigchild Chris@17: $descriptors[3] = ['pipe', 'w']; Chris@0: Chris@0: // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input Chris@14: $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;'; Chris@0: $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code'; Chris@0: Chris@0: // Workaround for the bug, when PTS functionality is enabled. Chris@0: // @see : https://bugs.php.net/69442 Chris@0: $ptsWorkaround = fopen(__FILE__, 'r'); Chris@0: } Chris@17: if (\defined('HHVM_VERSION')) { Chris@14: $envPairs = $env; Chris@14: } else { Chris@17: $envPairs = []; Chris@14: foreach ($env as $k => $v) { Chris@14: if (false !== $v) { Chris@14: $envPairs[] = $k.'='.$v; Chris@14: } Chris@14: } Chris@14: } Chris@0: Chris@14: if (!is_dir($this->cwd)) { Chris@14: @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); Chris@14: } Chris@0: Chris@14: $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); Chris@0: Chris@17: if (!\is_resource($this->process)) { Chris@0: throw new RuntimeException('Unable to launch a new process.'); Chris@0: } Chris@0: $this->status = self::STATUS_STARTED; Chris@0: Chris@0: if (isset($descriptors[3])) { Chris@0: $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]); Chris@0: } Chris@0: Chris@0: if ($this->tty) { Chris@0: return; Chris@0: } Chris@0: Chris@0: $this->updateStatus(false); Chris@0: $this->checkTimeout(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Restarts the process. Chris@0: * Chris@0: * Be warned that the process is cloned before being started. Chris@0: * Chris@0: * @param callable|null $callback A PHP callback to run whenever there is some Chris@0: * output available on STDOUT or STDERR Chris@14: * @param array $env An array of additional env vars to set when running the process Chris@0: * Chris@0: * @return $this Chris@0: * Chris@0: * @throws RuntimeException When process can't be launched Chris@0: * @throws RuntimeException When process is already running Chris@0: * Chris@0: * @see start() Chris@14: * Chris@14: * @final since version 3.3 Chris@0: */ Chris@17: public function restart(callable $callback = null/*, array $env = []*/) Chris@0: { Chris@0: if ($this->isRunning()) { Chris@0: throw new RuntimeException('Process is already running'); Chris@0: } Chris@17: $env = 1 < \func_num_args() ? func_get_arg(1) : null; Chris@0: Chris@0: $process = clone $this; Chris@14: $process->start($callback, $env); Chris@0: Chris@0: return $process; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Waits for the process to terminate. Chris@0: * Chris@0: * The callback receives the type of output (out or err) and some bytes Chris@0: * from the output in real-time while writing the standard input to the process. Chris@0: * It allows to have feedback from the independent process during execution. Chris@0: * Chris@0: * @param callable|null $callback A valid PHP callback Chris@0: * Chris@0: * @return int The exitcode of the process Chris@0: * Chris@0: * @throws RuntimeException When process timed out Chris@0: * @throws RuntimeException When process stopped after receiving signal Chris@0: * @throws LogicException When process is not yet started Chris@0: */ Chris@0: public function wait(callable $callback = null) Chris@0: { Chris@0: $this->requireProcessIsStarted(__FUNCTION__); Chris@0: Chris@0: $this->updateStatus(false); Chris@0: Chris@0: if (null !== $callback) { Chris@0: if (!$this->processPipes->haveReadSupport()) { Chris@0: $this->stop(0); Chris@0: throw new \LogicException('Pass the callback to the Process::start method or enableOutput to use a callback with Process::wait'); Chris@0: } Chris@0: $this->callback = $this->buildCallback($callback); Chris@0: } Chris@0: Chris@0: do { Chris@0: $this->checkTimeout(); Chris@17: $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); Chris@17: $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running); Chris@0: } while ($running); Chris@0: Chris@0: while ($this->isRunning()) { Chris@0: usleep(1000); Chris@0: } Chris@0: Chris@0: if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) { Chris@0: throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig'])); Chris@0: } Chris@0: Chris@0: return $this->exitcode; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the Pid (process identifier), if applicable. Chris@0: * Chris@0: * @return int|null The process id if running, null otherwise Chris@0: */ Chris@0: public function getPid() Chris@0: { Chris@0: return $this->isRunning() ? $this->processInformation['pid'] : null; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sends a POSIX signal to the process. Chris@0: * Chris@0: * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php) Chris@0: * Chris@0: * @return $this Chris@0: * Chris@0: * @throws LogicException In case the process is not running Chris@0: * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed Chris@0: * @throws RuntimeException In case of failure Chris@0: */ Chris@0: public function signal($signal) Chris@0: { Chris@0: $this->doSignal($signal, true); Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Disables fetching output and error output from the underlying process. Chris@0: * Chris@0: * @return $this Chris@0: * Chris@0: * @throws RuntimeException In case the process is already running Chris@0: * @throws LogicException if an idle timeout is set Chris@0: */ Chris@0: public function disableOutput() Chris@0: { Chris@0: if ($this->isRunning()) { Chris@0: throw new RuntimeException('Disabling output while the process is running is not possible.'); Chris@0: } Chris@0: if (null !== $this->idleTimeout) { Chris@0: throw new LogicException('Output can not be disabled while an idle timeout is set.'); Chris@0: } Chris@0: Chris@0: $this->outputDisabled = true; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Enables fetching output and error output from the underlying process. Chris@0: * Chris@0: * @return $this Chris@0: * Chris@0: * @throws RuntimeException In case the process is already running Chris@0: */ Chris@0: public function enableOutput() Chris@0: { Chris@0: if ($this->isRunning()) { Chris@0: throw new RuntimeException('Enabling output while the process is running is not possible.'); Chris@0: } Chris@0: Chris@0: $this->outputDisabled = false; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns true in case the output is disabled, false otherwise. Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: public function isOutputDisabled() Chris@0: { Chris@0: return $this->outputDisabled; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the current output of the process (STDOUT). Chris@0: * Chris@0: * @return string The process output Chris@0: * Chris@0: * @throws LogicException in case the output has been disabled Chris@0: * @throws LogicException In case the process is not started Chris@0: */ Chris@0: public function getOutput() Chris@0: { Chris@0: $this->readPipesForOutput(__FUNCTION__); Chris@0: Chris@0: if (false === $ret = stream_get_contents($this->stdout, -1, 0)) { Chris@0: return ''; Chris@0: } Chris@0: Chris@0: return $ret; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the output incrementally. Chris@0: * Chris@0: * In comparison with the getOutput method which always return the whole Chris@0: * output, this one returns the new output since the last call. Chris@0: * Chris@0: * @return string The process output since the last call Chris@0: * Chris@0: * @throws LogicException in case the output has been disabled Chris@0: * @throws LogicException In case the process is not started Chris@0: */ Chris@0: public function getIncrementalOutput() Chris@0: { Chris@0: $this->readPipesForOutput(__FUNCTION__); Chris@0: Chris@0: $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); Chris@0: $this->incrementalOutputOffset = ftell($this->stdout); Chris@0: Chris@0: if (false === $latest) { Chris@0: return ''; Chris@0: } Chris@0: Chris@0: return $latest; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR). Chris@0: * Chris@0: * @param int $flags A bit field of Process::ITER_* flags Chris@0: * Chris@0: * @throws LogicException in case the output has been disabled Chris@0: * @throws LogicException In case the process is not started Chris@0: * Chris@0: * @return \Generator Chris@0: */ Chris@0: public function getIterator($flags = 0) Chris@0: { Chris@0: $this->readPipesForOutput(__FUNCTION__, false); Chris@0: Chris@0: $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags); Chris@0: $blocking = !(self::ITER_NON_BLOCKING & $flags); Chris@0: $yieldOut = !(self::ITER_SKIP_OUT & $flags); Chris@0: $yieldErr = !(self::ITER_SKIP_ERR & $flags); Chris@0: Chris@0: while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) { Chris@0: if ($yieldOut) { Chris@0: $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); Chris@0: Chris@0: if (isset($out[0])) { Chris@0: if ($clearOutput) { Chris@0: $this->clearOutput(); Chris@0: } else { Chris@0: $this->incrementalOutputOffset = ftell($this->stdout); Chris@0: } Chris@0: Chris@0: yield self::OUT => $out; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($yieldErr) { Chris@0: $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); Chris@0: Chris@0: if (isset($err[0])) { Chris@0: if ($clearOutput) { Chris@0: $this->clearErrorOutput(); Chris@0: } else { Chris@0: $this->incrementalErrorOutputOffset = ftell($this->stderr); Chris@0: } Chris@0: Chris@0: yield self::ERR => $err; Chris@0: } Chris@0: } Chris@0: Chris@0: if (!$blocking && !isset($out[0]) && !isset($err[0])) { Chris@0: yield self::OUT => ''; Chris@0: } Chris@0: Chris@0: $this->checkTimeout(); Chris@0: $this->readPipesForOutput(__FUNCTION__, $blocking); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Clears the process output. Chris@0: * Chris@0: * @return $this Chris@0: */ Chris@0: public function clearOutput() Chris@0: { Chris@0: ftruncate($this->stdout, 0); Chris@0: fseek($this->stdout, 0); Chris@0: $this->incrementalOutputOffset = 0; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the current error output of the process (STDERR). Chris@0: * Chris@0: * @return string The process error output Chris@0: * Chris@0: * @throws LogicException in case the output has been disabled Chris@0: * @throws LogicException In case the process is not started Chris@0: */ Chris@0: public function getErrorOutput() Chris@0: { Chris@0: $this->readPipesForOutput(__FUNCTION__); Chris@0: Chris@0: if (false === $ret = stream_get_contents($this->stderr, -1, 0)) { Chris@0: return ''; Chris@0: } Chris@0: Chris@0: return $ret; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the errorOutput incrementally. Chris@0: * Chris@0: * In comparison with the getErrorOutput method which always return the Chris@0: * whole error output, this one returns the new error output since the last Chris@0: * call. Chris@0: * Chris@0: * @return string The process error output since the last call Chris@0: * Chris@0: * @throws LogicException in case the output has been disabled Chris@0: * @throws LogicException In case the process is not started Chris@0: */ Chris@0: public function getIncrementalErrorOutput() Chris@0: { Chris@0: $this->readPipesForOutput(__FUNCTION__); Chris@0: Chris@0: $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); Chris@0: $this->incrementalErrorOutputOffset = ftell($this->stderr); Chris@0: Chris@0: if (false === $latest) { Chris@0: return ''; Chris@0: } Chris@0: Chris@0: return $latest; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Clears the process output. Chris@0: * Chris@0: * @return $this Chris@0: */ Chris@0: public function clearErrorOutput() Chris@0: { Chris@0: ftruncate($this->stderr, 0); Chris@0: fseek($this->stderr, 0); Chris@0: $this->incrementalErrorOutputOffset = 0; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the exit code returned by the process. Chris@0: * Chris@17: * @return int|null The exit status code, null if the Process is not terminated Chris@0: * Chris@0: * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled Chris@0: */ Chris@0: public function getExitCode() Chris@0: { Chris@0: if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { Chris@0: throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); Chris@0: } Chris@0: Chris@0: $this->updateStatus(false); Chris@0: Chris@0: return $this->exitcode; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns a string representation for the exit code returned by the process. Chris@0: * Chris@0: * This method relies on the Unix exit code status standardization Chris@0: * and might not be relevant for other operating systems. Chris@0: * Chris@17: * @return string|null A string representation for the exit status code, null if the Process is not terminated Chris@0: * Chris@0: * @see http://tldp.org/LDP/abs/html/exitcodes.html Chris@0: * @see http://en.wikipedia.org/wiki/Unix_signal Chris@0: */ Chris@0: public function getExitCodeText() Chris@0: { Chris@0: if (null === $exitcode = $this->getExitCode()) { Chris@0: return; Chris@0: } Chris@0: Chris@0: return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error'; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Checks if the process ended successfully. Chris@0: * Chris@0: * @return bool true if the process ended successfully, false otherwise Chris@0: */ Chris@0: public function isSuccessful() Chris@0: { Chris@0: return 0 === $this->getExitCode(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns true if the child process has been terminated by an uncaught signal. Chris@0: * Chris@0: * It always returns false on Windows. Chris@0: * Chris@0: * @return bool Chris@0: * Chris@0: * @throws RuntimeException In case --enable-sigchild is activated Chris@0: * @throws LogicException In case the process is not terminated Chris@0: */ Chris@0: public function hasBeenSignaled() Chris@0: { Chris@0: $this->requireProcessIsTerminated(__FUNCTION__); Chris@0: Chris@0: if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { Chris@0: throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); Chris@0: } Chris@0: Chris@0: return $this->processInformation['signaled']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the number of the signal that caused the child process to terminate its execution. Chris@0: * Chris@0: * It is only meaningful if hasBeenSignaled() returns true. Chris@0: * Chris@0: * @return int Chris@0: * Chris@0: * @throws RuntimeException In case --enable-sigchild is activated Chris@0: * @throws LogicException In case the process is not terminated Chris@0: */ Chris@0: public function getTermSignal() Chris@0: { Chris@0: $this->requireProcessIsTerminated(__FUNCTION__); Chris@0: Chris@0: if ($this->isSigchildEnabled() && (!$this->enhanceSigchildCompatibility || -1 === $this->processInformation['termsig'])) { Chris@0: throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); Chris@0: } Chris@0: Chris@0: return $this->processInformation['termsig']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns true if the child process has been stopped by a signal. Chris@0: * Chris@0: * It always returns false on Windows. Chris@0: * Chris@0: * @return bool Chris@0: * Chris@0: * @throws LogicException In case the process is not terminated Chris@0: */ Chris@0: public function hasBeenStopped() Chris@0: { Chris@0: $this->requireProcessIsTerminated(__FUNCTION__); Chris@0: Chris@0: return $this->processInformation['stopped']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the number of the signal that caused the child process to stop its execution. Chris@0: * Chris@0: * It is only meaningful if hasBeenStopped() returns true. Chris@0: * Chris@0: * @return int Chris@0: * Chris@0: * @throws LogicException In case the process is not terminated Chris@0: */ Chris@0: public function getStopSignal() Chris@0: { Chris@0: $this->requireProcessIsTerminated(__FUNCTION__); Chris@0: Chris@0: return $this->processInformation['stopsig']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Checks if the process is currently running. Chris@0: * Chris@0: * @return bool true if the process is currently running, false otherwise Chris@0: */ Chris@0: public function isRunning() Chris@0: { Chris@0: if (self::STATUS_STARTED !== $this->status) { Chris@0: return false; Chris@0: } Chris@0: Chris@0: $this->updateStatus(false); Chris@0: Chris@0: return $this->processInformation['running']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Checks if the process has been started with no regard to the current state. Chris@0: * Chris@0: * @return bool true if status is ready, false otherwise Chris@0: */ Chris@0: public function isStarted() Chris@0: { Chris@14: return self::STATUS_READY != $this->status; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Checks if the process is terminated. Chris@0: * Chris@0: * @return bool true if process is terminated, false otherwise Chris@0: */ Chris@0: public function isTerminated() Chris@0: { Chris@0: $this->updateStatus(false); Chris@0: Chris@14: return self::STATUS_TERMINATED == $this->status; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the process status. Chris@0: * Chris@0: * The status is one of: ready, started, terminated. Chris@0: * Chris@0: * @return string The current process status Chris@0: */ Chris@0: public function getStatus() Chris@0: { Chris@0: $this->updateStatus(false); Chris@0: Chris@0: return $this->status; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Stops the process. Chris@0: * Chris@0: * @param int|float $timeout The timeout in seconds Chris@0: * @param int $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9) Chris@0: * Chris@0: * @return int The exit-code of the process Chris@0: */ Chris@0: public function stop($timeout = 10, $signal = null) Chris@0: { Chris@0: $timeoutMicro = microtime(true) + $timeout; Chris@0: if ($this->isRunning()) { Chris@0: // given `SIGTERM` may not be defined and that `proc_terminate` uses the constant value and not the constant itself, we use the same here Chris@0: $this->doSignal(15, false); Chris@0: do { Chris@0: usleep(1000); Chris@0: } while ($this->isRunning() && microtime(true) < $timeoutMicro); Chris@0: Chris@0: if ($this->isRunning()) { Chris@0: // Avoid exception here: process is supposed to be running, but it might have stopped just Chris@0: // after this line. In any case, let's silently discard the error, we cannot do anything. Chris@0: $this->doSignal($signal ?: 9, false); Chris@0: } Chris@0: } Chris@0: Chris@0: if ($this->isRunning()) { Chris@0: if (isset($this->fallbackStatus['pid'])) { Chris@0: unset($this->fallbackStatus['pid']); Chris@0: Chris@0: return $this->stop(0, $signal); Chris@0: } Chris@0: $this->close(); Chris@0: } Chris@0: Chris@0: return $this->exitcode; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Adds a line to the STDOUT stream. Chris@0: * Chris@0: * @internal Chris@0: * Chris@0: * @param string $line The line to append Chris@0: */ Chris@0: public function addOutput($line) Chris@0: { Chris@0: $this->lastOutputTime = microtime(true); Chris@0: Chris@0: fseek($this->stdout, 0, SEEK_END); Chris@0: fwrite($this->stdout, $line); Chris@0: fseek($this->stdout, $this->incrementalOutputOffset); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Adds a line to the STDERR stream. Chris@0: * Chris@0: * @internal Chris@0: * Chris@0: * @param string $line The line to append Chris@0: */ Chris@0: public function addErrorOutput($line) Chris@0: { Chris@0: $this->lastOutputTime = microtime(true); Chris@0: Chris@0: fseek($this->stderr, 0, SEEK_END); Chris@0: fwrite($this->stderr, $line); Chris@0: fseek($this->stderr, $this->incrementalErrorOutputOffset); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the command line to be executed. Chris@0: * Chris@0: * @return string The command to execute Chris@0: */ Chris@0: public function getCommandLine() Chris@0: { Chris@17: return \is_array($this->commandline) ? implode(' ', array_map([$this, 'escapeArgument'], $this->commandline)) : $this->commandline; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the command line to be executed. Chris@0: * Chris@14: * @param string|array $commandline The command to execute Chris@0: * Chris@0: * @return self The current Process instance Chris@0: */ Chris@0: public function setCommandLine($commandline) Chris@0: { Chris@0: $this->commandline = $commandline; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the process timeout (max. runtime). Chris@0: * Chris@0: * @return float|null The timeout in seconds or null if it's disabled Chris@0: */ Chris@0: public function getTimeout() Chris@0: { Chris@0: return $this->timeout; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the process idle timeout (max. time since last output). Chris@0: * Chris@0: * @return float|null The timeout in seconds or null if it's disabled Chris@0: */ Chris@0: public function getIdleTimeout() Chris@0: { Chris@0: return $this->idleTimeout; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the process timeout (max. runtime). Chris@0: * Chris@0: * To disable the timeout, set this value to null. Chris@0: * Chris@0: * @param int|float|null $timeout The timeout in seconds Chris@0: * Chris@0: * @return self The current Process instance Chris@0: * Chris@0: * @throws InvalidArgumentException if the timeout is negative Chris@0: */ Chris@0: public function setTimeout($timeout) Chris@0: { Chris@0: $this->timeout = $this->validateTimeout($timeout); Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the process idle timeout (max. time since last output). Chris@0: * Chris@0: * To disable the timeout, set this value to null. Chris@0: * Chris@0: * @param int|float|null $timeout The timeout in seconds Chris@0: * Chris@0: * @return self The current Process instance Chris@0: * Chris@0: * @throws LogicException if the output is disabled Chris@0: * @throws InvalidArgumentException if the timeout is negative Chris@0: */ Chris@0: public function setIdleTimeout($timeout) Chris@0: { Chris@0: if (null !== $timeout && $this->outputDisabled) { Chris@0: throw new LogicException('Idle timeout can not be set while the output is disabled.'); Chris@0: } Chris@0: Chris@0: $this->idleTimeout = $this->validateTimeout($timeout); Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Enables or disables the TTY mode. Chris@0: * Chris@0: * @param bool $tty True to enabled and false to disable Chris@0: * Chris@0: * @return self The current Process instance Chris@0: * Chris@0: * @throws RuntimeException In case the TTY mode is not supported Chris@0: */ Chris@0: public function setTty($tty) Chris@0: { Chris@17: if ('\\' === \DIRECTORY_SEPARATOR && $tty) { Chris@0: throw new RuntimeException('TTY mode is not supported on Windows platform.'); Chris@0: } Chris@0: if ($tty) { Chris@0: static $isTtySupported; Chris@0: Chris@0: if (null === $isTtySupported) { Chris@17: $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes); Chris@0: } Chris@0: Chris@0: if (!$isTtySupported) { Chris@0: throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.'); Chris@0: } Chris@0: } Chris@0: Chris@0: $this->tty = (bool) $tty; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Checks if the TTY mode is enabled. Chris@0: * Chris@0: * @return bool true if the TTY mode is enabled, false otherwise Chris@0: */ Chris@0: public function isTty() Chris@0: { Chris@0: return $this->tty; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets PTY mode. Chris@0: * Chris@0: * @param bool $bool Chris@0: * Chris@0: * @return self Chris@0: */ Chris@0: public function setPty($bool) Chris@0: { Chris@0: $this->pty = (bool) $bool; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns PTY state. Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: public function isPty() Chris@0: { Chris@0: return $this->pty; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the working directory. Chris@0: * Chris@0: * @return string|null The current working directory or null on failure Chris@0: */ Chris@0: public function getWorkingDirectory() Chris@0: { Chris@0: if (null === $this->cwd) { Chris@0: // getcwd() will return false if any one of the parent directories does not have Chris@0: // the readable or search mode set, even if the current directory does Chris@0: return getcwd() ?: null; Chris@0: } Chris@0: Chris@0: return $this->cwd; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the current working directory. Chris@0: * Chris@0: * @param string $cwd The new working directory Chris@0: * Chris@0: * @return self The current Process instance Chris@0: */ Chris@0: public function setWorkingDirectory($cwd) Chris@0: { Chris@0: $this->cwd = $cwd; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the environment variables. Chris@0: * Chris@0: * @return array The current environment variables Chris@0: */ Chris@0: public function getEnv() Chris@0: { Chris@0: return $this->env; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the environment variables. Chris@0: * Chris@14: * Each environment variable value should be a string. Chris@0: * If it is an array, the variable is ignored. Chris@14: * If it is false or null, it will be removed when Chris@14: * env vars are otherwise inherited. Chris@0: * Chris@0: * That happens in PHP when 'argv' is registered into Chris@0: * the $_ENV array for instance. Chris@0: * Chris@0: * @param array $env The new environment variables Chris@0: * Chris@0: * @return self The current Process instance Chris@0: */ Chris@0: public function setEnv(array $env) Chris@0: { Chris@0: // Process can not handle env values that are arrays Chris@0: $env = array_filter($env, function ($value) { Chris@17: return !\is_array($value); Chris@0: }); Chris@0: Chris@0: $this->env = $env; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the Process input. Chris@0: * Chris@0: * @return resource|string|\Iterator|null The Process input Chris@0: */ Chris@0: public function getInput() Chris@0: { Chris@0: return $this->input; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the input. Chris@0: * Chris@0: * This content will be passed to the underlying process standard input. Chris@0: * Chris@14: * @param string|int|float|bool|resource|\Traversable|null $input The content Chris@0: * Chris@0: * @return self The current Process instance Chris@0: * Chris@0: * @throws LogicException In case the process is running Chris@0: */ Chris@0: public function setInput($input) Chris@0: { Chris@0: if ($this->isRunning()) { Chris@0: throw new LogicException('Input can not be set while the process is running.'); Chris@0: } Chris@0: Chris@0: $this->input = ProcessUtils::validateInput(__METHOD__, $input); Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets the options for proc_open. Chris@0: * Chris@0: * @return array The current options Chris@14: * Chris@14: * @deprecated since version 3.3, to be removed in 4.0. Chris@0: */ Chris@0: public function getOptions() Chris@0: { Chris@14: @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0.', __METHOD__), E_USER_DEPRECATED); Chris@14: Chris@0: return $this->options; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the options for proc_open. Chris@0: * Chris@0: * @param array $options The new options Chris@0: * Chris@0: * @return self The current Process instance Chris@14: * Chris@14: * @deprecated since version 3.3, to be removed in 4.0. Chris@0: */ Chris@0: public function setOptions(array $options) Chris@0: { Chris@14: @trigger_error(sprintf('The %s() method is deprecated since Symfony 3.3 and will be removed in 4.0.', __METHOD__), E_USER_DEPRECATED); Chris@14: Chris@0: $this->options = $options; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Gets whether or not Windows compatibility is enabled. Chris@0: * Chris@0: * This is true by default. Chris@0: * Chris@0: * @return bool Chris@14: * Chris@14: * @deprecated since version 3.3, to be removed in 4.0. Enhanced Windows compatibility will always be enabled. Chris@0: */ Chris@0: public function getEnhanceWindowsCompatibility() Chris@0: { Chris@14: @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); Chris@14: Chris@0: return $this->enhanceWindowsCompatibility; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets whether or not Windows compatibility is enabled. Chris@0: * Chris@0: * @param bool $enhance Chris@0: * Chris@0: * @return self The current Process instance Chris@14: * Chris@14: * @deprecated since version 3.3, to be removed in 4.0. Enhanced Windows compatibility will always be enabled. Chris@0: */ Chris@0: public function setEnhanceWindowsCompatibility($enhance) Chris@0: { Chris@14: @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); Chris@14: Chris@0: $this->enhanceWindowsCompatibility = (bool) $enhance; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns whether sigchild compatibility mode is activated or not. Chris@0: * Chris@0: * @return bool Chris@14: * Chris@14: * @deprecated since version 3.3, to be removed in 4.0. Sigchild compatibility will always be enabled. Chris@0: */ Chris@0: public function getEnhanceSigchildCompatibility() Chris@0: { Chris@14: @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); Chris@14: Chris@0: return $this->enhanceSigchildCompatibility; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Activates sigchild compatibility mode. Chris@0: * Chris@0: * Sigchild compatibility mode is required to get the exit code and Chris@0: * determine the success of a process when PHP has been compiled with Chris@0: * the --enable-sigchild option Chris@0: * Chris@0: * @param bool $enhance Chris@0: * Chris@0: * @return self The current Process instance Chris@14: * Chris@14: * @deprecated since version 3.3, to be removed in 4.0. Chris@0: */ Chris@0: public function setEnhanceSigchildCompatibility($enhance) Chris@0: { Chris@14: @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); Chris@14: Chris@0: $this->enhanceSigchildCompatibility = (bool) $enhance; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets whether environment variables will be inherited or not. Chris@0: * Chris@0: * @param bool $inheritEnv Chris@0: * Chris@0: * @return self The current Process instance Chris@0: */ Chris@0: public function inheritEnvironmentVariables($inheritEnv = true) Chris@0: { Chris@14: if (!$inheritEnv) { Chris@14: @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); Chris@14: } Chris@14: Chris@0: $this->inheritEnv = (bool) $inheritEnv; Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns whether environment variables will be inherited or not. Chris@0: * Chris@0: * @return bool Chris@14: * Chris@14: * @deprecated since version 3.3, to be removed in 4.0. Environment variables will always be inherited. Chris@0: */ Chris@0: public function areEnvironmentVariablesInherited() Chris@0: { Chris@14: @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); Chris@14: Chris@0: return $this->inheritEnv; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Performs a check between the timeout definition and the time the process started. Chris@0: * Chris@0: * In case you run a background process (with the start method), you should Chris@0: * trigger this method regularly to ensure the process timeout Chris@0: * Chris@0: * @throws ProcessTimedOutException In case the timeout was reached Chris@0: */ Chris@0: public function checkTimeout() Chris@0: { Chris@14: if (self::STATUS_STARTED !== $this->status) { Chris@0: return; Chris@0: } Chris@0: Chris@0: if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { Chris@0: $this->stop(0); Chris@0: Chris@0: throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL); Chris@0: } Chris@0: Chris@0: if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { Chris@0: $this->stop(0); Chris@0: Chris@0: throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns whether PTY is supported on the current operating system. Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: public static function isPtySupported() Chris@0: { Chris@0: static $result; Chris@0: Chris@0: if (null !== $result) { Chris@0: return $result; Chris@0: } Chris@0: Chris@17: if ('\\' === \DIRECTORY_SEPARATOR) { Chris@0: return $result = false; Chris@0: } Chris@0: Chris@17: return $result = (bool) @proc_open('echo 1 >/dev/null', [['pty'], ['pty'], ['pty']], $pipes); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Creates the descriptors needed by the proc_open. Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: private function getDescriptors() Chris@0: { Chris@0: if ($this->input instanceof \Iterator) { Chris@0: $this->input->rewind(); Chris@0: } Chris@17: if ('\\' === \DIRECTORY_SEPARATOR) { Chris@0: $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $this->hasCallback); Chris@0: } else { Chris@0: $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->hasCallback); Chris@0: } Chris@0: Chris@0: return $this->processPipes->getDescriptors(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Builds up the callback used by wait(). Chris@0: * Chris@0: * The callbacks adds all occurred output to the specific buffer and calls Chris@0: * the user callback (if present) with the received output. Chris@0: * Chris@0: * @param callable|null $callback The user defined PHP callback Chris@0: * Chris@0: * @return \Closure A PHP closure Chris@0: */ Chris@0: protected function buildCallback(callable $callback = null) Chris@0: { Chris@0: if ($this->outputDisabled) { Chris@0: return function ($type, $data) use ($callback) { Chris@0: if (null !== $callback) { Chris@17: \call_user_func($callback, $type, $data); Chris@0: } Chris@0: }; Chris@0: } Chris@0: Chris@0: $out = self::OUT; Chris@0: Chris@0: return function ($type, $data) use ($callback, $out) { Chris@0: if ($out == $type) { Chris@0: $this->addOutput($data); Chris@0: } else { Chris@0: $this->addErrorOutput($data); Chris@0: } Chris@0: Chris@0: if (null !== $callback) { Chris@17: \call_user_func($callback, $type, $data); Chris@0: } Chris@0: }; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Updates the status of the process, reads pipes. Chris@0: * Chris@0: * @param bool $blocking Whether to use a blocking read call Chris@0: */ Chris@0: protected function updateStatus($blocking) Chris@0: { Chris@0: if (self::STATUS_STARTED !== $this->status) { Chris@0: return; Chris@0: } Chris@0: Chris@0: $this->processInformation = proc_get_status($this->process); Chris@0: $running = $this->processInformation['running']; Chris@0: Chris@17: $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running); Chris@0: Chris@0: if ($this->fallbackStatus && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { Chris@0: $this->processInformation = $this->fallbackStatus + $this->processInformation; Chris@0: } Chris@0: Chris@0: if (!$running) { Chris@0: $this->close(); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns whether PHP has been compiled with the '--enable-sigchild' option or not. Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: protected function isSigchildEnabled() Chris@0: { Chris@0: if (null !== self::$sigchild) { Chris@0: return self::$sigchild; Chris@0: } Chris@0: Chris@17: if (!\function_exists('phpinfo') || \defined('HHVM_VERSION')) { Chris@0: return self::$sigchild = false; Chris@0: } Chris@0: Chris@0: ob_start(); Chris@0: phpinfo(INFO_GENERAL); Chris@0: Chris@0: return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Reads pipes for the freshest output. Chris@0: * Chris@0: * @param string $caller The name of the method that needs fresh outputs Chris@0: * @param bool $blocking Whether to use blocking calls or not Chris@0: * Chris@0: * @throws LogicException in case output has been disabled or process is not started Chris@0: */ Chris@0: private function readPipesForOutput($caller, $blocking = false) Chris@0: { Chris@0: if ($this->outputDisabled) { Chris@0: throw new LogicException('Output has been disabled.'); Chris@0: } Chris@0: Chris@0: $this->requireProcessIsStarted($caller); Chris@0: Chris@0: $this->updateStatus($blocking); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Validates and returns the filtered timeout. Chris@0: * Chris@0: * @param int|float|null $timeout Chris@0: * Chris@0: * @return float|null Chris@0: * Chris@0: * @throws InvalidArgumentException if the given timeout is a negative number Chris@0: */ Chris@0: private function validateTimeout($timeout) Chris@0: { Chris@0: $timeout = (float) $timeout; Chris@0: Chris@0: if (0.0 === $timeout) { Chris@0: $timeout = null; Chris@0: } elseif ($timeout < 0) { Chris@0: throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); Chris@0: } Chris@0: Chris@0: return $timeout; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Reads pipes, executes callback. Chris@0: * Chris@0: * @param bool $blocking Whether to use blocking calls or not Chris@0: * @param bool $close Whether to close file handles or not Chris@0: */ Chris@0: private function readPipes($blocking, $close) Chris@0: { Chris@0: $result = $this->processPipes->readAndWrite($blocking, $close); Chris@0: Chris@0: $callback = $this->callback; Chris@0: foreach ($result as $type => $data) { Chris@0: if (3 !== $type) { Chris@14: $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data); Chris@0: } elseif (!isset($this->fallbackStatus['signaled'])) { Chris@0: $this->fallbackStatus['exitcode'] = (int) $data; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Closes process resource, closes file handles, sets the exitcode. Chris@0: * Chris@0: * @return int The exitcode Chris@0: */ Chris@0: private function close() Chris@0: { Chris@0: $this->processPipes->close(); Chris@17: if (\is_resource($this->process)) { Chris@0: proc_close($this->process); Chris@0: } Chris@0: $this->exitcode = $this->processInformation['exitcode']; Chris@0: $this->status = self::STATUS_TERMINATED; Chris@0: Chris@0: if (-1 === $this->exitcode) { Chris@0: if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) { Chris@0: // if process has been signaled, no exitcode but a valid termsig, apply Unix convention Chris@0: $this->exitcode = 128 + $this->processInformation['termsig']; Chris@0: } elseif ($this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { Chris@0: $this->processInformation['signaled'] = true; Chris@0: $this->processInformation['termsig'] = -1; Chris@0: } Chris@0: } Chris@0: Chris@0: // Free memory from self-reference callback created by buildCallback Chris@0: // Doing so in other contexts like __destruct or by garbage collector is ineffective Chris@0: // Now pipes are closed, so the callback is no longer necessary Chris@0: $this->callback = null; Chris@0: Chris@0: return $this->exitcode; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Resets data related to the latest run of the process. Chris@0: */ Chris@0: private function resetProcessData() Chris@0: { Chris@0: $this->starttime = null; Chris@0: $this->callback = null; Chris@0: $this->exitcode = null; Chris@17: $this->fallbackStatus = []; Chris@0: $this->processInformation = null; Chris@17: $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+b'); Chris@17: $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+b'); Chris@0: $this->process = null; Chris@0: $this->latestSignal = null; Chris@0: $this->status = self::STATUS_READY; Chris@0: $this->incrementalOutputOffset = 0; Chris@0: $this->incrementalErrorOutputOffset = 0; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sends a POSIX signal to the process. Chris@0: * Chris@0: * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php) Chris@0: * @param bool $throwException Whether to throw exception in case signal failed Chris@0: * Chris@0: * @return bool True if the signal was sent successfully, false otherwise Chris@0: * Chris@0: * @throws LogicException In case the process is not running Chris@0: * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed Chris@0: * @throws RuntimeException In case of failure Chris@0: */ Chris@0: private function doSignal($signal, $throwException) Chris@0: { Chris@0: if (null === $pid = $this->getPid()) { Chris@0: if ($throwException) { Chris@0: throw new LogicException('Can not send signal on a non running process.'); Chris@0: } Chris@0: Chris@0: return false; Chris@0: } Chris@0: Chris@17: if ('\\' === \DIRECTORY_SEPARATOR) { Chris@0: exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode); Chris@0: if ($exitCode && $this->isRunning()) { Chris@0: if ($throwException) { Chris@0: throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output))); Chris@0: } Chris@0: Chris@0: return false; Chris@0: } Chris@0: } else { Chris@0: if (!$this->enhanceSigchildCompatibility || !$this->isSigchildEnabled()) { Chris@0: $ok = @proc_terminate($this->process, $signal); Chris@17: } elseif (\function_exists('posix_kill')) { Chris@0: $ok = @posix_kill($pid, $signal); Chris@17: } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) { Chris@0: $ok = false === fgets($pipes[2]); Chris@0: } Chris@0: if (!$ok) { Chris@0: if ($throwException) { Chris@0: throw new RuntimeException(sprintf('Error while sending signal `%s`.', $signal)); Chris@0: } Chris@0: Chris@0: return false; Chris@0: } Chris@0: } Chris@0: Chris@0: $this->latestSignal = (int) $signal; Chris@0: $this->fallbackStatus['signaled'] = true; Chris@0: $this->fallbackStatus['exitcode'] = -1; Chris@0: $this->fallbackStatus['termsig'] = $this->latestSignal; Chris@0: Chris@0: return true; Chris@0: } Chris@0: Chris@14: private function prepareWindowsCommandLine($cmd, array &$env) Chris@14: { Chris@14: $uid = uniqid('', true); Chris@14: $varCount = 0; Chris@17: $varCache = []; Chris@14: $cmd = preg_replace_callback( Chris@14: '/"(?:( Chris@14: [^"%!^]*+ Chris@14: (?: Chris@14: (?: !LF! | "(?:\^[%!^])?+" ) Chris@14: [^"%!^]*+ Chris@14: )++ Chris@14: ) | [^"]*+ )"/x', Chris@14: function ($m) use (&$env, &$varCache, &$varCount, $uid) { Chris@14: if (!isset($m[1])) { Chris@14: return $m[0]; Chris@14: } Chris@14: if (isset($varCache[$m[0]])) { Chris@14: return $varCache[$m[0]]; Chris@14: } Chris@14: if (false !== strpos($value = $m[1], "\0")) { Chris@14: $value = str_replace("\0", '?', $value); Chris@14: } Chris@14: if (false === strpbrk($value, "\"%!\n")) { Chris@14: return '"'.$value.'"'; Chris@14: } Chris@14: Chris@17: $value = str_replace(['!LF!', '"^!"', '"^%"', '"^^"', '""'], ["\n", '!', '%', '^', '"'], $value); Chris@14: $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"'; Chris@14: $var = $uid.++$varCount; Chris@14: Chris@14: $env[$var] = $value; Chris@14: Chris@14: return $varCache[$m[0]] = '!'.$var.'!'; Chris@14: }, Chris@14: $cmd Chris@14: ); Chris@14: Chris@14: $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; Chris@14: foreach ($this->processPipes->getFiles() as $offset => $filename) { Chris@14: $cmd .= ' '.$offset.'>"'.$filename.'"'; Chris@14: } Chris@14: Chris@14: return $cmd; Chris@14: } Chris@14: Chris@0: /** Chris@0: * Ensures the process is running or terminated, throws a LogicException if the process has a not started. Chris@0: * Chris@0: * @param string $functionName The function name that was called Chris@0: * Chris@14: * @throws LogicException if the process has not run Chris@0: */ Chris@0: private function requireProcessIsStarted($functionName) Chris@0: { Chris@0: if (!$this->isStarted()) { Chris@0: throw new LogicException(sprintf('Process must be started before calling %s.', $functionName)); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Ensures the process is terminated, throws a LogicException if the process has a status different than `terminated`. Chris@0: * Chris@0: * @param string $functionName The function name that was called Chris@0: * Chris@14: * @throws LogicException if the process is not yet terminated Chris@0: */ Chris@0: private function requireProcessIsTerminated($functionName) Chris@0: { Chris@0: if (!$this->isTerminated()) { Chris@0: throw new LogicException(sprintf('Process must be terminated before calling %s.', $functionName)); Chris@0: } Chris@0: } Chris@14: Chris@14: /** Chris@14: * Escapes a string to be used as a shell argument. Chris@14: * Chris@14: * @param string $argument The argument that will be escaped Chris@14: * Chris@14: * @return string The escaped argument Chris@14: */ Chris@14: private function escapeArgument($argument) Chris@14: { Chris@17: if ('\\' !== \DIRECTORY_SEPARATOR) { Chris@14: return "'".str_replace("'", "'\\''", $argument)."'"; Chris@14: } Chris@14: if ('' === $argument = (string) $argument) { Chris@14: return '""'; Chris@14: } Chris@14: if (false !== strpos($argument, "\0")) { Chris@14: $argument = str_replace("\0", '?', $argument); Chris@14: } Chris@14: if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) { Chris@14: return $argument; Chris@14: } Chris@14: $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument); Chris@14: Chris@17: return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"'; Chris@14: } Chris@14: Chris@14: private function getDefaultEnv() Chris@14: { Chris@17: $env = []; Chris@14: Chris@14: foreach ($_SERVER as $k => $v) { Chris@17: if (\is_string($v) && false !== $v = getenv($k)) { Chris@14: $env[$k] = $v; Chris@14: } Chris@14: } Chris@14: Chris@14: foreach ($_ENV as $k => $v) { Chris@17: if (\is_string($v)) { Chris@14: $env[$k] = $v; Chris@14: } Chris@14: } Chris@14: Chris@14: return $env; Chris@14: } Chris@0: }