This repository has been archived on 2023-08-20. You can view files and clone it, but cannot push or open issues or pull requests.
symfony/src/Symfony/Component/Process/Process.php

1242 lines
36 KiB
PHP
Raw Normal View History

2010-04-17 13:49:58 +01:00
<?php
/*
2010-04-24 00:22:16 +01:00
* This file is part of the Symfony package.
2010-04-17 13:49:58 +01:00
*
* (c) Fabien Potencier <fabien@symfony.com>
2010-04-17 13:49:58 +01:00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process;
2012-08-30 20:25:16 +01:00
use Symfony\Component\Process\Exception\InvalidArgumentException;
use Symfony\Component\Process\Exception\LogicException;
2012-08-30 20:25:16 +01:00
use Symfony\Component\Process\Exception\RuntimeException;
2010-04-17 13:49:58 +01:00
/**
* Process is a thin wrapper around proc_* functions to easily
* start independent PHP processes.
2010-04-17 13:49:58 +01:00
*
* @author Fabien Potencier <fabien@symfony.com>
2011-03-24 08:13:58 +00:00
*
* @api
2010-04-17 13:49:58 +01:00
*/
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;
2013-04-06 19:51:55 +01:00
// Timeout Precision in seconds.
2013-04-07 15:10:23 +01:00
const TIMEOUT_PRECISION = 0.2;
2013-04-06 19:51:55 +01:00
2013-08-13 17:00:31 +01:00
private $callback;
2011-03-24 08:10:42 +00:00
private $commandline;
private $cwd;
private $env;
private $stdin;
2013-04-06 19:51:55 +01:00
private $starttime;
2011-03-24 08:10:42 +00:00
private $timeout;
private $options;
private $exitcode;
private $fallbackExitcode;
private $processInformation;
2011-03-24 08:10:42 +00:00
private $stdout;
private $stderr;
private $enhanceWindowsCompatibility = true;
private $enhanceSigchildCompatibility;
private $process;
private $status = self::STATUS_READY;
private $incrementalOutputOffset = 0;
private $incrementalErrorOutputOffset = 0;
private $tty;
private $useFileHandles = false;
/** @var ProcessPipes */
private $processPipes;
2012-04-22 20:52:08 +01:00
private $latestSignal;
private static $sigchild;
/**
* Exit codes translation table.
*
* User-defined errors must use exit codes in the 64-113 range.
*
* @var array
*/
2012-07-09 13:50:58 +01:00
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.
*
2014-11-30 13:33:44 +00:00
* @param string $commandline The command line to run
* @param string|null $cwd The working directory or null to use the working dir of the current PHP process
* @param array|null $env The environment variables or null to inherit
* @param string|null $stdin The STDIN content
* @param int|float|null $timeout The timeout in seconds or null to disable
* @param array $options An array of options for proc_open
*
2012-08-30 20:25:16 +01:00
* @throws RuntimeException When proc_open is not installed
2011-03-24 08:13:58 +00:00
*
* @api
*/
public function __construct($commandline, $cwd = null, array $env = null, $stdin = null, $timeout = 60, array $options = array())
2010-04-17 13:49:58 +01:00
{
if (!function_exists('proc_open')) {
2012-08-30 20:25:16 +01:00
throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.');
}
2010-04-17 13:49:58 +01:00
$this->commandline = $commandline;
$this->cwd = $cwd;
2013-03-02 17:41:21 +00:00
2013-12-27 15:08:19 +00:00
// 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
2013-03-02 17:41:21 +00:00
// @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') || '\\' === DIRECTORY_SEPARATOR)) {
$this->cwd = getcwd();
}
if (null !== $env) {
$this->setEnv($env);
}
$this->stdin = $stdin;
$this->setTimeout($timeout);
$this->useFileHandles = '\\' === DIRECTORY_SEPARATOR;
$this->enhanceSigchildCompatibility = '\\' !== DIRECTORY_SEPARATOR && $this->isSigchildEnabled();
2012-02-22 13:38:43 +00:00
$this->options = array_replace(array('suppress_errors' => true, 'binary_pipes' => true), $options);
2010-04-17 13:49:58 +01:00
}
public function __destruct()
{
// stop() will check if we have a process running.
$this->stop();
}
2012-09-07 08:03:48 +01:00
public function __clone()
{
$this->resetProcessData();
2012-09-07 08:03:48 +01:00
}
/**
* 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
*
2014-11-30 13:33:44 +00:00
* @return int The exit status code
*
* @throws RuntimeException When process can't be launched
* @throws RuntimeException When process stopped after receiving signal
2011-03-24 08:13:58 +00:00
*
* @api
*/
public function run($callback = null)
2010-04-18 08:27:43 +01:00
{
$this->start($callback);
2013-08-13 17:00:31 +01:00
return $this->wait();
}
/**
2012-03-23 11:59:10 +00:00
* Starts the process and returns after sending the STDIN.
*
2012-03-23 11:59:10 +00:00
* 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().
2012-03-23 11:59:10 +00:00
*
* 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
2012-07-28 23:02:29 +01:00
* with true as a second parameter then the callback will get all data occurred
2012-03-23 11:59:10 +00:00
* 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 launched
2012-08-30 20:25:16 +01:00
* @throws RuntimeException When process is already running
*/
public function start($callback = null)
{
2012-03-23 11:06:08 +00:00
if ($this->isRunning()) {
2012-08-30 20:25:16 +01:00
throw new RuntimeException('Process is already running');
}
2012-03-23 11:59:10 +00:00
$this->resetProcessData();
2013-04-06 19:51:55 +01:00
$this->starttime = microtime(true);
2013-08-13 17:00:31 +01:00
$this->callback = $this->buildCallback($callback);
$descriptors = $this->getDescriptors();
$commandline = $this->commandline;
if ('\\' === DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) {
$commandline = 'cmd /V:ON /E:ON /C "('.$commandline.')';
foreach ($this->processPipes->getFiles() as $offset => $filename) {
$commandline .= ' '.$offset.'>'.ProcessUtils::escapeArgument($filename);
}
$commandline .= '"';
if (!isset($this->options['bypass_shell'])) {
$this->options['bypass_shell'] = true;
}
}
$this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options);
if (!is_resource($this->process)) {
2012-08-30 20:25:16 +01:00
throw new RuntimeException('Unable to launch a new process.');
}
$this->status = self::STATUS_STARTED;
$this->processPipes->unblock();
2014-01-07 09:02:59 +00:00
if ($this->tty) {
return;
}
$this->processPipes->write(false, $this->stdin);
2013-08-13 17:00:31 +01:00
$this->updateStatus(false);
$this->checkTimeout();
}
2012-09-07 08:03:48 +01:00
/**
2012-10-04 15:23:11 +01:00
* Restarts the process.
*
* Be warned that the process is cloned before being started.
2012-09-07 08:03:48 +01:00
*
* @param callable $callback A PHP callback to run whenever there is some
* output available on STDOUT or STDERR
*
2012-10-04 15:23:11 +01:00
* @return Process The new process
2012-09-07 08:03:48 +01:00
*
* @throws RuntimeException When process can't be launched
* @throws RuntimeException When process is already running
2012-10-04 15:23:11 +01:00
*
2012-09-07 08:03:48 +01:00
* @see start()
*/
public function restart($callback = null)
{
if ($this->isRunning()) {
throw new RuntimeException('Process is already running');
2012-09-07 08:03:48 +01:00
}
$process = clone $this;
$process->start($callback);
return $process;
}
/**
2012-03-23 11:59:10 +00:00
* Waits for the process to terminate.
*
2012-03-23 11:59:10 +00:00
* 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
2012-03-23 11:59:10 +00:00
*
2014-11-30 13:33:44 +00:00
* @return int The exitcode of the process
*
* @throws RuntimeException When process timed out
* @throws RuntimeException When process stopped after receiving signal
* @throws LogicException When process is not yet started
*/
public function wait($callback = null)
{
$this->requireProcessIsStarted(__FUNCTION__);
2013-08-13 17:00:31 +01:00
$this->updateStatus(false);
if (null !== $callback) {
$this->callback = $this->buildCallback($callback);
}
do {
2013-04-06 19:51:55 +01:00
$this->checkTimeout();
$running = '\\' === DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->hasOpenHandles();
$close = '\\' !== DIRECTORY_SEPARATOR || !$running;
$this->readPipes(true, $close);
} while ($running);
while ($this->isRunning()) {
usleep(1000);
}
2010-04-17 13:49:58 +01:00
if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) {
throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig']));
}
2011-07-12 07:16:22 +01:00
return $this->exitcode;
}
/**
* Returns the Pid (process identifier), if applicable.
*
2014-11-30 13:33:44 +00:00
* @return int|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(false);
return $this->isRunning() ? $this->processInformation['pid'] : null;
}
/**
2013-12-27 15:08:19 +00:00
* Sends a POSIX signal to the process.
*
2014-11-30 13:33:44 +00:00
* @param int $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)
{
$this->doSignal($signal, true);
return $this;
}
/**
* Returns the current output of the process (STDOUT).
2012-03-23 11:59:10 +00:00
*
* @return string The process output
2011-03-24 08:13:58 +00:00
*
* @throws LogicException In case the process is not started
*
2011-03-24 08:13:58 +00:00
* @api
*/
public function getOutput()
2010-04-17 13:49:58 +01:00
{
$this->requireProcessIsStarted(__FUNCTION__);
$this->readPipes(false, '\\' === DIRECTORY_SEPARATOR ? !$this->processInformation['running'] : true);
return $this->stdout;
2010-04-17 13:49:58 +01:00
}
/**
* 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.
*
* @throws LogicException In case the process is not started
*
* @return string The process output since the last call
*/
public function getIncrementalOutput()
{
$this->requireProcessIsStarted(__FUNCTION__);
$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
2011-03-24 08:13:58 +00:00
*
* @throws LogicException In case the process is not started
*
2011-03-24 08:13:58 +00:00
* @api
*/
public function getErrorOutput()
2010-04-17 13:49:58 +01:00
{
$this->requireProcessIsStarted(__FUNCTION__);
$this->readPipes(false, '\\' === DIRECTORY_SEPARATOR ? !$this->processInformation['running'] : true);
return $this->stderr;
2010-04-17 13:49:58 +01:00
}
/**
* 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.
*
* @throws LogicException In case the process is not started
*
* @return string The process error output since the last call
*/
public function getIncrementalErrorOutput()
{
$this->requireProcessIsStarted(__FUNCTION__);
$data = $this->getErrorOutput();
$latest = substr($data, $this->incrementalErrorOutputOffset);
$this->incrementalErrorOutputOffset = strlen($data);
return $latest;
}
/**
* Returns the exit code returned by the process.
*
2014-11-30 13:33:44 +00:00
* @return null|int The exit status code, null if the Process is not terminated
2011-03-24 08:13:58 +00:00
*
* @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled
*
2011-03-24 08:13:58 +00:00
* @api
*/
public function getExitCode()
2010-04-17 13:49:58 +01:00
{
if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) {
throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.');
}
2013-08-13 17:00:31 +01:00
$this->updateStatus(false);
return $this->exitcode;
}
2010-04-17 13:49:58 +01:00
/**
* 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 null|string A string representation for the exit status code, null if the Process is not terminated.
*
* @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled
*
* @see http://tldp.org/LDP/abs/html/exitcodes.html
* @see http://en.wikipedia.org/wiki/Unix_signal
*/
public function getExitCodeText()
{
if (null === $exitcode = $this->getExitCode()) {
return;
}
return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error';
}
/**
* Checks if the process ended successfully.
*
2014-11-30 13:33:44 +00:00
* @return bool true if the process ended successfully, false otherwise
2011-03-24 08:13:58 +00:00
*
* @api
*/
public function isSuccessful()
{
2013-08-21 15:50:47 +01:00
return 0 === $this->getExitCode();
}
/**
* Returns true if the child process has been terminated by an uncaught signal.
*
* It always returns false on Windows.
*
2014-04-16 11:30:19 +01:00
* @return bool
2011-03-24 08:13:58 +00:00
*
* @throws RuntimeException In case --enable-sigchild is activated
* @throws LogicException In case the process is not terminated
*
2011-03-24 08:13:58 +00:00
* @api
*/
public function hasBeenSignaled()
{
$this->requireProcessIsTerminated(__FUNCTION__);
if ($this->isSigchildEnabled()) {
throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
}
2013-08-13 17:00:31 +01:00
$this->updateStatus(false);
return $this->processInformation['signaled'];
}
2010-04-17 13:49:58 +01:00
/**
* Returns the number of the signal that caused the child process to terminate its execution.
*
* It is only meaningful if hasBeenSignaled() returns true.
*
2014-04-16 11:30:19 +01:00
* @return int
2011-03-24 08:13:58 +00:00
*
* @throws RuntimeException In case --enable-sigchild is activated
* @throws LogicException In case the process is not terminated
*
2011-03-24 08:13:58 +00:00
* @api
*/
public function getTermSignal()
{
$this->requireProcessIsTerminated(__FUNCTION__);
if ($this->isSigchildEnabled()) {
throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.');
}
2013-08-13 17:00:31 +01:00
$this->updateStatus(false);
return $this->processInformation['termsig'];
2010-04-17 13:49:58 +01:00
}
/**
* Returns true if the child process has been stopped by a signal.
*
* It always returns false on Windows.
*
2014-04-16 11:30:19 +01:00
* @return bool
2011-03-24 08:13:58 +00:00
*
* @throws LogicException In case the process is not terminated
*
2011-03-24 08:13:58 +00:00
* @api
*/
public function hasBeenStopped()
{
$this->requireProcessIsTerminated(__FUNCTION__);
2013-08-13 17:00:31 +01:00
$this->updateStatus(false);
return $this->processInformation['stopped'];
}
2010-04-17 13:49:58 +01:00
/**
2012-10-06 13:46:45 +01:00
* Returns the number of the signal that caused the child process to stop its execution.
*
* It is only meaningful if hasBeenStopped() returns true.
*
2014-04-16 11:30:19 +01:00
* @return int
2011-03-24 08:13:58 +00:00
*
* @throws LogicException In case the process is not terminated
*
2011-03-24 08:13:58 +00:00
* @api
*/
public function getStopSignal()
{
$this->requireProcessIsTerminated(__FUNCTION__);
2013-08-13 17:00:31 +01:00
$this->updateStatus(false);
return $this->processInformation['stopsig'];
}
/**
2012-03-23 11:59:10 +00:00
* Checks if the process is currently running.
*
2014-11-30 13:33:44 +00:00
* @return bool true if the process is currently running, false otherwise
*/
2012-03-23 11:59:10 +00:00
public function isRunning()
2012-03-23 11:06:08 +00:00
{
2012-03-23 11:59:10 +00:00
if (self::STATUS_STARTED !== $this->status) {
return false;
}
2013-08-13 17:00:31 +01:00
$this->updateStatus(false);
2012-03-23 11:59:10 +00:00
return $this->processInformation['running'];
}
/**
* Checks if the process has been started with no regard to the current state.
*
2014-11-30 13:33:44 +00:00
* @return bool true if status is ready, false otherwise
*/
public function isStarted()
{
return $this->status != self::STATUS_READY;
}
/**
* Checks if the process is terminated.
*
2014-11-30 13:33:44 +00:00
* @return bool true if process is terminated, false otherwise
*/
public function isTerminated()
{
2013-08-13 17:00:31 +01:00
$this->updateStatus(false);
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()
{
2013-08-13 17:00:31 +01:00
$this->updateStatus(false);
return $this->status;
}
/**
2012-03-23 11:59:10 +00:00
* Stops the process.
*
2014-11-30 13:33:44 +00:00
* @param int|float $timeout The timeout in seconds
* @param int $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL
2012-03-23 11:59:10 +00:00
*
2014-11-30 13:33:44 +00:00
* @return int The exit-code of the process
2012-03-23 11:59:10 +00:00
*
2012-08-30 20:25:16 +01:00
* @throws RuntimeException if the process got signaled
*/
public function stop($timeout = 10, $signal = null)
2012-03-23 11:06:08 +00:00
{
$timeoutMicro = microtime(true) + $timeout;
2012-03-23 11:06:08 +00:00
if ($this->isRunning()) {
if ('\\' === DIRECTORY_SEPARATOR && !$this->isSigchildEnabled()) {
exec(sprintf("taskkill /F /T /PID %d 2>&1", $this->getPid()), $output, $exitCode);
if ($exitCode > 0) {
throw new RuntimeException('Unable to kill the process');
}
}
// given `SIGTERM` may not be defined and that `proc_terminate` uses the constant value and not the constant itself, we use the same here
$this->doSignal(15, false);
do {
usleep(1000);
} while ($this->isRunning() && microtime(true) < $timeoutMicro);
if ($this->isRunning() && !$this->isSigchildEnabled()) {
if (null !== $signal || defined('SIGKILL')) {
// avoid exception here :
// process is supposed to be running, but it might have stop
// just after this line.
// in any case, let's silently discard the error, we can not do anything
$this->doSignal($signal ?: SIGKILL, false);
}
}
}
$this->updateStatus(false);
if ($this->processInformation['running']) {
$this->close();
}
return $this->exitcode;
}
2010-04-17 13:49:58 +01:00
2012-10-06 05:39:50 +01:00
/**
2012-10-06 13:46:45 +01:00
* Adds a line to the STDOUT stream.
2012-10-06 05:39:50 +01:00
*
* @param string $line The line to append
*/
public function addOutput($line)
2010-04-17 13:49:58 +01:00
{
$this->stdout .= $line;
2010-04-17 13:49:58 +01:00
}
2012-10-06 05:39:50 +01:00
/**
2012-10-06 13:46:45 +01:00
* Adds a line to the STDERR stream.
2012-10-06 05:39:50 +01:00
*
* @param string $line The line to append
*/
public function addErrorOutput($line)
{
$this->stderr .= $line;
}
2011-03-24 08:10:42 +00:00
2012-10-06 05:39:50 +01:00
/**
2012-10-06 13:46:45 +01:00
* Gets the command line to be executed.
2012-10-06 05:39:50 +01:00
*
* @return string The command to execute
*/
2011-03-24 08:15:33 +00:00
public function getCommandLine()
{
return $this->commandline;
}
2012-10-03 15:02:00 +01:00
/**
2012-10-06 13:46:45 +01:00
* Sets the command line to be executed.
2012-10-06 05:39:50 +01:00
*
* @param string $commandline The command to execute
*
2012-10-03 15:02:00 +01:00
* @return self The current Process instance
*/
2011-03-24 08:10:42 +00:00
public function setCommandLine($commandline)
{
$this->commandline = $commandline;
2012-09-16 04:07:09 +01:00
return $this;
2011-03-24 08:10:42 +00:00
}
2012-10-06 05:39:50 +01:00
/**
2012-10-06 13:46:45 +01:00
* Gets the process timeout.
2012-10-06 05:39:50 +01:00
*
2013-11-08 00:33:52 +00:00
* @return float|null The timeout in seconds or null if it's disabled
2012-10-06 05:39:50 +01:00
*/
public function getTimeout()
{
return $this->timeout;
}
/**
* Sets the process timeout.
*
* To disable the timeout, set this value to null.
*
2014-11-30 13:33:44 +00:00
* @param int|float|null $timeout The timeout in seconds
2012-10-03 15:02:00 +01:00
*
* @return self The current Process instance
*
* @throws InvalidArgumentException if the timeout is negative
*/
public function setTimeout($timeout)
{
2013-04-06 19:51:55 +01:00
$timeout = (float) $timeout;
2013-11-08 00:33:52 +00:00
if (0.0 === $timeout) {
$timeout = null;
} elseif ($timeout < 0) {
throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
}
$this->timeout = $timeout;
2012-09-16 04:07:09 +01:00
return $this;
}
/**
2013-04-20 13:33:48 +01:00
* Enables or disables the TTY mode.
*
2014-11-30 13:33:44 +00:00
* @param bool $tty True to enabled and false to disable
*
* @return self The current Process instance
*
* @throws RuntimeException In case the TTY mode is not supported
*/
2013-04-20 13:33:48 +01:00
public function setTty($tty)
{
if ('\\' === DIRECTORY_SEPARATOR && $tty) {
throw new RuntimeException('TTY mode is not supported on Windows platform.');
}
$this->tty = (bool) $tty;
return $this;
}
/**
2014-01-07 09:02:59 +00:00
* Checks if the TTY mode is enabled.
*
2014-11-30 13:33:44 +00:00
* @return bool true if the TTY mode is enabled, false otherwise
*/
2013-04-20 13:33:48 +01:00
public function isTty()
{
return $this->tty;
}
2012-10-06 05:39:50 +01:00
/**
2012-10-06 13:46:45 +01:00
* Gets the working directory.
2012-10-06 05:39:50 +01:00
*
2013-11-08 00:33:52 +00:00
* @return string|null The current working directory or null on failure
2012-10-06 05:39:50 +01:00
*/
public function getWorkingDirectory()
{
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;
}
2012-10-03 15:02:00 +01:00
/**
2012-10-06 13:46:45 +01:00
* Sets the current working directory.
2012-10-06 05:39:50 +01:00
*
* @param string $cwd The new working directory
*
2012-10-03 15:02:00 +01:00
* @return self The current Process instance
*/
public function setWorkingDirectory($cwd)
{
$this->cwd = $cwd;
2012-09-16 04:07:09 +01:00
return $this;
}
2012-10-06 05:39:50 +01:00
/**
2012-10-06 13:46:45 +01:00
* Gets the environment variables.
2012-10-06 05:39:50 +01:00
*
* @return array The current environment variables
*/
public function getEnv()
{
return $this->env;
}
2012-10-03 15:02:00 +01:00
/**
2012-10-06 13:46:45 +01:00
* Sets the environment variables.
2012-10-06 05:39:50 +01:00
*
* 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.
*
2012-10-06 05:39:50 +01:00
* @param array $env The new environment variables
*
2012-10-03 15:02:00 +01:00
* @return self The current Process instance
*/
public function setEnv(array $env)
{
// Process can not handle env values that are arrays
2014-02-21 07:41:38 +00:00
$env = array_filter($env, function ($value) {
return !is_array($value);
});
$this->env = array();
foreach ($env as $key => $value) {
$this->env[(binary) $key] = (binary) $value;
}
2012-09-16 04:07:09 +01:00
return $this;
}
2012-10-06 05:39:50 +01:00
/**
2012-10-06 13:46:45 +01:00
* Gets the contents of STDIN.
2012-10-06 05:39:50 +01:00
*
2013-11-08 00:33:52 +00:00
* @return string|null The current contents
2012-10-06 05:39:50 +01:00
*/
public function getStdin()
{
return $this->stdin;
}
2012-10-03 15:02:00 +01:00
/**
2012-10-06 13:46:45 +01:00
* Sets the contents of STDIN.
2012-10-06 05:39:50 +01:00
*
2013-11-08 00:33:52 +00:00
* @param string|null $stdin The new contents
*
2012-10-03 15:02:00 +01:00
* @return self The current Process instance
*
* @throws LogicException In case the process is running
* @throws InvalidArgumentException In case the argument is invalid
2012-10-03 15:02:00 +01:00
*/
public function setStdin($stdin)
{
if ($this->isRunning()) {
throw new LogicException('STDIN can not be set while the process is running.');
}
$this->stdin = ProcessUtils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $stdin);
2012-09-16 04:07:09 +01:00
return $this;
}
2012-10-06 05:39:50 +01:00
/**
2012-10-06 13:46:45 +01:00
* Gets the options for proc_open.
2012-10-06 05:39:50 +01:00
*
* @return array The current options
*/
public function getOptions()
{
return $this->options;
}
2012-10-03 15:02:00 +01:00
/**
2012-10-06 13:46:45 +01:00
* Sets the options for proc_open.
2012-10-06 05:39:50 +01:00
*
* @param array $options The new options
*
2012-10-03 15:02:00 +01:00
* @return self The current Process instance
*/
public function setOptions(array $options)
{
$this->options = $options;
2012-09-16 04:07:09 +01:00
return $this;
}
2012-10-06 05:44:21 +01:00
/**
2013-04-20 13:33:48 +01:00
* Gets whether or not Windows compatibility is enabled.
2012-10-06 05:44:21 +01:00
*
* This is true by default.
*
2014-04-16 11:30:19 +01:00
* @return bool
2012-10-06 05:44:21 +01:00
*/
public function getEnhanceWindowsCompatibility()
{
return $this->enhanceWindowsCompatibility;
}
2012-10-03 15:02:00 +01:00
/**
2013-04-20 13:33:48 +01:00
* Sets whether or not Windows compatibility is enabled.
2012-10-06 05:44:21 +01:00
*
2014-11-30 13:33:44 +00:00
* @param bool $enhance
*
2012-10-03 15:02:00 +01:00
* @return self The current Process instance
*/
public function setEnhanceWindowsCompatibility($enhance)
{
$this->enhanceWindowsCompatibility = (bool) $enhance;
2012-09-16 04:07:09 +01:00
2012-10-03 15:02:00 +01:00
return $this;
}
/**
2013-04-20 13:33:48 +01:00
* Returns whether sigchild compatibility mode is activated or not.
*
2014-04-16 11:30:19 +01:00
* @return bool
*/
public function getEnhanceSigchildCompatibility()
{
return $this->enhanceSigchildCompatibility;
}
/**
2013-04-20 13:33:48 +01:00
* 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
*
2014-11-30 13:33:44 +00:00
* @param bool $enhance
2012-10-03 15:02:00 +01:00
*
* @return self The current Process instance
*/
public function setEnhanceSigchildCompatibility($enhance)
{
$this->enhanceSigchildCompatibility = (bool) $enhance;
2012-10-03 15:02:00 +01:00
return $this;
}
2013-04-06 19:51:55 +01:00
/**
2013-04-20 13:33:48 +01:00
* Performs a check between the timeout definition and the time the process started.
2013-04-06 19:51:55 +01:00
*
* 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 ($this->status !== self::STATUS_STARTED) {
return;
}
2013-11-08 00:33:52 +00:00
if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
2013-04-06 19:51:55 +01:00
$this->stop(0);
2013-04-07 15:10:23 +01:00
throw new RuntimeException('The process timed-out.');
2013-04-06 19:51:55 +01:00
}
}
/**
2013-04-20 13:33:48 +01:00
* Creates the descriptors needed by the proc_open.
*
* @return array
*/
private function getDescriptors()
{
2014-01-07 09:02:59 +00:00
$this->processPipes = new ProcessPipes($this->useFileHandles, $this->tty);
$descriptors = $this->processPipes->getDescriptors();
if (!$this->useFileHandles && $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().
2012-03-23 11:59:10 +00:00
*
2012-07-28 23:02:29 +01:00
* The callbacks adds all occurred output to the specific buffer and calls
* the user callback (if present) with the received output.
2012-03-23 11:59:10 +00:00
*
* @param callback|null $callback The user defined PHP callback
2012-03-23 11:59:10 +00:00
*
* @return callback A PHP callable
*/
2012-03-23 11:59:10 +00:00
protected function buildCallback($callback)
{
$that = $this;
$out = self::OUT;
$err = self::ERR;
2012-03-23 11:59:10 +00:00
$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;
}
/**
2013-08-13 17:00:31 +01:00
* Updates the status of the process, reads pipes.
*
2014-11-30 13:33:44 +00:00
* @param bool $blocking Whether to use a blocking read call.
*/
2013-08-13 17:00:31 +01:00
protected function updateStatus($blocking)
{
2012-03-23 11:59:10 +00:00
if (self::STATUS_STARTED !== $this->status) {
return;
}
$this->processInformation = proc_get_status($this->process);
$this->captureExitCode();
$this->readPipes($blocking, '\\' === DIRECTORY_SEPARATOR ? !$this->processInformation['running'] : true);
2012-03-23 11:59:10 +00:00
if (!$this->processInformation['running']) {
$this->close();
}
}
/**
2013-04-20 13:33:48 +01:00
* Returns whether PHP has been compiled with the '--enable-sigchild' option or not.
*
2014-04-16 11:30:19 +01:00
* @return bool
*/
protected function isSigchildEnabled()
{
if (null !== self::$sigchild) {
return self::$sigchild;
}
if (!function_exists('phpinfo')) {
return self::$sigchild = false;
}
ob_start();
phpinfo(INFO_GENERAL);
return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild');
}
2013-08-13 17:00:31 +01:00
/**
* Reads pipes, executes callback.
*
2014-11-30 13:33:44 +00:00
* @param bool $blocking Whether to use blocking calls or not.
* @param bool $close Whether to close file handles or not.
2013-08-13 17:00:31 +01:00
*/
private function readPipes($blocking, $close)
2013-08-13 17:00:31 +01:00
{
if ($close) {
$result = $this->processPipes->readAndCloseHandles($blocking);
} else {
$result = $this->processPipes->read($blocking);
}
2014-11-21 08:49:35 +00:00
$callback = $this->callback;
foreach ($result as $type => $data) {
if (3 == $type) {
$this->fallbackExitcode = (int) $data;
} else {
2014-11-21 08:49:35 +00:00
$callback($type === self::STDOUT ? self::OUT : self::ERR, $data);
}
2013-08-13 17:00:31 +01:00
}
}
/**
2013-12-28 21:34:46 +00:00
* Captures the exitcode if mentioned in the process information.
*/
private function captureExitCode()
{
if (isset($this->processInformation['exitcode']) && -1 != $this->processInformation['exitcode']) {
$this->exitcode = $this->processInformation['exitcode'];
}
}
/**
* Closes process resource, closes file handles, sets the exitcode.
*
2014-11-30 13:33:44 +00:00
* @return int The exitcode
*/
private function close()
{
$this->processPipes->close();
if (is_resource($this->process)) {
$exitcode = proc_close($this->process);
} else {
$exitcode = -1;
}
$this->exitcode = -1 !== $exitcode ? $exitcode : (null !== $this->exitcode ? $this->exitcode : -1);
$this->status = self::STATUS_TERMINATED;
if (-1 === $this->exitcode && null !== $this->fallbackExitcode) {
$this->exitcode = $this->fallbackExitcode;
} elseif (-1 === $this->exitcode && $this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) {
2013-12-27 15:08:19 +00:00
// if process has been signaled, no exitcode but a valid termsig, apply Unix convention
$this->exitcode = 128 + $this->processInformation['termsig'];
}
return $this->exitcode;
}
/**
* Resets data related to the latest run of the process.
*/
private function resetProcessData()
{
$this->starttime = null;
$this->callback = null;
$this->exitcode = null;
$this->fallbackExitcode = null;
$this->processInformation = null;
$this->stdout = null;
$this->stderr = null;
$this->process = null;
$this->latestSignal = null;
$this->status = self::STATUS_READY;
$this->incrementalOutputOffset = 0;
$this->incrementalErrorOutputOffset = 0;
}
/**
* Sends a POSIX signal to the process.
*
2014-11-30 13:33:44 +00:00
* @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php)
* @param bool $throwException Whether to throw exception in case signal failed
*
2014-11-30 13:33:44 +00:00
* @return bool True if the signal was sent successfully, false otherwise
*
* @throws LogicException In case the process is not running
* @throws RuntimeException In case --enable-sigchild is activated
* @throws RuntimeException In case of failure
*/
private function doSignal($signal, $throwException)
{
if (!$this->isRunning()) {
if ($throwException) {
throw new LogicException('Can not send signal on a non running process.');
}
return false;
}
if ($this->isSigchildEnabled()) {
if ($throwException) {
throw new RuntimeException('This PHP has been compiled with --enable-sigchild. The process can not be signaled.');
}
return false;
}
if (true !== @proc_terminate($this->process, $signal)) {
if ($throwException) {
throw new RuntimeException(sprintf('Error while sending signal `%s`.', $signal));
}
return false;
}
$this->latestSignal = $signal;
return true;
}
/**
* Ensures the process is running or terminated, throws a LogicException if the process has a not started.
*
* @param string $functionName The function name that was called.
*
* @throws LogicException If the process has not run.
*/
private function requireProcessIsStarted($functionName)
{
if (!$this->isStarted()) {
throw new LogicException(sprintf('Process must be started before calling %s.', $functionName));
}
}
/**
* Ensures the process is terminated, throws a LogicException if the process has a status different than `terminated`.
*
* @param string $functionName The function name that was called.
*
* @throws LogicException If the process is not yet terminated.
*/
private function requireProcessIsTerminated($functionName)
{
if (!$this->isTerminated()) {
throw new LogicException(sprintf('Process must be terminated before calling %s.', $functionName));
}
}
2010-04-17 13:49:58 +01:00
}