Source of file Process.php
Size: 37,524 Bytes - Last Modified: 2013-07-17T08:22:21+02:00
/home/theseer/Downloads/Symfony/vendor/symfony/symfony/src/Symfony/Component/Process/Process.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257 | <?php /* * This file is part of the Symfony package. * * (c) Fabien Potencier <fabien@symfony.com> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\InvalidArgumentException; use Symfony\Component\Process\Exception\LogicException; use Symfony\Component\Process\Exception\RuntimeException; /** * Process is a thin wrapper around proc_* functions to ease * start independent PHP processes. * * @author Fabien Potencier <fabien@symfony.com> * * @api */ class Process { const ERR = 'err'; const OUT = 'out'; const STATUS_READY = 'ready'; const STATUS_STARTED = 'started'; const STATUS_TERMINATED = 'terminated'; const STDIN = 0; const STDOUT = 1; const STDERR = 2; // Timeout Precision in seconds. const TIMEOUT_PRECISION = 0.2; private $commandline; private $cwd; private $env; private $stdin; private $starttime; private $timeout; private $options; private $exitcode; private $fallbackExitcode; private $processInformation; private $stdout; private $stderr; private $enhanceWindowsCompatibility; private $enhanceSigchildCompatibility; private $pipes; private $process; private $status = self::STATUS_READY; private $incrementalOutputOffset; private $incrementalErrorOutputOffset; private $tty; private $fileHandles; private $readBytes; private static $sigchild; /** * Exit codes translation table. * * User-defined errors must use exit codes in the 64-113 range. * * @var array */ public static $exitCodes = array( 0 => 'OK', 1 => 'General error', 2 => 'Misuse of shell builtins', 126 => 'Invoked command cannot execute', 127 => 'Command not found', 128 => 'Invalid exit argument', // signals 129 => 'Hangup', 130 => 'Interrupt', 131 => 'Quit and dump core', 132 => 'Illegal instruction', 133 => 'Trace/breakpoint trap', 134 => 'Process aborted', 135 => 'Bus error: "access to undefined portion of memory object"', 136 => 'Floating point exception: "erroneous arithmetic operation"', 137 => 'Kill (terminate immediately)', 138 => 'User-defined 1', 139 => 'Segmentation violation', 140 => 'User-defined 2', 141 => 'Write to pipe with no one reading', 142 => 'Signal raised by alarm', 143 => 'Termination (request to terminate)', // 144 - not defined 145 => 'Child process terminated, stopped (or continued*)', 146 => 'Continue if stopped', 147 => 'Stop executing temporarily', 148 => 'Terminal stop signal', 149 => 'Background process attempting to read from tty ("in")', 150 => 'Background process attempting to write to tty ("out")', 151 => 'Urgent data available on socket', 152 => 'CPU time limit exceeded', 153 => 'File size limit exceeded', 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', 155 => 'Profiling timer expired', // 156 - not defined 157 => 'Pollable event', // 158 - not defined 159 => 'Bad syscall', ); /** * Constructor. * * @param string $commandline The command line to run * @param string $cwd The working directory * @param array $env The environment variables or null to inherit * @param string $stdin The STDIN content * @param integer $timeout The timeout in seconds * @param array $options An array of options for proc_open * * @throws RuntimeException When proc_open is not installed * * @api */ public function __construct($commandline, $cwd = null, array $env = null, $stdin = null, $timeout = 60, array $options = array()) { if (!function_exists('proc_open')) { throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.'); } $this->commandline = $commandline; $this->cwd = $cwd; // on windows, if the cwd changed via chdir(), proc_open defaults to the dir where php was started // on gnu/linux, PHP builds with --enable-maintainer-zts are also affected // @see : https://bugs.php.net/bug.php?id=51800 // @see : https://bugs.php.net/bug.php?id=50524 if (null === $this->cwd && (defined('ZEND_THREAD_SAFE') || defined('PHP_WINDOWS_VERSION_BUILD'))) { $this->cwd = getcwd(); } if (null !== $env) { $this->setEnv($env); } else { $this->env = null; } $this->stdin = $stdin; $this->setTimeout($timeout); $this->enhanceWindowsCompatibility = true; $this->enhanceSigchildCompatibility = !defined('PHP_WINDOWS_VERSION_BUILD') && $this->isSigchildEnabled(); $this->options = array_replace(array('suppress_errors' => true, 'binary_pipes' => true), $options); } public function __destruct() { // stop() will check if we have a process running. $this->stop(); } public function __clone() { $this->exitcode = null; $this->fallbackExitcode = null; $this->processInformation = null; $this->stdout = null; $this->stderr = null; $this->pipes = null; $this->process = null; $this->status = self::STATUS_READY; $this->fileHandles = null; $this->readBytes = null; } /** * Runs the process. * * The callback receives the type of output (out or err) and * some bytes from the output in real-time. It allows to have feedback * from the independent process during execution. * * The STDOUT and STDERR are also available after the process is finished * via the getOutput() and getErrorOutput() methods. * * @param callback|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @return integer The exit status code * * @throws RuntimeException When process can't be launch or is stopped * * @api */ public function run($callback = null) { $this->start($callback); return $this->wait($callback); } /** * Starts the process and returns after sending the STDIN. * * This method blocks until all STDIN data is sent to the process then it * returns while the process runs in the background. * * The termination of the process can be awaited with wait(). * * The callback receives the type of output (out or err) and some bytes from * the output in real-time while writing the standard input to the process. * It allows to have feedback from the independent process during execution. * If there is no callback passed, the wait() method can be called * with true as a second parameter then the callback will get all data occurred * in (and since) the start call. * * @param callback|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @throws RuntimeException When process can't be launch or is stopped * @throws RuntimeException When process is already running */ public function start($callback = null) { if ($this->isRunning()) { throw new RuntimeException('Process is already running'); } $this->starttime = microtime(true); $this->stdout = ''; $this->stderr = ''; $this->incrementalOutputOffset = 0; $this->incrementalErrorOutputOffset = 0; $callback = $this->buildCallback($callback); $descriptors = $this->getDescriptors(); $commandline = $this->commandline; if (defined('PHP_WINDOWS_VERSION_BUILD') && $this->enhanceWindowsCompatibility) { $commandline = 'cmd /V:ON /E:ON /C "'.$commandline.'"'; if (!isset($this->options['bypass_shell'])) { $this->options['bypass_shell'] = true; } } $this->process = proc_open($commandline, $descriptors, $this->pipes, $this->cwd, $this->env, $this->options); if (!is_resource($this->process)) { throw new RuntimeException('Unable to launch a new process.'); } $this->status = self::STATUS_STARTED; foreach ($this->pipes as $pipe) { stream_set_blocking($pipe, false); } if ($this->tty) { $this->status = self::STATUS_TERMINATED; return; } if (null === $this->stdin) { fclose($this->pipes[0]); unset($this->pipes[0]); return; } $writePipes = array($this->pipes[0]); unset($this->pipes[0]); $stdinLen = strlen($this->stdin); $stdinOffset = 0; while ($writePipes) { if (defined('PHP_WINDOWS_VERSION_BUILD')) { $this->processFileHandles($callback); } $r = $this->pipes; $w = $writePipes; $e = null; if (false === $n = @stream_select($r, $w, $e, 0, ceil(static::TIMEOUT_PRECISION * 1E6))) { // if a system call has been interrupted, forget about it, let's try again if ($this->hasSystemCallBeenInterrupted()) { continue; } break; } // nothing has changed, let's wait until the process is ready if (0 === $n) { continue; } if ($w) { $written = fwrite($writePipes[0], (binary) substr($this->stdin, $stdinOffset), 8192); if (false !== $written) { $stdinOffset += $written; } if ($stdinOffset >= $stdinLen) { fclose($writePipes[0]); $writePipes = null; } } foreach ($r as $pipe) { $type = array_search($pipe, $this->pipes); $data = fread($pipe, 8192); if (strlen($data) > 0) { call_user_func($callback, $type == 1 ? self::OUT : self::ERR, $data); } if (false === $data || feof($pipe)) { fclose($pipe); unset($this->pipes[$type]); } } $this->checkTimeout(); } $this->updateStatus(); } /** * Restarts the process. * * Be warned that the process is cloned before being started. * * @param callable $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @return Process The new process * * @throws \RuntimeException When process can't be launch or is stopped * @throws \RuntimeException When process is already running * * @see start() */ public function restart($callback = null) { if ($this->isRunning()) { throw new RuntimeException('Process is already running'); } $process = clone $this; $process->start($callback); return $process; } /** * Waits for the process to terminate. * * The callback receives the type of output (out or err) and some bytes * from the output in real-time while writing the standard input to the process. * It allows to have feedback from the independent process during execution. * * @param callback|null $callback A valid PHP callback * * @return integer The exitcode of the process * * @throws \RuntimeException When process timed out * @throws \RuntimeException When process stopped after receiving signal */ public function wait($callback = null) { $this->updateStatus(); $callback = $this->buildCallback($callback); while ($this->pipes || (defined('PHP_WINDOWS_VERSION_BUILD') && $this->fileHandles)) { if (defined('PHP_WINDOWS_VERSION_BUILD') && $this->fileHandles) { $this->processFileHandles($callback, !$this->pipes); } $this->checkTimeout(); if ($this->pipes) { $r = $this->pipes; $w = null; $e = null; // let's have a look if something changed in streams if (false === $n = @stream_select($r, $w, $e, 0, ceil(static::TIMEOUT_PRECISION * 1E6))) { // if a system call has been interrupted, forget about it, let's try again // otherwise, an error occured, let's reset pipes if (!$this->hasSystemCallBeenInterrupted()) { $this->pipes = array(); } continue; } // nothing has changed if (0 === $n) { continue; } foreach ($r as $pipe) { $type = array_search($pipe, $this->pipes); $data = fread($pipe, 8192); if (strlen($data) > 0) { // last exit code is output and caught to work around --enable-sigchild if (3 == $type) { $this->fallbackExitcode = (int) $data; } else { call_user_func($callback, $type == 1 ? self::OUT : self::ERR, $data); } } if (false === $data || feof($pipe)) { fclose($pipe); unset($this->pipes[$type]); } } } } $this->updateStatus(); if ($this->processInformation['signaled']) { if ($this->isSigchildEnabled()) { throw new RuntimeException('The process has been signaled.'); } throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig'])); } $time = 0; while ($this->isRunning() && $time < 1000000) { $time += 1000; usleep(1000); } $exitcode = proc_close($this->process); if ($this->processInformation['signaled']) { if ($this->isSigchildEnabled()) { throw new RuntimeException('The process has been signaled.'); } throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig'])); } $this->exitcode = $this->processInformation['running'] ? $exitcode : $this->processInformation['exitcode']; if (-1 == $this->exitcode && null !== $this->fallbackExitcode) { $this->exitcode = $this->fallbackExitcode; } return $this->exitcode; } /** * Returns the Pid (process identifier), if applicable. * * @return integer|null The process id if running, null otherwise * * @throws RuntimeException In case --enable-sigchild is activated */ public function getPid() { if ($this->isSigchildEnabled()) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved.'); } $this->updateStatus(); return $this->isRunning() ? $this->processInformation['pid'] : null; } /** * Sends a posix signal to the process. * * @param integer $signal A valid posix signal (see http://www.php.net/manual/en/pcntl.constants.php) * @return Process * * @throws LogicException In case the process is not running * @throws RuntimeException In case --enable-sigchild is activated * @throws RuntimeException In case of failure */ public function signal($signal) { if (!$this->isRunning()) { throw new LogicException('Can not send signal on a non running process.'); } if ($this->isSigchildEnabled()) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); } if (true !== @proc_terminate($this->process, $signal)) { throw new RuntimeException(sprintf('Error while sending signal `%d`.', $signal)); } return $this; } /** * Returns the current output of the process (STDOUT). * * @return string The process output * * @api */ public function getOutput() { $this->updateOutput(); return $this->stdout; } /** * Returns the output incrementally. * * In comparison with the getOutput method which always return the whole * output, this one returns the new output since the last call. * * @return string The process output since the last call */ public function getIncrementalOutput() { $data = $this->getOutput(); $latest = substr($data, $this->incrementalOutputOffset); $this->incrementalOutputOffset = strlen($data); return $latest; } /** * Returns the current error output of the process (STDERR). * * @return string The process error output * * @api */ public function getErrorOutput() { $this->updateErrorOutput(); return $this->stderr; } /** * Returns the errorOutput incrementally. * * In comparison with the getErrorOutput method which always return the * whole error output, this one returns the new error output since the last * call. * * @return string The process error output since the last call */ public function getIncrementalErrorOutput() { $data = $this->getErrorOutput(); $latest = substr($data, $this->incrementalErrorOutputOffset); $this->incrementalErrorOutputOffset = strlen($data); return $latest; } /** * Returns the exit code returned by the process. * * @return integer The exit status code * * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled * * @api */ public function getExitCode() { if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method'); } $this->updateStatus(); return $this->exitcode; } /** * Returns a string representation for the exit code returned by the process. * * This method relies on the Unix exit code status standardization * and might not be relevant for other operating systems. * * @return string A string representation for the exit status code * * @see http://tldp.org/LDP/abs/html/exitcodes.html * @see http://en.wikipedia.org/wiki/Unix_signal */ public function getExitCodeText() { $exitcode = $this->getExitCode(); return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error'; } /** * Checks if the process ended successfully. * * @return Boolean true if the process ended successfully, false otherwise * * @api */ public function isSuccessful() { return 0 == $this->getExitCode(); } /** * Returns true if the child process has been terminated by an uncaught signal. * * It always returns false on Windows. * * @return Boolean * * @throws RuntimeException In case --enable-sigchild is activated * * @api */ public function hasBeenSignaled() { if ($this->isSigchildEnabled()) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); } $this->updateStatus(); return $this->processInformation['signaled']; } /** * Returns the number of the signal that caused the child process to terminate its execution. * * It is only meaningful if hasBeenSignaled() returns true. * * @return integer * * @throws RuntimeException In case --enable-sigchild is activated * * @api */ public function getTermSignal() { if ($this->isSigchildEnabled()) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); } $this->updateStatus(); return $this->processInformation['termsig']; } /** * Returns true if the child process has been stopped by a signal. * * It always returns false on Windows. * * @return Boolean * * @api */ public function hasBeenStopped() { $this->updateStatus(); return $this->processInformation['stopped']; } /** * Returns the number of the signal that caused the child process to stop its execution. * * It is only meaningful if hasBeenStopped() returns true. * * @return integer * * @api */ public function getStopSignal() { $this->updateStatus(); return $this->processInformation['stopsig']; } /** * Checks if the process is currently running. * * @return Boolean true if the process is currently running, false otherwise */ public function isRunning() { if (self::STATUS_STARTED !== $this->status) { return false; } $this->updateStatus(); return $this->processInformation['running']; } /** * Checks if the process has been started with no regard to the current state. * * @return Boolean true if status is ready, false otherwise */ public function isStarted() { return $this->status != self::STATUS_READY; } /** * Checks if the process is terminated. * * @return Boolean true if process is terminated, false otherwise */ public function isTerminated() { $this->updateStatus(); return $this->status == self::STATUS_TERMINATED; } /** * Gets the process status. * * The status is one of: ready, started, terminated. * * @return string The current process status */ public function getStatus() { $this->updateStatus(); return $this->status; } /** * Stops the process. * * @param integer|float $timeout The timeout in seconds * @param integer $signal A posix signal to send in case the process has not stop at timeout, default is SIGKILL * * @return integer The exit-code of the process * * @throws RuntimeException if the process got signaled */ public function stop($timeout = 10, $signal = null) { $timeoutMicro = (int) $timeout*1E6; if ($this->isRunning()) { proc_terminate($this->process); $time = 0; while (1 == $this->isRunning() && $time < $timeoutMicro) { $time += 1000; usleep(1000); } if ($this->isRunning() && !$this->isSigchildEnabled()) { if (null !== $signal || defined('SIGKILL')) { $this->signal($signal ?: SIGKILL); } } foreach ($this->pipes as $pipe) { fclose($pipe); } $this->pipes = array(); $exitcode = proc_close($this->process); $this->exitcode = -1 === $this->processInformation['exitcode'] ? $exitcode : $this->processInformation['exitcode']; if (defined('PHP_WINDOWS_VERSION_BUILD')) { foreach ($this->fileHandles as $fileHandle) { fclose($fileHandle); } $this->fileHandles = array(); } } $this->status = self::STATUS_TERMINATED; return $this->exitcode; } /** * Adds a line to the STDOUT stream. * * @param string $line The line to append */ public function addOutput($line) { $this->stdout .= $line; } /** * Adds a line to the STDERR stream. * * @param string $line The line to append */ public function addErrorOutput($line) { $this->stderr .= $line; } /** * Gets the command line to be executed. * * @return string The command to execute */ public function getCommandLine() { return $this->commandline; } /** * Sets the command line to be executed. * * @param string $commandline The command to execute * * @return self The current Process instance */ public function setCommandLine($commandline) { $this->commandline = $commandline; return $this; } /** * Gets the process timeout. * * @return integer|null The timeout in seconds or null if it's disabled */ public function getTimeout() { return $this->timeout; } /** * Sets the process timeout. * * To disable the timeout, set this value to null. * * @param float|null $timeout The timeout in seconds * * @return self The current Process instance * * @throws InvalidArgumentException if the timeout is negative */ public function setTimeout($timeout) { if (null === $timeout) { $this->timeout = null; return $this; } $timeout = (float) $timeout; if ($timeout < 0) { throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); } $this->timeout = $timeout; return $this; } /** * Enables or disables the TTY mode. * * @param boolean $tty True to enabled and false to disable * * @return self The current Process instance */ public function setTty($tty) { $this->tty = (Boolean) $tty; return $this; } /** * Checks if the TTY mode is enabled. * * @return Boolean true if the TTY mode is enabled, false otherwise */ public function isTty() { return $this->tty; } /** * Gets the working directory. * * @return string The current working directory */ public function getWorkingDirectory() { // This is for BC only if (null === $this->cwd) { // getcwd() will return false if any one of the parent directories does not have // the readable or search mode set, even if the current directory does return getcwd() ?: null; } return $this->cwd; } /** * Sets the current working directory. * * @param string $cwd The new working directory * * @return self The current Process instance */ public function setWorkingDirectory($cwd) { $this->cwd = $cwd; return $this; } /** * Gets the environment variables. * * @return array The current environment variables */ public function getEnv() { return $this->env; } /** * Sets the environment variables. * * An environment variable value should be a string. * If it is an array, the variable is ignored. * * That happens in PHP when 'argv' is registered into * the $_ENV array for instance. * * @param array $env The new environment variables * * @return self The current Process instance */ public function setEnv(array $env) { // Process can not handle env values that are arrays $env = array_filter($env, function ($value) { if (!is_array($value)) { return true; } }); $this->env = array(); foreach ($env as $key => $value) { $this->env[(binary) $key] = (binary) $value; } return $this; } /** * Gets the contents of STDIN. * * @return string The current contents */ public function getStdin() { return $this->stdin; } /** * Sets the contents of STDIN. * * @param string $stdin The new contents * * @return self The current Process instance */ public function setStdin($stdin) { $this->stdin = $stdin; return $this; } /** * Gets the options for proc_open. * * @return array The current options */ public function getOptions() { return $this->options; } /** * Sets the options for proc_open. * * @param array $options The new options * * @return self The current Process instance */ public function setOptions(array $options) { $this->options = $options; return $this; } /** * Gets whether or not Windows compatibility is enabled. * * This is true by default. * * @return Boolean */ public function getEnhanceWindowsCompatibility() { return $this->enhanceWindowsCompatibility; } /** * Sets whether or not Windows compatibility is enabled. * * @param Boolean $enhance * * @return self The current Process instance */ public function setEnhanceWindowsCompatibility($enhance) { $this->enhanceWindowsCompatibility = (Boolean) $enhance; return $this; } /** * Returns whether sigchild compatibility mode is activated or not. * * @return Boolean */ public function getEnhanceSigchildCompatibility() { return $this->enhanceSigchildCompatibility; } /** * Activates sigchild compatibility mode. * * Sigchild compatibility mode is required to get the exit code and * determine the success of a process when PHP has been compiled with * the --enable-sigchild option * * @param Boolean $enhance * * @return self The current Process instance */ public function setEnhanceSigchildCompatibility($enhance) { $this->enhanceSigchildCompatibility = (Boolean) $enhance; return $this; } /** * Performs a check between the timeout definition and the time the process started. * * In case you run a background process (with the start method), you should * trigger this method regularly to ensure the process timeout * * @throws RuntimeException In case the timeout was reached */ public function checkTimeout() { if (0 < $this->timeout && $this->timeout < microtime(true) - $this->starttime) { $this->stop(0); throw new RuntimeException('The process timed-out.'); } } /** * Creates the descriptors needed by the proc_open. * * @return array */ private function getDescriptors() { //Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. //Workaround for this problem is to use temporary files instead of pipes on Windows platform. //@see https://bugs.php.net/bug.php?id=51800 if (defined('PHP_WINDOWS_VERSION_BUILD')) { $this->fileHandles = array( self::STDOUT => tmpfile(), ); if (false === $this->fileHandles[self::STDOUT]) { throw new RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable'); } $this->readBytes = array( self::STDOUT => 0, ); return array(array('pipe', 'r'), $this->fileHandles[self::STDOUT], array('pipe', 'w')); } if ($this->tty) { $descriptors = array( array('file', '/dev/tty', 'r'), array('file', '/dev/tty', 'w'), array('file', '/dev/tty', 'w'), ); } else { $descriptors = array( array('pipe', 'r'), // stdin array('pipe', 'w'), // stdout array('pipe', 'w'), // stderr ); } if ($this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { // last exit code is output on the fourth pipe and caught to work around --enable-sigchild $descriptors = array_merge($descriptors, array(array('pipe', 'w'))); $this->commandline = '('.$this->commandline.') 3>/dev/null; code=$?; echo $code >&3; exit $code'; } return $descriptors; } /** * Builds up the callback used by wait(). * * The callbacks adds all occurred output to the specific buffer and calls * the user callback (if present) with the received output. * * @param callback|null $callback The user defined PHP callback * * @return callback A PHP callable */ protected function buildCallback($callback) { $that = $this; $out = self::OUT; $err = self::ERR; $callback = function ($type, $data) use ($that, $callback, $out, $err) { if ($out == $type) { $that->addOutput($data); } else { $that->addErrorOutput($data); } if (null !== $callback) { call_user_func($callback, $type, $data); } }; return $callback; } /** * Updates the status of the process. */ protected function updateStatus() { if (self::STATUS_STARTED !== $this->status) { return; } $this->processInformation = proc_get_status($this->process); if (!$this->processInformation['running']) { $this->status = self::STATUS_TERMINATED; if (-1 !== $this->processInformation['exitcode']) { $this->exitcode = $this->processInformation['exitcode']; } } } /** * Updates the current error output of the process (STDERR). */ protected function updateErrorOutput() { if (isset($this->pipes[self::STDERR]) && is_resource($this->pipes[self::STDERR])) { $this->addErrorOutput(stream_get_contents($this->pipes[self::STDERR])); } } /** * Updates the current output of the process (STDOUT). */ protected function updateOutput() { if (defined('PHP_WINDOWS_VERSION_BUILD') && isset($this->fileHandles[self::STDOUT]) && is_resource($this->fileHandles[self::STDOUT])) { fseek($this->fileHandles[self::STDOUT], $this->readBytes[self::STDOUT]); $this->addOutput(stream_get_contents($this->fileHandles[self::STDOUT])); } elseif (isset($this->pipes[self::STDOUT]) && is_resource($this->pipes[self::STDOUT])) { $this->addOutput(stream_get_contents($this->pipes[self::STDOUT])); } } /** * Returns whether PHP has been compiled with the '--enable-sigchild' option or not. * * @return Boolean */ protected function isSigchildEnabled() { if (null !== self::$sigchild) { return self::$sigchild; } ob_start(); phpinfo(INFO_GENERAL); return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); } /** * Handles the windows file handles fallbacks. * * @param callable $callback A valid PHP callback * @param Boolean $closeEmptyHandles if true, handles that are empty will be assumed closed */ private function processFileHandles($callback, $closeEmptyHandles = false) { $fh = $this->fileHandles; foreach ($fh as $type => $fileHandle) { fseek($fileHandle, $this->readBytes[$type]); $data = fread($fileHandle, 8192); if (strlen($data) > 0) { $this->readBytes[$type] += strlen($data); call_user_func($callback, $type == 1 ? self::OUT : self::ERR, $data); } if (false === $data || ($closeEmptyHandles && '' === $data && feof($fileHandle))) { fclose($fileHandle); unset($this->fileHandles[$type]); } } } /** * Returns true if a system call has been interrupted. * * @return Boolean */ private function hasSystemCallBeenInterrupted() { $lastError = error_get_last(); // stream_select returns false when the `select` system call is interrupted by an incoming signal return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call'); } } |