|
|
<?php |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
namespace think; |
|
|
|
|
|
use think\process\exception\Failed as ProcessFailedException; |
|
|
use think\process\exception\Timeout as ProcessTimeoutException; |
|
|
use think\process\pipes\Pipes; |
|
|
use think\process\pipes\Unix as UnixPipes; |
|
|
use think\process\pipes\Windows as WindowsPipes; |
|
|
use think\process\Utils; |
|
|
|
|
|
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; |
|
|
|
|
|
const TIMEOUT_PRECISION = 0.2; |
|
|
|
|
|
private $callback; |
|
|
private $commandline; |
|
|
private $cwd; |
|
|
private $env; |
|
|
private $input; |
|
|
private $starttime; |
|
|
private $lastOutputTime; |
|
|
private $timeout; |
|
|
private $idleTimeout; |
|
|
private $options; |
|
|
private $exitcode; |
|
|
private $fallbackExitcode; |
|
|
private $processInformation; |
|
|
private $outputDisabled = false; |
|
|
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 $pty; |
|
|
|
|
|
private $useFileHandles = false; |
|
|
|
|
|
|
|
|
private $processPipes; |
|
|
|
|
|
private $latestSignal; |
|
|
|
|
|
private static $sigchild; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static $exitCodes = [ |
|
|
0 => 'OK', |
|
|
1 => 'General error', |
|
|
2 => 'Misuse of shell builtins', |
|
|
126 => 'Invoked command cannot execute', |
|
|
127 => 'Command not found', |
|
|
128 => 'Invalid exit argument', |
|
|
|
|
|
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)', |
|
|
|
|
|
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', |
|
|
|
|
|
157 => 'Pollable event', |
|
|
|
|
|
159 => 'Bad syscall', |
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = []) |
|
|
{ |
|
|
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; |
|
|
|
|
|
if (null === $this->cwd && (defined('ZEND_THREAD_SAFE') || '\\' === DS)) { |
|
|
$this->cwd = getcwd(); |
|
|
} |
|
|
if (null !== $env) { |
|
|
$this->setEnv($env); |
|
|
} |
|
|
|
|
|
$this->input = $input; |
|
|
$this->setTimeout($timeout); |
|
|
$this->useFileHandles = '\\' === DS; |
|
|
$this->pty = false; |
|
|
$this->enhanceWindowsCompatibility = true; |
|
|
$this->enhanceSigchildCompatibility = '\\' !== DS && $this->isSigchildEnabled(); |
|
|
$this->options = array_replace([ |
|
|
'suppress_errors' => true, |
|
|
'binary_pipes' => true, |
|
|
], $options); |
|
|
} |
|
|
|
|
|
public function __destruct() |
|
|
{ |
|
|
$this->stop(); |
|
|
} |
|
|
|
|
|
public function __clone() |
|
|
{ |
|
|
$this->resetProcessData(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function run($callback = null) |
|
|
{ |
|
|
$this->start($callback); |
|
|
|
|
|
return $this->wait(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function mustRun($callback = null) |
|
|
{ |
|
|
if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) { |
|
|
throw new \RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); |
|
|
} |
|
|
|
|
|
if (0 !== $this->run($callback)) { |
|
|
throw new ProcessFailedException($this); |
|
|
} |
|
|
|
|
|
return $this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function start($callback = null) |
|
|
{ |
|
|
if ($this->isRunning()) { |
|
|
throw new \RuntimeException('Process is already running'); |
|
|
} |
|
|
if ($this->outputDisabled && null !== $callback) { |
|
|
throw new \LogicException('Output has been disabled, enable it to allow the use of a callback.'); |
|
|
} |
|
|
|
|
|
$this->resetProcessData(); |
|
|
$this->starttime = $this->lastOutputTime = microtime(true); |
|
|
$this->callback = $this->buildCallback($callback); |
|
|
$descriptors = $this->getDescriptors(); |
|
|
|
|
|
$commandline = $this->commandline; |
|
|
|
|
|
if ('\\' === DS && $this->enhanceWindowsCompatibility) { |
|
|
$commandline = 'cmd /V:ON /E:ON /C "(' . $commandline . ')'; |
|
|
foreach ($this->processPipes->getFiles() as $offset => $filename) { |
|
|
$commandline .= ' ' . $offset . '>' . Utils::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)) { |
|
|
throw new \RuntimeException('Unable to launch a new process.'); |
|
|
} |
|
|
$this->status = self::STATUS_STARTED; |
|
|
|
|
|
if ($this->tty) { |
|
|
return; |
|
|
} |
|
|
|
|
|
$this->updateStatus(false); |
|
|
$this->checkTimeout(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function restart($callback = null) |
|
|
{ |
|
|
if ($this->isRunning()) { |
|
|
throw new \RuntimeException('Process is already running'); |
|
|
} |
|
|
|
|
|
$process = clone $this; |
|
|
$process->start($callback); |
|
|
|
|
|
return $process; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function wait($callback = null) |
|
|
{ |
|
|
$this->requireProcessIsStarted(__FUNCTION__); |
|
|
|
|
|
$this->updateStatus(false); |
|
|
if (null !== $callback) { |
|
|
$this->callback = $this->buildCallback($callback); |
|
|
} |
|
|
|
|
|
do { |
|
|
$this->checkTimeout(); |
|
|
$running = '\\' === DS ? $this->isRunning() : $this->processPipes->areOpen(); |
|
|
$close = '\\' !== DS || !$running; |
|
|
$this->readPipes(true, $close); |
|
|
} while ($running); |
|
|
|
|
|
while ($this->isRunning()) { |
|
|
usleep(1000); |
|
|
} |
|
|
|
|
|
if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) { |
|
|
throw new \RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig'])); |
|
|
} |
|
|
|
|
|
return $this->exitcode; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function signal($signal) |
|
|
{ |
|
|
$this->doSignal($signal, true); |
|
|
|
|
|
return $this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function disableOutput() |
|
|
{ |
|
|
if ($this->isRunning()) { |
|
|
throw new \RuntimeException('Disabling output while the process is running is not possible.'); |
|
|
} |
|
|
if (null !== $this->idleTimeout) { |
|
|
throw new \LogicException('Output can not be disabled while an idle timeout is set.'); |
|
|
} |
|
|
|
|
|
$this->outputDisabled = true; |
|
|
|
|
|
return $this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function enableOutput() |
|
|
{ |
|
|
if ($this->isRunning()) { |
|
|
throw new \RuntimeException('Enabling output while the process is running is not possible.'); |
|
|
} |
|
|
|
|
|
$this->outputDisabled = false; |
|
|
|
|
|
return $this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function isOutputDisabled() |
|
|
{ |
|
|
return $this->outputDisabled; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function getOutput() |
|
|
{ |
|
|
if ($this->outputDisabled) { |
|
|
throw new \LogicException('Output has been disabled.'); |
|
|
} |
|
|
|
|
|
$this->requireProcessIsStarted(__FUNCTION__); |
|
|
|
|
|
$this->readPipes(false, '\\' === DS ? !$this->processInformation['running'] : true); |
|
|
|
|
|
return $this->stdout; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function getIncrementalOutput() |
|
|
{ |
|
|
$this->requireProcessIsStarted(__FUNCTION__); |
|
|
|
|
|
$data = $this->getOutput(); |
|
|
|
|
|
$latest = substr($data, $this->incrementalOutputOffset); |
|
|
|
|
|
if (false === $latest) { |
|
|
return ''; |
|
|
} |
|
|
|
|
|
$this->incrementalOutputOffset = strlen($data); |
|
|
|
|
|
return $latest; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function clearOutput() |
|
|
{ |
|
|
$this->stdout = ''; |
|
|
$this->incrementalOutputOffset = 0; |
|
|
|
|
|
return $this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function getErrorOutput() |
|
|
{ |
|
|
if ($this->outputDisabled) { |
|
|
throw new \LogicException('Output has been disabled.'); |
|
|
} |
|
|
|
|
|
$this->requireProcessIsStarted(__FUNCTION__); |
|
|
|
|
|
$this->readPipes(false, '\\' === DS ? !$this->processInformation['running'] : true); |
|
|
|
|
|
return $this->stderr; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function getIncrementalErrorOutput() |
|
|
{ |
|
|
$this->requireProcessIsStarted(__FUNCTION__); |
|
|
|
|
|
$data = $this->getErrorOutput(); |
|
|
|
|
|
$latest = substr($data, $this->incrementalErrorOutputOffset); |
|
|
|
|
|
if (false === $latest) { |
|
|
return ''; |
|
|
} |
|
|
|
|
|
$this->incrementalErrorOutputOffset = strlen($data); |
|
|
|
|
|
return $latest; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function clearErrorOutput() |
|
|
{ |
|
|
$this->stderr = ''; |
|
|
$this->incrementalErrorOutputOffset = 0; |
|
|
|
|
|
return $this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(false); |
|
|
|
|
|
return $this->exitcode; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function getExitCodeText() |
|
|
{ |
|
|
if (null === $exitcode = $this->getExitCode()) { |
|
|
return; |
|
|
} |
|
|
|
|
|
return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error'; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function isSuccessful() |
|
|
{ |
|
|
return 0 === $this->getExitCode(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.'); |
|
|
} |
|
|
|
|
|
$this->updateStatus(false); |
|
|
|
|
|
return $this->processInformation['signaled']; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.'); |
|
|
} |
|
|
|
|
|
$this->updateStatus(false); |
|
|
|
|
|
return $this->processInformation['termsig']; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function hasBeenStopped() |
|
|
{ |
|
|
$this->requireProcessIsTerminated(__FUNCTION__); |
|
|
|
|
|
$this->updateStatus(false); |
|
|
|
|
|
return $this->processInformation['stopped']; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function getStopSignal() |
|
|
{ |
|
|
$this->requireProcessIsTerminated(__FUNCTION__); |
|
|
|
|
|
$this->updateStatus(false); |
|
|
|
|
|
return $this->processInformation['stopsig']; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function isRunning() |
|
|
{ |
|
|
if (self::STATUS_STARTED !== $this->status) { |
|
|
return false; |
|
|
} |
|
|
|
|
|
$this->updateStatus(false); |
|
|
|
|
|
return $this->processInformation['running']; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function isStarted() |
|
|
{ |
|
|
return self::STATUS_READY != $this->status; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function isTerminated() |
|
|
{ |
|
|
$this->updateStatus(false); |
|
|
|
|
|
return self::STATUS_TERMINATED == $this->status; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function getStatus() |
|
|
{ |
|
|
$this->updateStatus(false); |
|
|
|
|
|
return $this->status; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function stop() |
|
|
{ |
|
|
if ($this->isRunning()) { |
|
|
if ('\\' === DS && !$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'); |
|
|
} |
|
|
} else { |
|
|
$pids = preg_split('/\s+/', `ps -o pid --no-heading --ppid {$this->getPid()}`); |
|
|
foreach ($pids as $pid) { |
|
|
if (is_numeric($pid)) { |
|
|
posix_kill($pid, 9); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
$this->updateStatus(false); |
|
|
if ($this->processInformation['running']) { |
|
|
$this->close(); |
|
|
} |
|
|
|
|
|
return $this->exitcode; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function addOutput($line) |
|
|
{ |
|
|
$this->lastOutputTime = microtime(true); |
|
|
$this->stdout .= $line; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function addErrorOutput($line) |
|
|
{ |
|
|
$this->lastOutputTime = microtime(true); |
|
|
$this->stderr .= $line; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function getCommandLine() |
|
|
{ |
|
|
return $this->commandline; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function setCommandLine($commandline) |
|
|
{ |
|
|
$this->commandline = $commandline; |
|
|
|
|
|
return $this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function getTimeout() |
|
|
{ |
|
|
return $this->timeout; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function getIdleTimeout() |
|
|
{ |
|
|
return $this->idleTimeout; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function setTimeout($timeout) |
|
|
{ |
|
|
$this->timeout = $this->validateTimeout($timeout); |
|
|
|
|
|
return $this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function setIdleTimeout($timeout) |
|
|
{ |
|
|
if (null !== $timeout && $this->outputDisabled) { |
|
|
throw new \LogicException('Idle timeout can not be set while the output is disabled.'); |
|
|
} |
|
|
|
|
|
$this->idleTimeout = $this->validateTimeout($timeout); |
|
|
|
|
|
return $this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function setTty($tty) |
|
|
{ |
|
|
if ('\\' === DS && $tty) { |
|
|
throw new \RuntimeException('TTY mode is not supported on Windows platform.'); |
|
|
} |
|
|
if ($tty && (!file_exists('/dev/tty') || !is_readable('/dev/tty'))) { |
|
|
throw new \RuntimeException('TTY mode requires /dev/tty to be readable.'); |
|
|
} |
|
|
|
|
|
$this->tty = (bool) $tty; |
|
|
|
|
|
return $this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function isTty() |
|
|
{ |
|
|
return $this->tty; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function setPty($bool) |
|
|
{ |
|
|
$this->pty = (bool) $bool; |
|
|
|
|
|
return $this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function isPty() |
|
|
{ |
|
|
return $this->pty; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function getWorkingDirectory() |
|
|
{ |
|
|
if (null === $this->cwd) { |
|
|
return getcwd() ?: null; |
|
|
} |
|
|
|
|
|
return $this->cwd; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function setWorkingDirectory($cwd) |
|
|
{ |
|
|
$this->cwd = $cwd; |
|
|
|
|
|
return $this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function getEnv() |
|
|
{ |
|
|
return $this->env; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function setEnv(array $env) |
|
|
{ |
|
|
$env = array_filter($env, function ($value) { |
|
|
return !is_array($value); |
|
|
}); |
|
|
|
|
|
$this->env = []; |
|
|
foreach ($env as $key => $value) { |
|
|
$this->env[(binary) $key] = (binary) $value; |
|
|
} |
|
|
|
|
|
return $this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function getInput() |
|
|
{ |
|
|
return $this->input; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function setInput($input) |
|
|
{ |
|
|
if ($this->isRunning()) { |
|
|
throw new \LogicException('Input can not be set while the process is running.'); |
|
|
} |
|
|
|
|
|
$this->input = Utils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input); |
|
|
|
|
|
return $this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function getOptions() |
|
|
{ |
|
|
return $this->options; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function setOptions(array $options) |
|
|
{ |
|
|
$this->options = $options; |
|
|
|
|
|
return $this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function getEnhanceWindowsCompatibility() |
|
|
{ |
|
|
return $this->enhanceWindowsCompatibility; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function setEnhanceWindowsCompatibility($enhance) |
|
|
{ |
|
|
$this->enhanceWindowsCompatibility = (bool) $enhance; |
|
|
|
|
|
return $this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function getEnhanceSigchildCompatibility() |
|
|
{ |
|
|
return $this->enhanceSigchildCompatibility; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function setEnhanceSigchildCompatibility($enhance) |
|
|
{ |
|
|
$this->enhanceSigchildCompatibility = (bool) $enhance; |
|
|
|
|
|
return $this; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public function checkTimeout() |
|
|
{ |
|
|
if (self::STATUS_STARTED !== $this->status) { |
|
|
return; |
|
|
} |
|
|
|
|
|
if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { |
|
|
$this->stop(); |
|
|
|
|
|
throw new ProcessTimeoutException($this, ProcessTimeoutException::TYPE_GENERAL); |
|
|
} |
|
|
|
|
|
if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { |
|
|
$this->stop(); |
|
|
|
|
|
throw new ProcessTimeoutException($this, ProcessTimeoutException::TYPE_IDLE); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static function isPtySupported() |
|
|
{ |
|
|
static $result; |
|
|
|
|
|
if (null !== $result) { |
|
|
return $result; |
|
|
} |
|
|
|
|
|
if ('\\' === DS) { |
|
|
return $result = false; |
|
|
} |
|
|
|
|
|
$proc = @proc_open('echo 1', [['pty'], ['pty'], ['pty']], $pipes); |
|
|
if (is_resource($proc)) { |
|
|
proc_close($proc); |
|
|
|
|
|
return $result = true; |
|
|
} |
|
|
|
|
|
return $result = false; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private function getDescriptors() |
|
|
{ |
|
|
if ('\\' === DS) { |
|
|
$this->processPipes = WindowsPipes::create($this, $this->input); |
|
|
} else { |
|
|
$this->processPipes = UnixPipes::create($this, $this->input); |
|
|
} |
|
|
$descriptors = $this->processPipes->getDescriptors($this->outputDisabled); |
|
|
|
|
|
if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { |
|
|
|
|
|
$descriptors = array_merge($descriptors, [['pipe', 'w']]); |
|
|
|
|
|
$this->commandline = '(' . $this->commandline . ') 3>/dev/null; code=$?; echo $code >&3; exit $code'; |
|
|
} |
|
|
|
|
|
return $descriptors; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function buildCallback($callback) |
|
|
{ |
|
|
$out = self::OUT; |
|
|
$callback = function ($type, $data) use ($callback, $out) { |
|
|
if ($out == $type) { |
|
|
$this->addOutput($data); |
|
|
} else { |
|
|
$this->addErrorOutput($data); |
|
|
} |
|
|
|
|
|
if (null !== $callback) { |
|
|
call_user_func($callback, $type, $data); |
|
|
} |
|
|
}; |
|
|
|
|
|
return $callback; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function updateStatus($blocking) |
|
|
{ |
|
|
if (self::STATUS_STARTED !== $this->status) { |
|
|
return; |
|
|
} |
|
|
|
|
|
$this->processInformation = proc_get_status($this->process); |
|
|
$this->captureExitCode(); |
|
|
|
|
|
$this->readPipes($blocking, '\\' === DS ? !$this->processInformation['running'] : true); |
|
|
|
|
|
if (!$this->processInformation['running']) { |
|
|
$this->close(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private function validateTimeout($timeout) |
|
|
{ |
|
|
$timeout = (float) $timeout; |
|
|
|
|
|
if (0.0 === $timeout) { |
|
|
$timeout = null; |
|
|
} elseif ($timeout < 0) { |
|
|
throw new \InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); |
|
|
} |
|
|
|
|
|
return $timeout; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private function readPipes($blocking, $close) |
|
|
{ |
|
|
$result = $this->processPipes->readAndWrite($blocking, $close); |
|
|
|
|
|
$callback = $this->callback; |
|
|
foreach ($result as $type => $data) { |
|
|
if (3 == $type) { |
|
|
$this->fallbackExitcode = (int) $data; |
|
|
} else { |
|
|
$callback(self::STDOUT === $type ? self::OUT : self::ERR, $data); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private function captureExitCode() |
|
|
{ |
|
|
if (isset($this->processInformation['exitcode']) && -1 != $this->processInformation['exitcode']) { |
|
|
$this->exitcode = $this->processInformation['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'] |
|
|
) { |
|
|
$this->exitcode = 128 + $this->processInformation['termsig']; |
|
|
} |
|
|
|
|
|
return $this->exitcode; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private function requireProcessIsStarted($functionName) |
|
|
{ |
|
|
if (!$this->isStarted()) { |
|
|
throw new \LogicException(sprintf('Process must be started before calling %s.', $functionName)); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private function requireProcessIsTerminated($functionName) |
|
|
{ |
|
|
if (!$this->isTerminated()) { |
|
|
throw new \LogicException(sprintf('Process must be terminated before calling %s.', $functionName)); |
|
|
} |
|
|
} |
|
|
} |
|
|
|