diff --git a/src/Symfony/Components/Process/PhpProcess.php b/src/Symfony/Components/Process/PhpProcess.php new file mode 100644 index 0000000000..89a2b9d88c --- /dev/null +++ b/src/Symfony/Components/Process/PhpProcess.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * PhpProcess runs a PHP script in a forked process. + * + * @package Symfony + * @subpackage Components_Process + * @author Fabien Potencier + */ +class PhpProcess extends Process +{ + /** + * Constructor. + * + * @param string $script The PHP script to run (as a string) + * @param string $cwd The working directory + * @param array $env The environment variables + * @param integer $timeout The timeout in seconds + * @param array $options An array of options for proc_open + */ + public function __construct($script, $cwd = null, array $env = array(), $timeout = 60, array $options = array()) + { + parent::__construct(null, $cwd, $env, $script, $timeout, $options); + } + + /** + * Sets the path to the PHP binary to use. + */ + public function setPhpBinary($php) + { + $this->commandline = $php; + } + + /** + * Forks and run the process. + * + * @param Closure|string|array $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @return integer The exit status code + */ + public function run($callback) + { + if (null === $this->commandline) + { + $this->commandline = $this->getPhpBinary(); + } + + parent::run($callback); + } + + /** + * Returns the PHP binary path. + * + * return string The PHP binary path + */ + static public function getPhpBinary() + { + if (getenv('PHP_PATH')) + { + if (!is_executable($php = getenv('PHP_PATH'))) + { + throw new \RuntimeException('The defined PHP_PATH environment variable is not a valid PHP executable.'); + } + + return $php; + } + + $suffixes = DIRECTORY_SEPARATOR == '\\' ? (getenv('PATHEXT') ? explode(PATH_SEPARATOR, getenv('PATHEXT')) : array('.exe', '.bat', '.cmd', '.com')) : array(''); + foreach ($suffixes as $suffix) + { + if (is_executable($php = PHP_BINDIR.DIRECTORY_SEPARATOR.'php'.$suffix)) + { + return $php; + } + } + + throw new \RuntimeException("Unable to find the PHP executable."); + } +} diff --git a/src/Symfony/Components/Process/Process.php b/src/Symfony/Components/Process/Process.php new file mode 100644 index 0000000000..b94749aed9 --- /dev/null +++ b/src/Symfony/Components/Process/Process.php @@ -0,0 +1,209 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Process is a thin wrapper around proc_* functions to ease + * the forking processes from PHP. + * + * @package Symfony + * @subpackage Components_Process + * @author Fabien Potencier + */ +class Process +{ + protected $commandline; + protected $cwd; + protected $env; + protected $stdin; + protected $timeout; + protected $options; + protected $exitcode; + protected $status; + + /** + * Constructor. + * + * @param string $commandline The command line to run + * @param string $cwd The working directory + * @param array $env The environment variables + * @param string $stdin The STDIN content + * @param integer $timeout The timeout in seconds + * @param array $options An array of options for proc_open + */ + public function __construct($commandline, $cwd, array $env = array(), $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 = null === $cwd ? getcwd() : $cwd; + $this->env = array(); + foreach ($env as $key => $value) + { + $this->env[(binary) $key] = (binary) $value; + } + $this->stdin = $stdin; + $this->timeout = $timeout; + $this->options = array_merge($options, array('suppress_errors' => true, 'binary_pipes' => true)); + } + + /** + * Forks and run the process. + * + * @param Closure|string|array $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @return integer The exit status code + */ + public function run($callback) + { + $descriptors = array(array('pipe', 'r'), array('pipe', 'w'), array('pipe', 'w')); + + $proccess = proc_open($this->commandline, $descriptors, $pipes, $this->cwd, $this->env, $this->options); + + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + + if (!is_resource($proccess)) + { + throw new \RuntimeException('Unable to launch a new process.'); + } + + if (!is_null($this->stdin)) + { + fwrite($pipes[0], (binary) $this->stdin); + } + fclose($pipes[0]); + + while (true) + { + $r = $pipes; + $w = null; + $e = null; + + $n = @stream_select($r, $w, $e, $this->timeout); + + if ($n === false) + { + break; + } + elseif ($n === 0) + { + proc_terminate($proccess); + + throw new \RuntimeException('The process timed out.'); + } + elseif ($n > 0) + { + $called = false; + + while (true) + { + $c = false; + if ($line = (binary) fgets($pipes[1], 1024)) + { + $called = $c = true; + call_user_func($callback, 'out', $line); + } + + if ($line = fgets($pipes[2], 1024)) + { + $called = $c = true; + call_user_func($callback, 'err', $line); + } + + if (!$c) + { + break; + } + } + + if (!$called) + { + break; + } + } + } + + $this->status = proc_get_status($proccess); + + proc_close($proccess); + + if ($this->status['signaled']) + { + throw new \RuntimeException(sprintf('The process stopped because of a "%s" signal.', $this->status['stopsig'])); + } + + return $this->exitcode = $this->status['exitcode']; + } + + /** + * Returns the exit code returned by the process. + * + * @return integer The exit status code + */ + public function getExitCode() + { + return $this->exitcode; + } + + /** + * Returns true if the child process has been terminated by an uncaught signal. + * + * It always returns false on Windows. + * + * @return Boolean + */ + public function hasBeenSignaled() + { + return $this->status['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 + */ + public function getTermSignal() + { + return $this->status['termsig']; + } + + /** + * Returns true if the child process has been stopped by a signal. + * + * It always returns false on Windows. + * + * @return Boolean + */ + public function hasBeenStopped() + { + return $this->status['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 + */ + public function getStopSignal() + { + return $this->status['stopsig']; + } +}