| | <?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)); |
| | } |
| | } |
| | } |
| |
|