1*04fd306cSNickeau<?php 2*04fd306cSNickeau 3*04fd306cSNickeau/* 4*04fd306cSNickeau * This file is part of the Symfony package. 5*04fd306cSNickeau * 6*04fd306cSNickeau * (c) Fabien Potencier <fabien@symfony.com> 7*04fd306cSNickeau * 8*04fd306cSNickeau * For the full copyright and license information, please view the LICENSE 9*04fd306cSNickeau * file that was distributed with this source code. 10*04fd306cSNickeau */ 11*04fd306cSNickeau 12*04fd306cSNickeaunamespace Symfony\Component\Process; 13*04fd306cSNickeau 14*04fd306cSNickeauuse Symfony\Component\Process\Exception\InvalidArgumentException; 15*04fd306cSNickeauuse Symfony\Component\Process\Exception\LogicException; 16*04fd306cSNickeauuse Symfony\Component\Process\Exception\ProcessFailedException; 17*04fd306cSNickeauuse Symfony\Component\Process\Exception\ProcessSignaledException; 18*04fd306cSNickeauuse Symfony\Component\Process\Exception\ProcessTimedOutException; 19*04fd306cSNickeauuse Symfony\Component\Process\Exception\RuntimeException; 20*04fd306cSNickeauuse Symfony\Component\Process\Pipes\PipesInterface; 21*04fd306cSNickeauuse Symfony\Component\Process\Pipes\UnixPipes; 22*04fd306cSNickeauuse Symfony\Component\Process\Pipes\WindowsPipes; 23*04fd306cSNickeau 24*04fd306cSNickeau/** 25*04fd306cSNickeau * Process is a thin wrapper around proc_* functions to easily 26*04fd306cSNickeau * start independent PHP processes. 27*04fd306cSNickeau * 28*04fd306cSNickeau * @author Fabien Potencier <fabien@symfony.com> 29*04fd306cSNickeau * @author Romain Neutron <imprec@gmail.com> 30*04fd306cSNickeau * 31*04fd306cSNickeau * @implements \IteratorAggregate<string, string> 32*04fd306cSNickeau */ 33*04fd306cSNickeauclass Process implements \IteratorAggregate 34*04fd306cSNickeau{ 35*04fd306cSNickeau public const ERR = 'err'; 36*04fd306cSNickeau public const OUT = 'out'; 37*04fd306cSNickeau 38*04fd306cSNickeau public const STATUS_READY = 'ready'; 39*04fd306cSNickeau public const STATUS_STARTED = 'started'; 40*04fd306cSNickeau public const STATUS_TERMINATED = 'terminated'; 41*04fd306cSNickeau 42*04fd306cSNickeau public const STDIN = 0; 43*04fd306cSNickeau public const STDOUT = 1; 44*04fd306cSNickeau public const STDERR = 2; 45*04fd306cSNickeau 46*04fd306cSNickeau // Timeout Precision in seconds. 47*04fd306cSNickeau public const TIMEOUT_PRECISION = 0.2; 48*04fd306cSNickeau 49*04fd306cSNickeau public const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking 50*04fd306cSNickeau public const ITER_KEEP_OUTPUT = 2; // By default, outputs are cleared while iterating, use this flag to keep them in memory 51*04fd306cSNickeau public const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating 52*04fd306cSNickeau public const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating 53*04fd306cSNickeau 54*04fd306cSNickeau private $callback; 55*04fd306cSNickeau private $hasCallback = false; 56*04fd306cSNickeau private $commandline; 57*04fd306cSNickeau private $cwd; 58*04fd306cSNickeau private $env = []; 59*04fd306cSNickeau private $input; 60*04fd306cSNickeau private $starttime; 61*04fd306cSNickeau private $lastOutputTime; 62*04fd306cSNickeau private $timeout; 63*04fd306cSNickeau private $idleTimeout; 64*04fd306cSNickeau private $exitcode; 65*04fd306cSNickeau private $fallbackStatus = []; 66*04fd306cSNickeau private $processInformation; 67*04fd306cSNickeau private $outputDisabled = false; 68*04fd306cSNickeau private $stdout; 69*04fd306cSNickeau private $stderr; 70*04fd306cSNickeau private $process; 71*04fd306cSNickeau private $status = self::STATUS_READY; 72*04fd306cSNickeau private $incrementalOutputOffset = 0; 73*04fd306cSNickeau private $incrementalErrorOutputOffset = 0; 74*04fd306cSNickeau private $tty = false; 75*04fd306cSNickeau private $pty; 76*04fd306cSNickeau private $options = ['suppress_errors' => true, 'bypass_shell' => true]; 77*04fd306cSNickeau 78*04fd306cSNickeau private $useFileHandles = false; 79*04fd306cSNickeau /** @var PipesInterface */ 80*04fd306cSNickeau private $processPipes; 81*04fd306cSNickeau 82*04fd306cSNickeau private $latestSignal; 83*04fd306cSNickeau 84*04fd306cSNickeau private static $sigchild; 85*04fd306cSNickeau 86*04fd306cSNickeau /** 87*04fd306cSNickeau * Exit codes translation table. 88*04fd306cSNickeau * 89*04fd306cSNickeau * User-defined errors must use exit codes in the 64-113 range. 90*04fd306cSNickeau */ 91*04fd306cSNickeau public static $exitCodes = [ 92*04fd306cSNickeau 0 => 'OK', 93*04fd306cSNickeau 1 => 'General error', 94*04fd306cSNickeau 2 => 'Misuse of shell builtins', 95*04fd306cSNickeau 96*04fd306cSNickeau 126 => 'Invoked command cannot execute', 97*04fd306cSNickeau 127 => 'Command not found', 98*04fd306cSNickeau 128 => 'Invalid exit argument', 99*04fd306cSNickeau 100*04fd306cSNickeau // signals 101*04fd306cSNickeau 129 => 'Hangup', 102*04fd306cSNickeau 130 => 'Interrupt', 103*04fd306cSNickeau 131 => 'Quit and dump core', 104*04fd306cSNickeau 132 => 'Illegal instruction', 105*04fd306cSNickeau 133 => 'Trace/breakpoint trap', 106*04fd306cSNickeau 134 => 'Process aborted', 107*04fd306cSNickeau 135 => 'Bus error: "access to undefined portion of memory object"', 108*04fd306cSNickeau 136 => 'Floating point exception: "erroneous arithmetic operation"', 109*04fd306cSNickeau 137 => 'Kill (terminate immediately)', 110*04fd306cSNickeau 138 => 'User-defined 1', 111*04fd306cSNickeau 139 => 'Segmentation violation', 112*04fd306cSNickeau 140 => 'User-defined 2', 113*04fd306cSNickeau 141 => 'Write to pipe with no one reading', 114*04fd306cSNickeau 142 => 'Signal raised by alarm', 115*04fd306cSNickeau 143 => 'Termination (request to terminate)', 116*04fd306cSNickeau // 144 - not defined 117*04fd306cSNickeau 145 => 'Child process terminated, stopped (or continued*)', 118*04fd306cSNickeau 146 => 'Continue if stopped', 119*04fd306cSNickeau 147 => 'Stop executing temporarily', 120*04fd306cSNickeau 148 => 'Terminal stop signal', 121*04fd306cSNickeau 149 => 'Background process attempting to read from tty ("in")', 122*04fd306cSNickeau 150 => 'Background process attempting to write to tty ("out")', 123*04fd306cSNickeau 151 => 'Urgent data available on socket', 124*04fd306cSNickeau 152 => 'CPU time limit exceeded', 125*04fd306cSNickeau 153 => 'File size limit exceeded', 126*04fd306cSNickeau 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', 127*04fd306cSNickeau 155 => 'Profiling timer expired', 128*04fd306cSNickeau // 156 - not defined 129*04fd306cSNickeau 157 => 'Pollable event', 130*04fd306cSNickeau // 158 - not defined 131*04fd306cSNickeau 159 => 'Bad syscall', 132*04fd306cSNickeau ]; 133*04fd306cSNickeau 134*04fd306cSNickeau /** 135*04fd306cSNickeau * @param array $command The command to run and its arguments listed as separate entries 136*04fd306cSNickeau * @param string|null $cwd The working directory or null to use the working dir of the current PHP process 137*04fd306cSNickeau * @param array|null $env The environment variables or null to use the same environment as the current PHP process 138*04fd306cSNickeau * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input 139*04fd306cSNickeau * @param int|float|null $timeout The timeout in seconds or null to disable 140*04fd306cSNickeau * 141*04fd306cSNickeau * @throws LogicException When proc_open is not installed 142*04fd306cSNickeau */ 143*04fd306cSNickeau public function __construct(array $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60) 144*04fd306cSNickeau { 145*04fd306cSNickeau if (!\function_exists('proc_open')) { 146*04fd306cSNickeau throw new LogicException('The Process class relies on proc_open, which is not available on your PHP installation.'); 147*04fd306cSNickeau } 148*04fd306cSNickeau 149*04fd306cSNickeau $this->commandline = $command; 150*04fd306cSNickeau $this->cwd = $cwd; 151*04fd306cSNickeau 152*04fd306cSNickeau // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started 153*04fd306cSNickeau // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected 154*04fd306cSNickeau // @see : https://bugs.php.net/51800 155*04fd306cSNickeau // @see : https://bugs.php.net/50524 156*04fd306cSNickeau if (null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR)) { 157*04fd306cSNickeau $this->cwd = getcwd(); 158*04fd306cSNickeau } 159*04fd306cSNickeau if (null !== $env) { 160*04fd306cSNickeau $this->setEnv($env); 161*04fd306cSNickeau } 162*04fd306cSNickeau 163*04fd306cSNickeau $this->setInput($input); 164*04fd306cSNickeau $this->setTimeout($timeout); 165*04fd306cSNickeau $this->useFileHandles = '\\' === \DIRECTORY_SEPARATOR; 166*04fd306cSNickeau $this->pty = false; 167*04fd306cSNickeau } 168*04fd306cSNickeau 169*04fd306cSNickeau /** 170*04fd306cSNickeau * Creates a Process instance as a command-line to be run in a shell wrapper. 171*04fd306cSNickeau * 172*04fd306cSNickeau * Command-lines are parsed by the shell of your OS (/bin/sh on Unix-like, cmd.exe on Windows.) 173*04fd306cSNickeau * This allows using e.g. pipes or conditional execution. In this mode, signals are sent to the 174*04fd306cSNickeau * shell wrapper and not to your commands. 175*04fd306cSNickeau * 176*04fd306cSNickeau * In order to inject dynamic values into command-lines, we strongly recommend using placeholders. 177*04fd306cSNickeau * This will save escaping values, which is not portable nor secure anyway: 178*04fd306cSNickeau * 179*04fd306cSNickeau * $process = Process::fromShellCommandline('my_command "${:MY_VAR}"'); 180*04fd306cSNickeau * $process->run(null, ['MY_VAR' => $theValue]); 181*04fd306cSNickeau * 182*04fd306cSNickeau * @param string $command The command line to pass to the shell of the OS 183*04fd306cSNickeau * @param string|null $cwd The working directory or null to use the working dir of the current PHP process 184*04fd306cSNickeau * @param array|null $env The environment variables or null to use the same environment as the current PHP process 185*04fd306cSNickeau * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input 186*04fd306cSNickeau * @param int|float|null $timeout The timeout in seconds or null to disable 187*04fd306cSNickeau * 188*04fd306cSNickeau * @return static 189*04fd306cSNickeau * 190*04fd306cSNickeau * @throws LogicException When proc_open is not installed 191*04fd306cSNickeau */ 192*04fd306cSNickeau public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60) 193*04fd306cSNickeau { 194*04fd306cSNickeau $process = new static([], $cwd, $env, $input, $timeout); 195*04fd306cSNickeau $process->commandline = $command; 196*04fd306cSNickeau 197*04fd306cSNickeau return $process; 198*04fd306cSNickeau } 199*04fd306cSNickeau 200*04fd306cSNickeau /** 201*04fd306cSNickeau * @return array 202*04fd306cSNickeau */ 203*04fd306cSNickeau public function __sleep() 204*04fd306cSNickeau { 205*04fd306cSNickeau throw new \BadMethodCallException('Cannot serialize '.__CLASS__); 206*04fd306cSNickeau } 207*04fd306cSNickeau 208*04fd306cSNickeau public function __wakeup() 209*04fd306cSNickeau { 210*04fd306cSNickeau throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); 211*04fd306cSNickeau } 212*04fd306cSNickeau 213*04fd306cSNickeau public function __destruct() 214*04fd306cSNickeau { 215*04fd306cSNickeau if ($this->options['create_new_console'] ?? false) { 216*04fd306cSNickeau $this->processPipes->close(); 217*04fd306cSNickeau } else { 218*04fd306cSNickeau $this->stop(0); 219*04fd306cSNickeau } 220*04fd306cSNickeau } 221*04fd306cSNickeau 222*04fd306cSNickeau public function __clone() 223*04fd306cSNickeau { 224*04fd306cSNickeau $this->resetProcessData(); 225*04fd306cSNickeau } 226*04fd306cSNickeau 227*04fd306cSNickeau /** 228*04fd306cSNickeau * Runs the process. 229*04fd306cSNickeau * 230*04fd306cSNickeau * The callback receives the type of output (out or err) and 231*04fd306cSNickeau * some bytes from the output in real-time. It allows to have feedback 232*04fd306cSNickeau * from the independent process during execution. 233*04fd306cSNickeau * 234*04fd306cSNickeau * The STDOUT and STDERR are also available after the process is finished 235*04fd306cSNickeau * via the getOutput() and getErrorOutput() methods. 236*04fd306cSNickeau * 237*04fd306cSNickeau * @param callable|null $callback A PHP callback to run whenever there is some 238*04fd306cSNickeau * output available on STDOUT or STDERR 239*04fd306cSNickeau * 240*04fd306cSNickeau * @return int The exit status code 241*04fd306cSNickeau * 242*04fd306cSNickeau * @throws RuntimeException When process can't be launched 243*04fd306cSNickeau * @throws RuntimeException When process is already running 244*04fd306cSNickeau * @throws ProcessTimedOutException When process timed out 245*04fd306cSNickeau * @throws ProcessSignaledException When process stopped after receiving signal 246*04fd306cSNickeau * @throws LogicException In case a callback is provided and output has been disabled 247*04fd306cSNickeau * 248*04fd306cSNickeau * @final 249*04fd306cSNickeau */ 250*04fd306cSNickeau public function run(callable $callback = null, array $env = []): int 251*04fd306cSNickeau { 252*04fd306cSNickeau $this->start($callback, $env); 253*04fd306cSNickeau 254*04fd306cSNickeau return $this->wait(); 255*04fd306cSNickeau } 256*04fd306cSNickeau 257*04fd306cSNickeau /** 258*04fd306cSNickeau * Runs the process. 259*04fd306cSNickeau * 260*04fd306cSNickeau * This is identical to run() except that an exception is thrown if the process 261*04fd306cSNickeau * exits with a non-zero exit code. 262*04fd306cSNickeau * 263*04fd306cSNickeau * @return $this 264*04fd306cSNickeau * 265*04fd306cSNickeau * @throws ProcessFailedException if the process didn't terminate successfully 266*04fd306cSNickeau * 267*04fd306cSNickeau * @final 268*04fd306cSNickeau */ 269*04fd306cSNickeau public function mustRun(callable $callback = null, array $env = []): self 270*04fd306cSNickeau { 271*04fd306cSNickeau if (0 !== $this->run($callback, $env)) { 272*04fd306cSNickeau throw new ProcessFailedException($this); 273*04fd306cSNickeau } 274*04fd306cSNickeau 275*04fd306cSNickeau return $this; 276*04fd306cSNickeau } 277*04fd306cSNickeau 278*04fd306cSNickeau /** 279*04fd306cSNickeau * Starts the process and returns after writing the input to STDIN. 280*04fd306cSNickeau * 281*04fd306cSNickeau * This method blocks until all STDIN data is sent to the process then it 282*04fd306cSNickeau * returns while the process runs in the background. 283*04fd306cSNickeau * 284*04fd306cSNickeau * The termination of the process can be awaited with wait(). 285*04fd306cSNickeau * 286*04fd306cSNickeau * The callback receives the type of output (out or err) and some bytes from 287*04fd306cSNickeau * the output in real-time while writing the standard input to the process. 288*04fd306cSNickeau * It allows to have feedback from the independent process during execution. 289*04fd306cSNickeau * 290*04fd306cSNickeau * @param callable|null $callback A PHP callback to run whenever there is some 291*04fd306cSNickeau * output available on STDOUT or STDERR 292*04fd306cSNickeau * 293*04fd306cSNickeau * @throws RuntimeException When process can't be launched 294*04fd306cSNickeau * @throws RuntimeException When process is already running 295*04fd306cSNickeau * @throws LogicException In case a callback is provided and output has been disabled 296*04fd306cSNickeau */ 297*04fd306cSNickeau public function start(callable $callback = null, array $env = []) 298*04fd306cSNickeau { 299*04fd306cSNickeau if ($this->isRunning()) { 300*04fd306cSNickeau throw new RuntimeException('Process is already running.'); 301*04fd306cSNickeau } 302*04fd306cSNickeau 303*04fd306cSNickeau $this->resetProcessData(); 304*04fd306cSNickeau $this->starttime = $this->lastOutputTime = microtime(true); 305*04fd306cSNickeau $this->callback = $this->buildCallback($callback); 306*04fd306cSNickeau $this->hasCallback = null !== $callback; 307*04fd306cSNickeau $descriptors = $this->getDescriptors(); 308*04fd306cSNickeau 309*04fd306cSNickeau if ($this->env) { 310*04fd306cSNickeau $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->env, $env, 'strcasecmp') : $this->env; 311*04fd306cSNickeau } 312*04fd306cSNickeau 313*04fd306cSNickeau $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->getDefaultEnv(), $env, 'strcasecmp') : $this->getDefaultEnv(); 314*04fd306cSNickeau 315*04fd306cSNickeau if (\is_array($commandline = $this->commandline)) { 316*04fd306cSNickeau $commandline = implode(' ', array_map([$this, 'escapeArgument'], $commandline)); 317*04fd306cSNickeau 318*04fd306cSNickeau if ('\\' !== \DIRECTORY_SEPARATOR) { 319*04fd306cSNickeau // exec is mandatory to deal with sending a signal to the process 320*04fd306cSNickeau $commandline = 'exec '.$commandline; 321*04fd306cSNickeau } 322*04fd306cSNickeau } else { 323*04fd306cSNickeau $commandline = $this->replacePlaceholders($commandline, $env); 324*04fd306cSNickeau } 325*04fd306cSNickeau 326*04fd306cSNickeau if ('\\' === \DIRECTORY_SEPARATOR) { 327*04fd306cSNickeau $commandline = $this->prepareWindowsCommandLine($commandline, $env); 328*04fd306cSNickeau } elseif (!$this->useFileHandles && $this->isSigchildEnabled()) { 329*04fd306cSNickeau // last exit code is output on the fourth pipe and caught to work around --enable-sigchild 330*04fd306cSNickeau $descriptors[3] = ['pipe', 'w']; 331*04fd306cSNickeau 332*04fd306cSNickeau // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input 333*04fd306cSNickeau $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;'; 334*04fd306cSNickeau $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code'; 335*04fd306cSNickeau 336*04fd306cSNickeau // Workaround for the bug, when PTS functionality is enabled. 337*04fd306cSNickeau // @see : https://bugs.php.net/69442 338*04fd306cSNickeau $ptsWorkaround = fopen(__FILE__, 'r'); 339*04fd306cSNickeau } 340*04fd306cSNickeau 341*04fd306cSNickeau $envPairs = []; 342*04fd306cSNickeau foreach ($env as $k => $v) { 343*04fd306cSNickeau if (false !== $v && false === \in_array($k, ['argc', 'argv', 'ARGC', 'ARGV'], true)) { 344*04fd306cSNickeau $envPairs[] = $k.'='.$v; 345*04fd306cSNickeau } 346*04fd306cSNickeau } 347*04fd306cSNickeau 348*04fd306cSNickeau if (!is_dir($this->cwd)) { 349*04fd306cSNickeau throw new RuntimeException(sprintf('The provided cwd "%s" does not exist.', $this->cwd)); 350*04fd306cSNickeau } 351*04fd306cSNickeau 352*04fd306cSNickeau $this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); 353*04fd306cSNickeau 354*04fd306cSNickeau if (!\is_resource($this->process)) { 355*04fd306cSNickeau throw new RuntimeException('Unable to launch a new process.'); 356*04fd306cSNickeau } 357*04fd306cSNickeau $this->status = self::STATUS_STARTED; 358*04fd306cSNickeau 359*04fd306cSNickeau if (isset($descriptors[3])) { 360*04fd306cSNickeau $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]); 361*04fd306cSNickeau } 362*04fd306cSNickeau 363*04fd306cSNickeau if ($this->tty) { 364*04fd306cSNickeau return; 365*04fd306cSNickeau } 366*04fd306cSNickeau 367*04fd306cSNickeau $this->updateStatus(false); 368*04fd306cSNickeau $this->checkTimeout(); 369*04fd306cSNickeau } 370*04fd306cSNickeau 371*04fd306cSNickeau /** 372*04fd306cSNickeau * Restarts the process. 373*04fd306cSNickeau * 374*04fd306cSNickeau * Be warned that the process is cloned before being started. 375*04fd306cSNickeau * 376*04fd306cSNickeau * @param callable|null $callback A PHP callback to run whenever there is some 377*04fd306cSNickeau * output available on STDOUT or STDERR 378*04fd306cSNickeau * 379*04fd306cSNickeau * @return static 380*04fd306cSNickeau * 381*04fd306cSNickeau * @throws RuntimeException When process can't be launched 382*04fd306cSNickeau * @throws RuntimeException When process is already running 383*04fd306cSNickeau * 384*04fd306cSNickeau * @see start() 385*04fd306cSNickeau * 386*04fd306cSNickeau * @final 387*04fd306cSNickeau */ 388*04fd306cSNickeau public function restart(callable $callback = null, array $env = []): self 389*04fd306cSNickeau { 390*04fd306cSNickeau if ($this->isRunning()) { 391*04fd306cSNickeau throw new RuntimeException('Process is already running.'); 392*04fd306cSNickeau } 393*04fd306cSNickeau 394*04fd306cSNickeau $process = clone $this; 395*04fd306cSNickeau $process->start($callback, $env); 396*04fd306cSNickeau 397*04fd306cSNickeau return $process; 398*04fd306cSNickeau } 399*04fd306cSNickeau 400*04fd306cSNickeau /** 401*04fd306cSNickeau * Waits for the process to terminate. 402*04fd306cSNickeau * 403*04fd306cSNickeau * The callback receives the type of output (out or err) and some bytes 404*04fd306cSNickeau * from the output in real-time while writing the standard input to the process. 405*04fd306cSNickeau * It allows to have feedback from the independent process during execution. 406*04fd306cSNickeau * 407*04fd306cSNickeau * @param callable|null $callback A valid PHP callback 408*04fd306cSNickeau * 409*04fd306cSNickeau * @return int The exitcode of the process 410*04fd306cSNickeau * 411*04fd306cSNickeau * @throws ProcessTimedOutException When process timed out 412*04fd306cSNickeau * @throws ProcessSignaledException When process stopped after receiving signal 413*04fd306cSNickeau * @throws LogicException When process is not yet started 414*04fd306cSNickeau */ 415*04fd306cSNickeau public function wait(callable $callback = null) 416*04fd306cSNickeau { 417*04fd306cSNickeau $this->requireProcessIsStarted(__FUNCTION__); 418*04fd306cSNickeau 419*04fd306cSNickeau $this->updateStatus(false); 420*04fd306cSNickeau 421*04fd306cSNickeau if (null !== $callback) { 422*04fd306cSNickeau if (!$this->processPipes->haveReadSupport()) { 423*04fd306cSNickeau $this->stop(0); 424*04fd306cSNickeau throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait".'); 425*04fd306cSNickeau } 426*04fd306cSNickeau $this->callback = $this->buildCallback($callback); 427*04fd306cSNickeau } 428*04fd306cSNickeau 429*04fd306cSNickeau do { 430*04fd306cSNickeau $this->checkTimeout(); 431*04fd306cSNickeau $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); 432*04fd306cSNickeau $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running); 433*04fd306cSNickeau } while ($running); 434*04fd306cSNickeau 435*04fd306cSNickeau while ($this->isRunning()) { 436*04fd306cSNickeau $this->checkTimeout(); 437*04fd306cSNickeau usleep(1000); 438*04fd306cSNickeau } 439*04fd306cSNickeau 440*04fd306cSNickeau if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) { 441*04fd306cSNickeau throw new ProcessSignaledException($this); 442*04fd306cSNickeau } 443*04fd306cSNickeau 444*04fd306cSNickeau return $this->exitcode; 445*04fd306cSNickeau } 446*04fd306cSNickeau 447*04fd306cSNickeau /** 448*04fd306cSNickeau * Waits until the callback returns true. 449*04fd306cSNickeau * 450*04fd306cSNickeau * The callback receives the type of output (out or err) and some bytes 451*04fd306cSNickeau * from the output in real-time while writing the standard input to the process. 452*04fd306cSNickeau * It allows to have feedback from the independent process during execution. 453*04fd306cSNickeau * 454*04fd306cSNickeau * @throws RuntimeException When process timed out 455*04fd306cSNickeau * @throws LogicException When process is not yet started 456*04fd306cSNickeau * @throws ProcessTimedOutException In case the timeout was reached 457*04fd306cSNickeau */ 458*04fd306cSNickeau public function waitUntil(callable $callback): bool 459*04fd306cSNickeau { 460*04fd306cSNickeau $this->requireProcessIsStarted(__FUNCTION__); 461*04fd306cSNickeau $this->updateStatus(false); 462*04fd306cSNickeau 463*04fd306cSNickeau if (!$this->processPipes->haveReadSupport()) { 464*04fd306cSNickeau $this->stop(0); 465*04fd306cSNickeau throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::waitUntil".'); 466*04fd306cSNickeau } 467*04fd306cSNickeau $callback = $this->buildCallback($callback); 468*04fd306cSNickeau 469*04fd306cSNickeau $ready = false; 470*04fd306cSNickeau while (true) { 471*04fd306cSNickeau $this->checkTimeout(); 472*04fd306cSNickeau $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); 473*04fd306cSNickeau $output = $this->processPipes->readAndWrite($running, '\\' !== \DIRECTORY_SEPARATOR || !$running); 474*04fd306cSNickeau 475*04fd306cSNickeau foreach ($output as $type => $data) { 476*04fd306cSNickeau if (3 !== $type) { 477*04fd306cSNickeau $ready = $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data) || $ready; 478*04fd306cSNickeau } elseif (!isset($this->fallbackStatus['signaled'])) { 479*04fd306cSNickeau $this->fallbackStatus['exitcode'] = (int) $data; 480*04fd306cSNickeau } 481*04fd306cSNickeau } 482*04fd306cSNickeau if ($ready) { 483*04fd306cSNickeau return true; 484*04fd306cSNickeau } 485*04fd306cSNickeau if (!$running) { 486*04fd306cSNickeau return false; 487*04fd306cSNickeau } 488*04fd306cSNickeau 489*04fd306cSNickeau usleep(1000); 490*04fd306cSNickeau } 491*04fd306cSNickeau } 492*04fd306cSNickeau 493*04fd306cSNickeau /** 494*04fd306cSNickeau * Returns the Pid (process identifier), if applicable. 495*04fd306cSNickeau * 496*04fd306cSNickeau * @return int|null The process id if running, null otherwise 497*04fd306cSNickeau */ 498*04fd306cSNickeau public function getPid() 499*04fd306cSNickeau { 500*04fd306cSNickeau return $this->isRunning() ? $this->processInformation['pid'] : null; 501*04fd306cSNickeau } 502*04fd306cSNickeau 503*04fd306cSNickeau /** 504*04fd306cSNickeau * Sends a POSIX signal to the process. 505*04fd306cSNickeau * 506*04fd306cSNickeau * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants) 507*04fd306cSNickeau * 508*04fd306cSNickeau * @return $this 509*04fd306cSNickeau * 510*04fd306cSNickeau * @throws LogicException In case the process is not running 511*04fd306cSNickeau * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed 512*04fd306cSNickeau * @throws RuntimeException In case of failure 513*04fd306cSNickeau */ 514*04fd306cSNickeau public function signal(int $signal) 515*04fd306cSNickeau { 516*04fd306cSNickeau $this->doSignal($signal, true); 517*04fd306cSNickeau 518*04fd306cSNickeau return $this; 519*04fd306cSNickeau } 520*04fd306cSNickeau 521*04fd306cSNickeau /** 522*04fd306cSNickeau * Disables fetching output and error output from the underlying process. 523*04fd306cSNickeau * 524*04fd306cSNickeau * @return $this 525*04fd306cSNickeau * 526*04fd306cSNickeau * @throws RuntimeException In case the process is already running 527*04fd306cSNickeau * @throws LogicException if an idle timeout is set 528*04fd306cSNickeau */ 529*04fd306cSNickeau public function disableOutput() 530*04fd306cSNickeau { 531*04fd306cSNickeau if ($this->isRunning()) { 532*04fd306cSNickeau throw new RuntimeException('Disabling output while the process is running is not possible.'); 533*04fd306cSNickeau } 534*04fd306cSNickeau if (null !== $this->idleTimeout) { 535*04fd306cSNickeau throw new LogicException('Output cannot be disabled while an idle timeout is set.'); 536*04fd306cSNickeau } 537*04fd306cSNickeau 538*04fd306cSNickeau $this->outputDisabled = true; 539*04fd306cSNickeau 540*04fd306cSNickeau return $this; 541*04fd306cSNickeau } 542*04fd306cSNickeau 543*04fd306cSNickeau /** 544*04fd306cSNickeau * Enables fetching output and error output from the underlying process. 545*04fd306cSNickeau * 546*04fd306cSNickeau * @return $this 547*04fd306cSNickeau * 548*04fd306cSNickeau * @throws RuntimeException In case the process is already running 549*04fd306cSNickeau */ 550*04fd306cSNickeau public function enableOutput() 551*04fd306cSNickeau { 552*04fd306cSNickeau if ($this->isRunning()) { 553*04fd306cSNickeau throw new RuntimeException('Enabling output while the process is running is not possible.'); 554*04fd306cSNickeau } 555*04fd306cSNickeau 556*04fd306cSNickeau $this->outputDisabled = false; 557*04fd306cSNickeau 558*04fd306cSNickeau return $this; 559*04fd306cSNickeau } 560*04fd306cSNickeau 561*04fd306cSNickeau /** 562*04fd306cSNickeau * Returns true in case the output is disabled, false otherwise. 563*04fd306cSNickeau * 564*04fd306cSNickeau * @return bool 565*04fd306cSNickeau */ 566*04fd306cSNickeau public function isOutputDisabled() 567*04fd306cSNickeau { 568*04fd306cSNickeau return $this->outputDisabled; 569*04fd306cSNickeau } 570*04fd306cSNickeau 571*04fd306cSNickeau /** 572*04fd306cSNickeau * Returns the current output of the process (STDOUT). 573*04fd306cSNickeau * 574*04fd306cSNickeau * @return string 575*04fd306cSNickeau * 576*04fd306cSNickeau * @throws LogicException in case the output has been disabled 577*04fd306cSNickeau * @throws LogicException In case the process is not started 578*04fd306cSNickeau */ 579*04fd306cSNickeau public function getOutput() 580*04fd306cSNickeau { 581*04fd306cSNickeau $this->readPipesForOutput(__FUNCTION__); 582*04fd306cSNickeau 583*04fd306cSNickeau if (false === $ret = stream_get_contents($this->stdout, -1, 0)) { 584*04fd306cSNickeau return ''; 585*04fd306cSNickeau } 586*04fd306cSNickeau 587*04fd306cSNickeau return $ret; 588*04fd306cSNickeau } 589*04fd306cSNickeau 590*04fd306cSNickeau /** 591*04fd306cSNickeau * Returns the output incrementally. 592*04fd306cSNickeau * 593*04fd306cSNickeau * In comparison with the getOutput method which always return the whole 594*04fd306cSNickeau * output, this one returns the new output since the last call. 595*04fd306cSNickeau * 596*04fd306cSNickeau * @return string 597*04fd306cSNickeau * 598*04fd306cSNickeau * @throws LogicException in case the output has been disabled 599*04fd306cSNickeau * @throws LogicException In case the process is not started 600*04fd306cSNickeau */ 601*04fd306cSNickeau public function getIncrementalOutput() 602*04fd306cSNickeau { 603*04fd306cSNickeau $this->readPipesForOutput(__FUNCTION__); 604*04fd306cSNickeau 605*04fd306cSNickeau $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); 606*04fd306cSNickeau $this->incrementalOutputOffset = ftell($this->stdout); 607*04fd306cSNickeau 608*04fd306cSNickeau if (false === $latest) { 609*04fd306cSNickeau return ''; 610*04fd306cSNickeau } 611*04fd306cSNickeau 612*04fd306cSNickeau return $latest; 613*04fd306cSNickeau } 614*04fd306cSNickeau 615*04fd306cSNickeau /** 616*04fd306cSNickeau * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR). 617*04fd306cSNickeau * 618*04fd306cSNickeau * @param int $flags A bit field of Process::ITER_* flags 619*04fd306cSNickeau * 620*04fd306cSNickeau * @throws LogicException in case the output has been disabled 621*04fd306cSNickeau * @throws LogicException In case the process is not started 622*04fd306cSNickeau * 623*04fd306cSNickeau * @return \Generator<string, string> 624*04fd306cSNickeau */ 625*04fd306cSNickeau #[\ReturnTypeWillChange] 626*04fd306cSNickeau public function getIterator(int $flags = 0) 627*04fd306cSNickeau { 628*04fd306cSNickeau $this->readPipesForOutput(__FUNCTION__, false); 629*04fd306cSNickeau 630*04fd306cSNickeau $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags); 631*04fd306cSNickeau $blocking = !(self::ITER_NON_BLOCKING & $flags); 632*04fd306cSNickeau $yieldOut = !(self::ITER_SKIP_OUT & $flags); 633*04fd306cSNickeau $yieldErr = !(self::ITER_SKIP_ERR & $flags); 634*04fd306cSNickeau 635*04fd306cSNickeau while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) { 636*04fd306cSNickeau if ($yieldOut) { 637*04fd306cSNickeau $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); 638*04fd306cSNickeau 639*04fd306cSNickeau if (isset($out[0])) { 640*04fd306cSNickeau if ($clearOutput) { 641*04fd306cSNickeau $this->clearOutput(); 642*04fd306cSNickeau } else { 643*04fd306cSNickeau $this->incrementalOutputOffset = ftell($this->stdout); 644*04fd306cSNickeau } 645*04fd306cSNickeau 646*04fd306cSNickeau yield self::OUT => $out; 647*04fd306cSNickeau } 648*04fd306cSNickeau } 649*04fd306cSNickeau 650*04fd306cSNickeau if ($yieldErr) { 651*04fd306cSNickeau $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); 652*04fd306cSNickeau 653*04fd306cSNickeau if (isset($err[0])) { 654*04fd306cSNickeau if ($clearOutput) { 655*04fd306cSNickeau $this->clearErrorOutput(); 656*04fd306cSNickeau } else { 657*04fd306cSNickeau $this->incrementalErrorOutputOffset = ftell($this->stderr); 658*04fd306cSNickeau } 659*04fd306cSNickeau 660*04fd306cSNickeau yield self::ERR => $err; 661*04fd306cSNickeau } 662*04fd306cSNickeau } 663*04fd306cSNickeau 664*04fd306cSNickeau if (!$blocking && !isset($out[0]) && !isset($err[0])) { 665*04fd306cSNickeau yield self::OUT => ''; 666*04fd306cSNickeau } 667*04fd306cSNickeau 668*04fd306cSNickeau $this->checkTimeout(); 669*04fd306cSNickeau $this->readPipesForOutput(__FUNCTION__, $blocking); 670*04fd306cSNickeau } 671*04fd306cSNickeau } 672*04fd306cSNickeau 673*04fd306cSNickeau /** 674*04fd306cSNickeau * Clears the process output. 675*04fd306cSNickeau * 676*04fd306cSNickeau * @return $this 677*04fd306cSNickeau */ 678*04fd306cSNickeau public function clearOutput() 679*04fd306cSNickeau { 680*04fd306cSNickeau ftruncate($this->stdout, 0); 681*04fd306cSNickeau fseek($this->stdout, 0); 682*04fd306cSNickeau $this->incrementalOutputOffset = 0; 683*04fd306cSNickeau 684*04fd306cSNickeau return $this; 685*04fd306cSNickeau } 686*04fd306cSNickeau 687*04fd306cSNickeau /** 688*04fd306cSNickeau * Returns the current error output of the process (STDERR). 689*04fd306cSNickeau * 690*04fd306cSNickeau * @return string 691*04fd306cSNickeau * 692*04fd306cSNickeau * @throws LogicException in case the output has been disabled 693*04fd306cSNickeau * @throws LogicException In case the process is not started 694*04fd306cSNickeau */ 695*04fd306cSNickeau public function getErrorOutput() 696*04fd306cSNickeau { 697*04fd306cSNickeau $this->readPipesForOutput(__FUNCTION__); 698*04fd306cSNickeau 699*04fd306cSNickeau if (false === $ret = stream_get_contents($this->stderr, -1, 0)) { 700*04fd306cSNickeau return ''; 701*04fd306cSNickeau } 702*04fd306cSNickeau 703*04fd306cSNickeau return $ret; 704*04fd306cSNickeau } 705*04fd306cSNickeau 706*04fd306cSNickeau /** 707*04fd306cSNickeau * Returns the errorOutput incrementally. 708*04fd306cSNickeau * 709*04fd306cSNickeau * In comparison with the getErrorOutput method which always return the 710*04fd306cSNickeau * whole error output, this one returns the new error output since the last 711*04fd306cSNickeau * call. 712*04fd306cSNickeau * 713*04fd306cSNickeau * @return string 714*04fd306cSNickeau * 715*04fd306cSNickeau * @throws LogicException in case the output has been disabled 716*04fd306cSNickeau * @throws LogicException In case the process is not started 717*04fd306cSNickeau */ 718*04fd306cSNickeau public function getIncrementalErrorOutput() 719*04fd306cSNickeau { 720*04fd306cSNickeau $this->readPipesForOutput(__FUNCTION__); 721*04fd306cSNickeau 722*04fd306cSNickeau $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); 723*04fd306cSNickeau $this->incrementalErrorOutputOffset = ftell($this->stderr); 724*04fd306cSNickeau 725*04fd306cSNickeau if (false === $latest) { 726*04fd306cSNickeau return ''; 727*04fd306cSNickeau } 728*04fd306cSNickeau 729*04fd306cSNickeau return $latest; 730*04fd306cSNickeau } 731*04fd306cSNickeau 732*04fd306cSNickeau /** 733*04fd306cSNickeau * Clears the process output. 734*04fd306cSNickeau * 735*04fd306cSNickeau * @return $this 736*04fd306cSNickeau */ 737*04fd306cSNickeau public function clearErrorOutput() 738*04fd306cSNickeau { 739*04fd306cSNickeau ftruncate($this->stderr, 0); 740*04fd306cSNickeau fseek($this->stderr, 0); 741*04fd306cSNickeau $this->incrementalErrorOutputOffset = 0; 742*04fd306cSNickeau 743*04fd306cSNickeau return $this; 744*04fd306cSNickeau } 745*04fd306cSNickeau 746*04fd306cSNickeau /** 747*04fd306cSNickeau * Returns the exit code returned by the process. 748*04fd306cSNickeau * 749*04fd306cSNickeau * @return int|null The exit status code, null if the Process is not terminated 750*04fd306cSNickeau */ 751*04fd306cSNickeau public function getExitCode() 752*04fd306cSNickeau { 753*04fd306cSNickeau $this->updateStatus(false); 754*04fd306cSNickeau 755*04fd306cSNickeau return $this->exitcode; 756*04fd306cSNickeau } 757*04fd306cSNickeau 758*04fd306cSNickeau /** 759*04fd306cSNickeau * Returns a string representation for the exit code returned by the process. 760*04fd306cSNickeau * 761*04fd306cSNickeau * This method relies on the Unix exit code status standardization 762*04fd306cSNickeau * and might not be relevant for other operating systems. 763*04fd306cSNickeau * 764*04fd306cSNickeau * @return string|null A string representation for the exit status code, null if the Process is not terminated 765*04fd306cSNickeau * 766*04fd306cSNickeau * @see http://tldp.org/LDP/abs/html/exitcodes.html 767*04fd306cSNickeau * @see http://en.wikipedia.org/wiki/Unix_signal 768*04fd306cSNickeau */ 769*04fd306cSNickeau public function getExitCodeText() 770*04fd306cSNickeau { 771*04fd306cSNickeau if (null === $exitcode = $this->getExitCode()) { 772*04fd306cSNickeau return null; 773*04fd306cSNickeau } 774*04fd306cSNickeau 775*04fd306cSNickeau return self::$exitCodes[$exitcode] ?? 'Unknown error'; 776*04fd306cSNickeau } 777*04fd306cSNickeau 778*04fd306cSNickeau /** 779*04fd306cSNickeau * Checks if the process ended successfully. 780*04fd306cSNickeau * 781*04fd306cSNickeau * @return bool 782*04fd306cSNickeau */ 783*04fd306cSNickeau public function isSuccessful() 784*04fd306cSNickeau { 785*04fd306cSNickeau return 0 === $this->getExitCode(); 786*04fd306cSNickeau } 787*04fd306cSNickeau 788*04fd306cSNickeau /** 789*04fd306cSNickeau * Returns true if the child process has been terminated by an uncaught signal. 790*04fd306cSNickeau * 791*04fd306cSNickeau * It always returns false on Windows. 792*04fd306cSNickeau * 793*04fd306cSNickeau * @return bool 794*04fd306cSNickeau * 795*04fd306cSNickeau * @throws LogicException In case the process is not terminated 796*04fd306cSNickeau */ 797*04fd306cSNickeau public function hasBeenSignaled() 798*04fd306cSNickeau { 799*04fd306cSNickeau $this->requireProcessIsTerminated(__FUNCTION__); 800*04fd306cSNickeau 801*04fd306cSNickeau return $this->processInformation['signaled']; 802*04fd306cSNickeau } 803*04fd306cSNickeau 804*04fd306cSNickeau /** 805*04fd306cSNickeau * Returns the number of the signal that caused the child process to terminate its execution. 806*04fd306cSNickeau * 807*04fd306cSNickeau * It is only meaningful if hasBeenSignaled() returns true. 808*04fd306cSNickeau * 809*04fd306cSNickeau * @return int 810*04fd306cSNickeau * 811*04fd306cSNickeau * @throws RuntimeException In case --enable-sigchild is activated 812*04fd306cSNickeau * @throws LogicException In case the process is not terminated 813*04fd306cSNickeau */ 814*04fd306cSNickeau public function getTermSignal() 815*04fd306cSNickeau { 816*04fd306cSNickeau $this->requireProcessIsTerminated(__FUNCTION__); 817*04fd306cSNickeau 818*04fd306cSNickeau if ($this->isSigchildEnabled() && -1 === $this->processInformation['termsig']) { 819*04fd306cSNickeau throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal cannot be retrieved.'); 820*04fd306cSNickeau } 821*04fd306cSNickeau 822*04fd306cSNickeau return $this->processInformation['termsig']; 823*04fd306cSNickeau } 824*04fd306cSNickeau 825*04fd306cSNickeau /** 826*04fd306cSNickeau * Returns true if the child process has been stopped by a signal. 827*04fd306cSNickeau * 828*04fd306cSNickeau * It always returns false on Windows. 829*04fd306cSNickeau * 830*04fd306cSNickeau * @return bool 831*04fd306cSNickeau * 832*04fd306cSNickeau * @throws LogicException In case the process is not terminated 833*04fd306cSNickeau */ 834*04fd306cSNickeau public function hasBeenStopped() 835*04fd306cSNickeau { 836*04fd306cSNickeau $this->requireProcessIsTerminated(__FUNCTION__); 837*04fd306cSNickeau 838*04fd306cSNickeau return $this->processInformation['stopped']; 839*04fd306cSNickeau } 840*04fd306cSNickeau 841*04fd306cSNickeau /** 842*04fd306cSNickeau * Returns the number of the signal that caused the child process to stop its execution. 843*04fd306cSNickeau * 844*04fd306cSNickeau * It is only meaningful if hasBeenStopped() returns true. 845*04fd306cSNickeau * 846*04fd306cSNickeau * @return int 847*04fd306cSNickeau * 848*04fd306cSNickeau * @throws LogicException In case the process is not terminated 849*04fd306cSNickeau */ 850*04fd306cSNickeau public function getStopSignal() 851*04fd306cSNickeau { 852*04fd306cSNickeau $this->requireProcessIsTerminated(__FUNCTION__); 853*04fd306cSNickeau 854*04fd306cSNickeau return $this->processInformation['stopsig']; 855*04fd306cSNickeau } 856*04fd306cSNickeau 857*04fd306cSNickeau /** 858*04fd306cSNickeau * Checks if the process is currently running. 859*04fd306cSNickeau * 860*04fd306cSNickeau * @return bool 861*04fd306cSNickeau */ 862*04fd306cSNickeau public function isRunning() 863*04fd306cSNickeau { 864*04fd306cSNickeau if (self::STATUS_STARTED !== $this->status) { 865*04fd306cSNickeau return false; 866*04fd306cSNickeau } 867*04fd306cSNickeau 868*04fd306cSNickeau $this->updateStatus(false); 869*04fd306cSNickeau 870*04fd306cSNickeau return $this->processInformation['running']; 871*04fd306cSNickeau } 872*04fd306cSNickeau 873*04fd306cSNickeau /** 874*04fd306cSNickeau * Checks if the process has been started with no regard to the current state. 875*04fd306cSNickeau * 876*04fd306cSNickeau * @return bool 877*04fd306cSNickeau */ 878*04fd306cSNickeau public function isStarted() 879*04fd306cSNickeau { 880*04fd306cSNickeau return self::STATUS_READY != $this->status; 881*04fd306cSNickeau } 882*04fd306cSNickeau 883*04fd306cSNickeau /** 884*04fd306cSNickeau * Checks if the process is terminated. 885*04fd306cSNickeau * 886*04fd306cSNickeau * @return bool 887*04fd306cSNickeau */ 888*04fd306cSNickeau public function isTerminated() 889*04fd306cSNickeau { 890*04fd306cSNickeau $this->updateStatus(false); 891*04fd306cSNickeau 892*04fd306cSNickeau return self::STATUS_TERMINATED == $this->status; 893*04fd306cSNickeau } 894*04fd306cSNickeau 895*04fd306cSNickeau /** 896*04fd306cSNickeau * Gets the process status. 897*04fd306cSNickeau * 898*04fd306cSNickeau * The status is one of: ready, started, terminated. 899*04fd306cSNickeau * 900*04fd306cSNickeau * @return string 901*04fd306cSNickeau */ 902*04fd306cSNickeau public function getStatus() 903*04fd306cSNickeau { 904*04fd306cSNickeau $this->updateStatus(false); 905*04fd306cSNickeau 906*04fd306cSNickeau return $this->status; 907*04fd306cSNickeau } 908*04fd306cSNickeau 909*04fd306cSNickeau /** 910*04fd306cSNickeau * Stops the process. 911*04fd306cSNickeau * 912*04fd306cSNickeau * @param int|float $timeout The timeout in seconds 913*04fd306cSNickeau * @param int $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9) 914*04fd306cSNickeau * 915*04fd306cSNickeau * @return int|null The exit-code of the process or null if it's not running 916*04fd306cSNickeau */ 917*04fd306cSNickeau public function stop(float $timeout = 10, int $signal = null) 918*04fd306cSNickeau { 919*04fd306cSNickeau $timeoutMicro = microtime(true) + $timeout; 920*04fd306cSNickeau if ($this->isRunning()) { 921*04fd306cSNickeau // given SIGTERM may not be defined and that "proc_terminate" uses the constant value and not the constant itself, we use the same here 922*04fd306cSNickeau $this->doSignal(15, false); 923*04fd306cSNickeau do { 924*04fd306cSNickeau usleep(1000); 925*04fd306cSNickeau } while ($this->isRunning() && microtime(true) < $timeoutMicro); 926*04fd306cSNickeau 927*04fd306cSNickeau if ($this->isRunning()) { 928*04fd306cSNickeau // Avoid exception here: process is supposed to be running, but it might have stopped just 929*04fd306cSNickeau // after this line. In any case, let's silently discard the error, we cannot do anything. 930*04fd306cSNickeau $this->doSignal($signal ?: 9, false); 931*04fd306cSNickeau } 932*04fd306cSNickeau } 933*04fd306cSNickeau 934*04fd306cSNickeau if ($this->isRunning()) { 935*04fd306cSNickeau if (isset($this->fallbackStatus['pid'])) { 936*04fd306cSNickeau unset($this->fallbackStatus['pid']); 937*04fd306cSNickeau 938*04fd306cSNickeau return $this->stop(0, $signal); 939*04fd306cSNickeau } 940*04fd306cSNickeau $this->close(); 941*04fd306cSNickeau } 942*04fd306cSNickeau 943*04fd306cSNickeau return $this->exitcode; 944*04fd306cSNickeau } 945*04fd306cSNickeau 946*04fd306cSNickeau /** 947*04fd306cSNickeau * Adds a line to the STDOUT stream. 948*04fd306cSNickeau * 949*04fd306cSNickeau * @internal 950*04fd306cSNickeau */ 951*04fd306cSNickeau public function addOutput(string $line) 952*04fd306cSNickeau { 953*04fd306cSNickeau $this->lastOutputTime = microtime(true); 954*04fd306cSNickeau 955*04fd306cSNickeau fseek($this->stdout, 0, \SEEK_END); 956*04fd306cSNickeau fwrite($this->stdout, $line); 957*04fd306cSNickeau fseek($this->stdout, $this->incrementalOutputOffset); 958*04fd306cSNickeau } 959*04fd306cSNickeau 960*04fd306cSNickeau /** 961*04fd306cSNickeau * Adds a line to the STDERR stream. 962*04fd306cSNickeau * 963*04fd306cSNickeau * @internal 964*04fd306cSNickeau */ 965*04fd306cSNickeau public function addErrorOutput(string $line) 966*04fd306cSNickeau { 967*04fd306cSNickeau $this->lastOutputTime = microtime(true); 968*04fd306cSNickeau 969*04fd306cSNickeau fseek($this->stderr, 0, \SEEK_END); 970*04fd306cSNickeau fwrite($this->stderr, $line); 971*04fd306cSNickeau fseek($this->stderr, $this->incrementalErrorOutputOffset); 972*04fd306cSNickeau } 973*04fd306cSNickeau 974*04fd306cSNickeau /** 975*04fd306cSNickeau * Gets the last output time in seconds. 976*04fd306cSNickeau */ 977*04fd306cSNickeau public function getLastOutputTime(): ?float 978*04fd306cSNickeau { 979*04fd306cSNickeau return $this->lastOutputTime; 980*04fd306cSNickeau } 981*04fd306cSNickeau 982*04fd306cSNickeau /** 983*04fd306cSNickeau * Gets the command line to be executed. 984*04fd306cSNickeau * 985*04fd306cSNickeau * @return string 986*04fd306cSNickeau */ 987*04fd306cSNickeau public function getCommandLine() 988*04fd306cSNickeau { 989*04fd306cSNickeau return \is_array($this->commandline) ? implode(' ', array_map([$this, 'escapeArgument'], $this->commandline)) : $this->commandline; 990*04fd306cSNickeau } 991*04fd306cSNickeau 992*04fd306cSNickeau /** 993*04fd306cSNickeau * Gets the process timeout in seconds (max. runtime). 994*04fd306cSNickeau * 995*04fd306cSNickeau * @return float|null 996*04fd306cSNickeau */ 997*04fd306cSNickeau public function getTimeout() 998*04fd306cSNickeau { 999*04fd306cSNickeau return $this->timeout; 1000*04fd306cSNickeau } 1001*04fd306cSNickeau 1002*04fd306cSNickeau /** 1003*04fd306cSNickeau * Gets the process idle timeout in seconds (max. time since last output). 1004*04fd306cSNickeau * 1005*04fd306cSNickeau * @return float|null 1006*04fd306cSNickeau */ 1007*04fd306cSNickeau public function getIdleTimeout() 1008*04fd306cSNickeau { 1009*04fd306cSNickeau return $this->idleTimeout; 1010*04fd306cSNickeau } 1011*04fd306cSNickeau 1012*04fd306cSNickeau /** 1013*04fd306cSNickeau * Sets the process timeout (max. runtime) in seconds. 1014*04fd306cSNickeau * 1015*04fd306cSNickeau * To disable the timeout, set this value to null. 1016*04fd306cSNickeau * 1017*04fd306cSNickeau * @return $this 1018*04fd306cSNickeau * 1019*04fd306cSNickeau * @throws InvalidArgumentException if the timeout is negative 1020*04fd306cSNickeau */ 1021*04fd306cSNickeau public function setTimeout(?float $timeout) 1022*04fd306cSNickeau { 1023*04fd306cSNickeau $this->timeout = $this->validateTimeout($timeout); 1024*04fd306cSNickeau 1025*04fd306cSNickeau return $this; 1026*04fd306cSNickeau } 1027*04fd306cSNickeau 1028*04fd306cSNickeau /** 1029*04fd306cSNickeau * Sets the process idle timeout (max. time since last output) in seconds. 1030*04fd306cSNickeau * 1031*04fd306cSNickeau * To disable the timeout, set this value to null. 1032*04fd306cSNickeau * 1033*04fd306cSNickeau * @return $this 1034*04fd306cSNickeau * 1035*04fd306cSNickeau * @throws LogicException if the output is disabled 1036*04fd306cSNickeau * @throws InvalidArgumentException if the timeout is negative 1037*04fd306cSNickeau */ 1038*04fd306cSNickeau public function setIdleTimeout(?float $timeout) 1039*04fd306cSNickeau { 1040*04fd306cSNickeau if (null !== $timeout && $this->outputDisabled) { 1041*04fd306cSNickeau throw new LogicException('Idle timeout cannot be set while the output is disabled.'); 1042*04fd306cSNickeau } 1043*04fd306cSNickeau 1044*04fd306cSNickeau $this->idleTimeout = $this->validateTimeout($timeout); 1045*04fd306cSNickeau 1046*04fd306cSNickeau return $this; 1047*04fd306cSNickeau } 1048*04fd306cSNickeau 1049*04fd306cSNickeau /** 1050*04fd306cSNickeau * Enables or disables the TTY mode. 1051*04fd306cSNickeau * 1052*04fd306cSNickeau * @return $this 1053*04fd306cSNickeau * 1054*04fd306cSNickeau * @throws RuntimeException In case the TTY mode is not supported 1055*04fd306cSNickeau */ 1056*04fd306cSNickeau public function setTty(bool $tty) 1057*04fd306cSNickeau { 1058*04fd306cSNickeau if ('\\' === \DIRECTORY_SEPARATOR && $tty) { 1059*04fd306cSNickeau throw new RuntimeException('TTY mode is not supported on Windows platform.'); 1060*04fd306cSNickeau } 1061*04fd306cSNickeau 1062*04fd306cSNickeau if ($tty && !self::isTtySupported()) { 1063*04fd306cSNickeau throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.'); 1064*04fd306cSNickeau } 1065*04fd306cSNickeau 1066*04fd306cSNickeau $this->tty = $tty; 1067*04fd306cSNickeau 1068*04fd306cSNickeau return $this; 1069*04fd306cSNickeau } 1070*04fd306cSNickeau 1071*04fd306cSNickeau /** 1072*04fd306cSNickeau * Checks if the TTY mode is enabled. 1073*04fd306cSNickeau * 1074*04fd306cSNickeau * @return bool 1075*04fd306cSNickeau */ 1076*04fd306cSNickeau public function isTty() 1077*04fd306cSNickeau { 1078*04fd306cSNickeau return $this->tty; 1079*04fd306cSNickeau } 1080*04fd306cSNickeau 1081*04fd306cSNickeau /** 1082*04fd306cSNickeau * Sets PTY mode. 1083*04fd306cSNickeau * 1084*04fd306cSNickeau * @return $this 1085*04fd306cSNickeau */ 1086*04fd306cSNickeau public function setPty(bool $bool) 1087*04fd306cSNickeau { 1088*04fd306cSNickeau $this->pty = $bool; 1089*04fd306cSNickeau 1090*04fd306cSNickeau return $this; 1091*04fd306cSNickeau } 1092*04fd306cSNickeau 1093*04fd306cSNickeau /** 1094*04fd306cSNickeau * Returns PTY state. 1095*04fd306cSNickeau * 1096*04fd306cSNickeau * @return bool 1097*04fd306cSNickeau */ 1098*04fd306cSNickeau public function isPty() 1099*04fd306cSNickeau { 1100*04fd306cSNickeau return $this->pty; 1101*04fd306cSNickeau } 1102*04fd306cSNickeau 1103*04fd306cSNickeau /** 1104*04fd306cSNickeau * Gets the working directory. 1105*04fd306cSNickeau * 1106*04fd306cSNickeau * @return string|null 1107*04fd306cSNickeau */ 1108*04fd306cSNickeau public function getWorkingDirectory() 1109*04fd306cSNickeau { 1110*04fd306cSNickeau if (null === $this->cwd) { 1111*04fd306cSNickeau // getcwd() will return false if any one of the parent directories does not have 1112*04fd306cSNickeau // the readable or search mode set, even if the current directory does 1113*04fd306cSNickeau return getcwd() ?: null; 1114*04fd306cSNickeau } 1115*04fd306cSNickeau 1116*04fd306cSNickeau return $this->cwd; 1117*04fd306cSNickeau } 1118*04fd306cSNickeau 1119*04fd306cSNickeau /** 1120*04fd306cSNickeau * Sets the current working directory. 1121*04fd306cSNickeau * 1122*04fd306cSNickeau * @return $this 1123*04fd306cSNickeau */ 1124*04fd306cSNickeau public function setWorkingDirectory(string $cwd) 1125*04fd306cSNickeau { 1126*04fd306cSNickeau $this->cwd = $cwd; 1127*04fd306cSNickeau 1128*04fd306cSNickeau return $this; 1129*04fd306cSNickeau } 1130*04fd306cSNickeau 1131*04fd306cSNickeau /** 1132*04fd306cSNickeau * Gets the environment variables. 1133*04fd306cSNickeau * 1134*04fd306cSNickeau * @return array 1135*04fd306cSNickeau */ 1136*04fd306cSNickeau public function getEnv() 1137*04fd306cSNickeau { 1138*04fd306cSNickeau return $this->env; 1139*04fd306cSNickeau } 1140*04fd306cSNickeau 1141*04fd306cSNickeau /** 1142*04fd306cSNickeau * Sets the environment variables. 1143*04fd306cSNickeau * 1144*04fd306cSNickeau * @param array<string|\Stringable> $env The new environment variables 1145*04fd306cSNickeau * 1146*04fd306cSNickeau * @return $this 1147*04fd306cSNickeau */ 1148*04fd306cSNickeau public function setEnv(array $env) 1149*04fd306cSNickeau { 1150*04fd306cSNickeau $this->env = $env; 1151*04fd306cSNickeau 1152*04fd306cSNickeau return $this; 1153*04fd306cSNickeau } 1154*04fd306cSNickeau 1155*04fd306cSNickeau /** 1156*04fd306cSNickeau * Gets the Process input. 1157*04fd306cSNickeau * 1158*04fd306cSNickeau * @return resource|string|\Iterator|null 1159*04fd306cSNickeau */ 1160*04fd306cSNickeau public function getInput() 1161*04fd306cSNickeau { 1162*04fd306cSNickeau return $this->input; 1163*04fd306cSNickeau } 1164*04fd306cSNickeau 1165*04fd306cSNickeau /** 1166*04fd306cSNickeau * Sets the input. 1167*04fd306cSNickeau * 1168*04fd306cSNickeau * This content will be passed to the underlying process standard input. 1169*04fd306cSNickeau * 1170*04fd306cSNickeau * @param string|int|float|bool|resource|\Traversable|null $input The content 1171*04fd306cSNickeau * 1172*04fd306cSNickeau * @return $this 1173*04fd306cSNickeau * 1174*04fd306cSNickeau * @throws LogicException In case the process is running 1175*04fd306cSNickeau */ 1176*04fd306cSNickeau public function setInput($input) 1177*04fd306cSNickeau { 1178*04fd306cSNickeau if ($this->isRunning()) { 1179*04fd306cSNickeau throw new LogicException('Input cannot be set while the process is running.'); 1180*04fd306cSNickeau } 1181*04fd306cSNickeau 1182*04fd306cSNickeau $this->input = ProcessUtils::validateInput(__METHOD__, $input); 1183*04fd306cSNickeau 1184*04fd306cSNickeau return $this; 1185*04fd306cSNickeau } 1186*04fd306cSNickeau 1187*04fd306cSNickeau /** 1188*04fd306cSNickeau * Performs a check between the timeout definition and the time the process started. 1189*04fd306cSNickeau * 1190*04fd306cSNickeau * In case you run a background process (with the start method), you should 1191*04fd306cSNickeau * trigger this method regularly to ensure the process timeout 1192*04fd306cSNickeau * 1193*04fd306cSNickeau * @throws ProcessTimedOutException In case the timeout was reached 1194*04fd306cSNickeau */ 1195*04fd306cSNickeau public function checkTimeout() 1196*04fd306cSNickeau { 1197*04fd306cSNickeau if (self::STATUS_STARTED !== $this->status) { 1198*04fd306cSNickeau return; 1199*04fd306cSNickeau } 1200*04fd306cSNickeau 1201*04fd306cSNickeau if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { 1202*04fd306cSNickeau $this->stop(0); 1203*04fd306cSNickeau 1204*04fd306cSNickeau throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL); 1205*04fd306cSNickeau } 1206*04fd306cSNickeau 1207*04fd306cSNickeau if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { 1208*04fd306cSNickeau $this->stop(0); 1209*04fd306cSNickeau 1210*04fd306cSNickeau throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE); 1211*04fd306cSNickeau } 1212*04fd306cSNickeau } 1213*04fd306cSNickeau 1214*04fd306cSNickeau /** 1215*04fd306cSNickeau * @throws LogicException in case process is not started 1216*04fd306cSNickeau */ 1217*04fd306cSNickeau public function getStartTime(): float 1218*04fd306cSNickeau { 1219*04fd306cSNickeau if (!$this->isStarted()) { 1220*04fd306cSNickeau throw new LogicException('Start time is only available after process start.'); 1221*04fd306cSNickeau } 1222*04fd306cSNickeau 1223*04fd306cSNickeau return $this->starttime; 1224*04fd306cSNickeau } 1225*04fd306cSNickeau 1226*04fd306cSNickeau /** 1227*04fd306cSNickeau * Defines options to pass to the underlying proc_open(). 1228*04fd306cSNickeau * 1229*04fd306cSNickeau * @see https://php.net/proc_open for the options supported by PHP. 1230*04fd306cSNickeau * 1231*04fd306cSNickeau * Enabling the "create_new_console" option allows a subprocess to continue 1232*04fd306cSNickeau * to run after the main process exited, on both Windows and *nix 1233*04fd306cSNickeau */ 1234*04fd306cSNickeau public function setOptions(array $options) 1235*04fd306cSNickeau { 1236*04fd306cSNickeau if ($this->isRunning()) { 1237*04fd306cSNickeau throw new RuntimeException('Setting options while the process is running is not possible.'); 1238*04fd306cSNickeau } 1239*04fd306cSNickeau 1240*04fd306cSNickeau $defaultOptions = $this->options; 1241*04fd306cSNickeau $existingOptions = ['blocking_pipes', 'create_process_group', 'create_new_console']; 1242*04fd306cSNickeau 1243*04fd306cSNickeau foreach ($options as $key => $value) { 1244*04fd306cSNickeau if (!\in_array($key, $existingOptions)) { 1245*04fd306cSNickeau $this->options = $defaultOptions; 1246*04fd306cSNickeau throw new LogicException(sprintf('Invalid option "%s" passed to "%s()". Supported options are "%s".', $key, __METHOD__, implode('", "', $existingOptions))); 1247*04fd306cSNickeau } 1248*04fd306cSNickeau $this->options[$key] = $value; 1249*04fd306cSNickeau } 1250*04fd306cSNickeau } 1251*04fd306cSNickeau 1252*04fd306cSNickeau /** 1253*04fd306cSNickeau * Returns whether TTY is supported on the current operating system. 1254*04fd306cSNickeau */ 1255*04fd306cSNickeau public static function isTtySupported(): bool 1256*04fd306cSNickeau { 1257*04fd306cSNickeau static $isTtySupported; 1258*04fd306cSNickeau 1259*04fd306cSNickeau if (null === $isTtySupported) { 1260*04fd306cSNickeau $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes); 1261*04fd306cSNickeau } 1262*04fd306cSNickeau 1263*04fd306cSNickeau return $isTtySupported; 1264*04fd306cSNickeau } 1265*04fd306cSNickeau 1266*04fd306cSNickeau /** 1267*04fd306cSNickeau * Returns whether PTY is supported on the current operating system. 1268*04fd306cSNickeau * 1269*04fd306cSNickeau * @return bool 1270*04fd306cSNickeau */ 1271*04fd306cSNickeau public static function isPtySupported() 1272*04fd306cSNickeau { 1273*04fd306cSNickeau static $result; 1274*04fd306cSNickeau 1275*04fd306cSNickeau if (null !== $result) { 1276*04fd306cSNickeau return $result; 1277*04fd306cSNickeau } 1278*04fd306cSNickeau 1279*04fd306cSNickeau if ('\\' === \DIRECTORY_SEPARATOR) { 1280*04fd306cSNickeau return $result = false; 1281*04fd306cSNickeau } 1282*04fd306cSNickeau 1283*04fd306cSNickeau return $result = (bool) @proc_open('echo 1 >/dev/null', [['pty'], ['pty'], ['pty']], $pipes); 1284*04fd306cSNickeau } 1285*04fd306cSNickeau 1286*04fd306cSNickeau /** 1287*04fd306cSNickeau * Creates the descriptors needed by the proc_open. 1288*04fd306cSNickeau */ 1289*04fd306cSNickeau private function getDescriptors(): array 1290*04fd306cSNickeau { 1291*04fd306cSNickeau if ($this->input instanceof \Iterator) { 1292*04fd306cSNickeau $this->input->rewind(); 1293*04fd306cSNickeau } 1294*04fd306cSNickeau if ('\\' === \DIRECTORY_SEPARATOR) { 1295*04fd306cSNickeau $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $this->hasCallback); 1296*04fd306cSNickeau } else { 1297*04fd306cSNickeau $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->hasCallback); 1298*04fd306cSNickeau } 1299*04fd306cSNickeau 1300*04fd306cSNickeau return $this->processPipes->getDescriptors(); 1301*04fd306cSNickeau } 1302*04fd306cSNickeau 1303*04fd306cSNickeau /** 1304*04fd306cSNickeau * Builds up the callback used by wait(). 1305*04fd306cSNickeau * 1306*04fd306cSNickeau * The callbacks adds all occurred output to the specific buffer and calls 1307*04fd306cSNickeau * the user callback (if present) with the received output. 1308*04fd306cSNickeau * 1309*04fd306cSNickeau * @param callable|null $callback The user defined PHP callback 1310*04fd306cSNickeau * 1311*04fd306cSNickeau * @return \Closure 1312*04fd306cSNickeau */ 1313*04fd306cSNickeau protected function buildCallback(callable $callback = null) 1314*04fd306cSNickeau { 1315*04fd306cSNickeau if ($this->outputDisabled) { 1316*04fd306cSNickeau return function ($type, $data) use ($callback): bool { 1317*04fd306cSNickeau return null !== $callback && $callback($type, $data); 1318*04fd306cSNickeau }; 1319*04fd306cSNickeau } 1320*04fd306cSNickeau 1321*04fd306cSNickeau $out = self::OUT; 1322*04fd306cSNickeau 1323*04fd306cSNickeau return function ($type, $data) use ($callback, $out): bool { 1324*04fd306cSNickeau if ($out == $type) { 1325*04fd306cSNickeau $this->addOutput($data); 1326*04fd306cSNickeau } else { 1327*04fd306cSNickeau $this->addErrorOutput($data); 1328*04fd306cSNickeau } 1329*04fd306cSNickeau 1330*04fd306cSNickeau return null !== $callback && $callback($type, $data); 1331*04fd306cSNickeau }; 1332*04fd306cSNickeau } 1333*04fd306cSNickeau 1334*04fd306cSNickeau /** 1335*04fd306cSNickeau * Updates the status of the process, reads pipes. 1336*04fd306cSNickeau * 1337*04fd306cSNickeau * @param bool $blocking Whether to use a blocking read call 1338*04fd306cSNickeau */ 1339*04fd306cSNickeau protected function updateStatus(bool $blocking) 1340*04fd306cSNickeau { 1341*04fd306cSNickeau if (self::STATUS_STARTED !== $this->status) { 1342*04fd306cSNickeau return; 1343*04fd306cSNickeau } 1344*04fd306cSNickeau 1345*04fd306cSNickeau $this->processInformation = proc_get_status($this->process); 1346*04fd306cSNickeau $running = $this->processInformation['running']; 1347*04fd306cSNickeau 1348*04fd306cSNickeau $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running); 1349*04fd306cSNickeau 1350*04fd306cSNickeau if ($this->fallbackStatus && $this->isSigchildEnabled()) { 1351*04fd306cSNickeau $this->processInformation = $this->fallbackStatus + $this->processInformation; 1352*04fd306cSNickeau } 1353*04fd306cSNickeau 1354*04fd306cSNickeau if (!$running) { 1355*04fd306cSNickeau $this->close(); 1356*04fd306cSNickeau } 1357*04fd306cSNickeau } 1358*04fd306cSNickeau 1359*04fd306cSNickeau /** 1360*04fd306cSNickeau * Returns whether PHP has been compiled with the '--enable-sigchild' option or not. 1361*04fd306cSNickeau * 1362*04fd306cSNickeau * @return bool 1363*04fd306cSNickeau */ 1364*04fd306cSNickeau protected function isSigchildEnabled() 1365*04fd306cSNickeau { 1366*04fd306cSNickeau if (null !== self::$sigchild) { 1367*04fd306cSNickeau return self::$sigchild; 1368*04fd306cSNickeau } 1369*04fd306cSNickeau 1370*04fd306cSNickeau if (!\function_exists('phpinfo')) { 1371*04fd306cSNickeau return self::$sigchild = false; 1372*04fd306cSNickeau } 1373*04fd306cSNickeau 1374*04fd306cSNickeau ob_start(); 1375*04fd306cSNickeau phpinfo(\INFO_GENERAL); 1376*04fd306cSNickeau 1377*04fd306cSNickeau return self::$sigchild = str_contains(ob_get_clean(), '--enable-sigchild'); 1378*04fd306cSNickeau } 1379*04fd306cSNickeau 1380*04fd306cSNickeau /** 1381*04fd306cSNickeau * Reads pipes for the freshest output. 1382*04fd306cSNickeau * 1383*04fd306cSNickeau * @param string $caller The name of the method that needs fresh outputs 1384*04fd306cSNickeau * @param bool $blocking Whether to use blocking calls or not 1385*04fd306cSNickeau * 1386*04fd306cSNickeau * @throws LogicException in case output has been disabled or process is not started 1387*04fd306cSNickeau */ 1388*04fd306cSNickeau private function readPipesForOutput(string $caller, bool $blocking = false) 1389*04fd306cSNickeau { 1390*04fd306cSNickeau if ($this->outputDisabled) { 1391*04fd306cSNickeau throw new LogicException('Output has been disabled.'); 1392*04fd306cSNickeau } 1393*04fd306cSNickeau 1394*04fd306cSNickeau $this->requireProcessIsStarted($caller); 1395*04fd306cSNickeau 1396*04fd306cSNickeau $this->updateStatus($blocking); 1397*04fd306cSNickeau } 1398*04fd306cSNickeau 1399*04fd306cSNickeau /** 1400*04fd306cSNickeau * Validates and returns the filtered timeout. 1401*04fd306cSNickeau * 1402*04fd306cSNickeau * @throws InvalidArgumentException if the given timeout is a negative number 1403*04fd306cSNickeau */ 1404*04fd306cSNickeau private function validateTimeout(?float $timeout): ?float 1405*04fd306cSNickeau { 1406*04fd306cSNickeau $timeout = (float) $timeout; 1407*04fd306cSNickeau 1408*04fd306cSNickeau if (0.0 === $timeout) { 1409*04fd306cSNickeau $timeout = null; 1410*04fd306cSNickeau } elseif ($timeout < 0) { 1411*04fd306cSNickeau throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); 1412*04fd306cSNickeau } 1413*04fd306cSNickeau 1414*04fd306cSNickeau return $timeout; 1415*04fd306cSNickeau } 1416*04fd306cSNickeau 1417*04fd306cSNickeau /** 1418*04fd306cSNickeau * Reads pipes, executes callback. 1419*04fd306cSNickeau * 1420*04fd306cSNickeau * @param bool $blocking Whether to use blocking calls or not 1421*04fd306cSNickeau * @param bool $close Whether to close file handles or not 1422*04fd306cSNickeau */ 1423*04fd306cSNickeau private function readPipes(bool $blocking, bool $close) 1424*04fd306cSNickeau { 1425*04fd306cSNickeau $result = $this->processPipes->readAndWrite($blocking, $close); 1426*04fd306cSNickeau 1427*04fd306cSNickeau $callback = $this->callback; 1428*04fd306cSNickeau foreach ($result as $type => $data) { 1429*04fd306cSNickeau if (3 !== $type) { 1430*04fd306cSNickeau $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data); 1431*04fd306cSNickeau } elseif (!isset($this->fallbackStatus['signaled'])) { 1432*04fd306cSNickeau $this->fallbackStatus['exitcode'] = (int) $data; 1433*04fd306cSNickeau } 1434*04fd306cSNickeau } 1435*04fd306cSNickeau } 1436*04fd306cSNickeau 1437*04fd306cSNickeau /** 1438*04fd306cSNickeau * Closes process resource, closes file handles, sets the exitcode. 1439*04fd306cSNickeau * 1440*04fd306cSNickeau * @return int The exitcode 1441*04fd306cSNickeau */ 1442*04fd306cSNickeau private function close(): int 1443*04fd306cSNickeau { 1444*04fd306cSNickeau $this->processPipes->close(); 1445*04fd306cSNickeau if (\is_resource($this->process)) { 1446*04fd306cSNickeau proc_close($this->process); 1447*04fd306cSNickeau } 1448*04fd306cSNickeau $this->exitcode = $this->processInformation['exitcode']; 1449*04fd306cSNickeau $this->status = self::STATUS_TERMINATED; 1450*04fd306cSNickeau 1451*04fd306cSNickeau if (-1 === $this->exitcode) { 1452*04fd306cSNickeau if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) { 1453*04fd306cSNickeau // if process has been signaled, no exitcode but a valid termsig, apply Unix convention 1454*04fd306cSNickeau $this->exitcode = 128 + $this->processInformation['termsig']; 1455*04fd306cSNickeau } elseif ($this->isSigchildEnabled()) { 1456*04fd306cSNickeau $this->processInformation['signaled'] = true; 1457*04fd306cSNickeau $this->processInformation['termsig'] = -1; 1458*04fd306cSNickeau } 1459*04fd306cSNickeau } 1460*04fd306cSNickeau 1461*04fd306cSNickeau // Free memory from self-reference callback created by buildCallback 1462*04fd306cSNickeau // Doing so in other contexts like __destruct or by garbage collector is ineffective 1463*04fd306cSNickeau // Now pipes are closed, so the callback is no longer necessary 1464*04fd306cSNickeau $this->callback = null; 1465*04fd306cSNickeau 1466*04fd306cSNickeau return $this->exitcode; 1467*04fd306cSNickeau } 1468*04fd306cSNickeau 1469*04fd306cSNickeau /** 1470*04fd306cSNickeau * Resets data related to the latest run of the process. 1471*04fd306cSNickeau */ 1472*04fd306cSNickeau private function resetProcessData() 1473*04fd306cSNickeau { 1474*04fd306cSNickeau $this->starttime = null; 1475*04fd306cSNickeau $this->callback = null; 1476*04fd306cSNickeau $this->exitcode = null; 1477*04fd306cSNickeau $this->fallbackStatus = []; 1478*04fd306cSNickeau $this->processInformation = null; 1479*04fd306cSNickeau $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+'); 1480*04fd306cSNickeau $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+'); 1481*04fd306cSNickeau $this->process = null; 1482*04fd306cSNickeau $this->latestSignal = null; 1483*04fd306cSNickeau $this->status = self::STATUS_READY; 1484*04fd306cSNickeau $this->incrementalOutputOffset = 0; 1485*04fd306cSNickeau $this->incrementalErrorOutputOffset = 0; 1486*04fd306cSNickeau } 1487*04fd306cSNickeau 1488*04fd306cSNickeau /** 1489*04fd306cSNickeau * Sends a POSIX signal to the process. 1490*04fd306cSNickeau * 1491*04fd306cSNickeau * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants) 1492*04fd306cSNickeau * @param bool $throwException Whether to throw exception in case signal failed 1493*04fd306cSNickeau * 1494*04fd306cSNickeau * @throws LogicException In case the process is not running 1495*04fd306cSNickeau * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed 1496*04fd306cSNickeau * @throws RuntimeException In case of failure 1497*04fd306cSNickeau */ 1498*04fd306cSNickeau private function doSignal(int $signal, bool $throwException): bool 1499*04fd306cSNickeau { 1500*04fd306cSNickeau if (null === $pid = $this->getPid()) { 1501*04fd306cSNickeau if ($throwException) { 1502*04fd306cSNickeau throw new LogicException('Cannot send signal on a non running process.'); 1503*04fd306cSNickeau } 1504*04fd306cSNickeau 1505*04fd306cSNickeau return false; 1506*04fd306cSNickeau } 1507*04fd306cSNickeau 1508*04fd306cSNickeau if ('\\' === \DIRECTORY_SEPARATOR) { 1509*04fd306cSNickeau exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode); 1510*04fd306cSNickeau if ($exitCode && $this->isRunning()) { 1511*04fd306cSNickeau if ($throwException) { 1512*04fd306cSNickeau throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output))); 1513*04fd306cSNickeau } 1514*04fd306cSNickeau 1515*04fd306cSNickeau return false; 1516*04fd306cSNickeau } 1517*04fd306cSNickeau } else { 1518*04fd306cSNickeau if (!$this->isSigchildEnabled()) { 1519*04fd306cSNickeau $ok = @proc_terminate($this->process, $signal); 1520*04fd306cSNickeau } elseif (\function_exists('posix_kill')) { 1521*04fd306cSNickeau $ok = @posix_kill($pid, $signal); 1522*04fd306cSNickeau } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) { 1523*04fd306cSNickeau $ok = false === fgets($pipes[2]); 1524*04fd306cSNickeau } 1525*04fd306cSNickeau if (!$ok) { 1526*04fd306cSNickeau if ($throwException) { 1527*04fd306cSNickeau throw new RuntimeException(sprintf('Error while sending signal "%s".', $signal)); 1528*04fd306cSNickeau } 1529*04fd306cSNickeau 1530*04fd306cSNickeau return false; 1531*04fd306cSNickeau } 1532*04fd306cSNickeau } 1533*04fd306cSNickeau 1534*04fd306cSNickeau $this->latestSignal = $signal; 1535*04fd306cSNickeau $this->fallbackStatus['signaled'] = true; 1536*04fd306cSNickeau $this->fallbackStatus['exitcode'] = -1; 1537*04fd306cSNickeau $this->fallbackStatus['termsig'] = $this->latestSignal; 1538*04fd306cSNickeau 1539*04fd306cSNickeau return true; 1540*04fd306cSNickeau } 1541*04fd306cSNickeau 1542*04fd306cSNickeau private function prepareWindowsCommandLine(string $cmd, array &$env): string 1543*04fd306cSNickeau { 1544*04fd306cSNickeau $uid = uniqid('', true); 1545*04fd306cSNickeau $varCount = 0; 1546*04fd306cSNickeau $varCache = []; 1547*04fd306cSNickeau $cmd = preg_replace_callback( 1548*04fd306cSNickeau '/"(?:( 1549*04fd306cSNickeau [^"%!^]*+ 1550*04fd306cSNickeau (?: 1551*04fd306cSNickeau (?: !LF! | "(?:\^[%!^])?+" ) 1552*04fd306cSNickeau [^"%!^]*+ 1553*04fd306cSNickeau )++ 1554*04fd306cSNickeau ) | [^"]*+ )"/x', 1555*04fd306cSNickeau function ($m) use (&$env, &$varCache, &$varCount, $uid) { 1556*04fd306cSNickeau if (!isset($m[1])) { 1557*04fd306cSNickeau return $m[0]; 1558*04fd306cSNickeau } 1559*04fd306cSNickeau if (isset($varCache[$m[0]])) { 1560*04fd306cSNickeau return $varCache[$m[0]]; 1561*04fd306cSNickeau } 1562*04fd306cSNickeau if (str_contains($value = $m[1], "\0")) { 1563*04fd306cSNickeau $value = str_replace("\0", '?', $value); 1564*04fd306cSNickeau } 1565*04fd306cSNickeau if (false === strpbrk($value, "\"%!\n")) { 1566*04fd306cSNickeau return '"'.$value.'"'; 1567*04fd306cSNickeau } 1568*04fd306cSNickeau 1569*04fd306cSNickeau $value = str_replace(['!LF!', '"^!"', '"^%"', '"^^"', '""'], ["\n", '!', '%', '^', '"'], $value); 1570*04fd306cSNickeau $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"'; 1571*04fd306cSNickeau $var = $uid.++$varCount; 1572*04fd306cSNickeau 1573*04fd306cSNickeau $env[$var] = $value; 1574*04fd306cSNickeau 1575*04fd306cSNickeau return $varCache[$m[0]] = '!'.$var.'!'; 1576*04fd306cSNickeau }, 1577*04fd306cSNickeau $cmd 1578*04fd306cSNickeau ); 1579*04fd306cSNickeau 1580*04fd306cSNickeau $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; 1581*04fd306cSNickeau foreach ($this->processPipes->getFiles() as $offset => $filename) { 1582*04fd306cSNickeau $cmd .= ' '.$offset.'>"'.$filename.'"'; 1583*04fd306cSNickeau } 1584*04fd306cSNickeau 1585*04fd306cSNickeau return $cmd; 1586*04fd306cSNickeau } 1587*04fd306cSNickeau 1588*04fd306cSNickeau /** 1589*04fd306cSNickeau * Ensures the process is running or terminated, throws a LogicException if the process has a not started. 1590*04fd306cSNickeau * 1591*04fd306cSNickeau * @throws LogicException if the process has not run 1592*04fd306cSNickeau */ 1593*04fd306cSNickeau private function requireProcessIsStarted(string $functionName) 1594*04fd306cSNickeau { 1595*04fd306cSNickeau if (!$this->isStarted()) { 1596*04fd306cSNickeau throw new LogicException(sprintf('Process must be started before calling "%s()".', $functionName)); 1597*04fd306cSNickeau } 1598*04fd306cSNickeau } 1599*04fd306cSNickeau 1600*04fd306cSNickeau /** 1601*04fd306cSNickeau * Ensures the process is terminated, throws a LogicException if the process has a status different than "terminated". 1602*04fd306cSNickeau * 1603*04fd306cSNickeau * @throws LogicException if the process is not yet terminated 1604*04fd306cSNickeau */ 1605*04fd306cSNickeau private function requireProcessIsTerminated(string $functionName) 1606*04fd306cSNickeau { 1607*04fd306cSNickeau if (!$this->isTerminated()) { 1608*04fd306cSNickeau throw new LogicException(sprintf('Process must be terminated before calling "%s()".', $functionName)); 1609*04fd306cSNickeau } 1610*04fd306cSNickeau } 1611*04fd306cSNickeau 1612*04fd306cSNickeau /** 1613*04fd306cSNickeau * Escapes a string to be used as a shell argument. 1614*04fd306cSNickeau */ 1615*04fd306cSNickeau private function escapeArgument(?string $argument): string 1616*04fd306cSNickeau { 1617*04fd306cSNickeau if ('' === $argument || null === $argument) { 1618*04fd306cSNickeau return '""'; 1619*04fd306cSNickeau } 1620*04fd306cSNickeau if ('\\' !== \DIRECTORY_SEPARATOR) { 1621*04fd306cSNickeau return "'".str_replace("'", "'\\''", $argument)."'"; 1622*04fd306cSNickeau } 1623*04fd306cSNickeau if (str_contains($argument, "\0")) { 1624*04fd306cSNickeau $argument = str_replace("\0", '?', $argument); 1625*04fd306cSNickeau } 1626*04fd306cSNickeau if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) { 1627*04fd306cSNickeau return $argument; 1628*04fd306cSNickeau } 1629*04fd306cSNickeau $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument); 1630*04fd306cSNickeau 1631*04fd306cSNickeau return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"'; 1632*04fd306cSNickeau } 1633*04fd306cSNickeau 1634*04fd306cSNickeau private function replacePlaceholders(string $commandline, array $env) 1635*04fd306cSNickeau { 1636*04fd306cSNickeau return preg_replace_callback('/"\$\{:([_a-zA-Z]++[_a-zA-Z0-9]*+)\}"/', function ($matches) use ($commandline, $env) { 1637*04fd306cSNickeau if (!isset($env[$matches[1]]) || false === $env[$matches[1]]) { 1638*04fd306cSNickeau throw new InvalidArgumentException(sprintf('Command line is missing a value for parameter "%s": ', $matches[1]).$commandline); 1639*04fd306cSNickeau } 1640*04fd306cSNickeau 1641*04fd306cSNickeau return $this->escapeArgument($env[$matches[1]]); 1642*04fd306cSNickeau }, $commandline); 1643*04fd306cSNickeau } 1644*04fd306cSNickeau 1645*04fd306cSNickeau private function getDefaultEnv(): array 1646*04fd306cSNickeau { 1647*04fd306cSNickeau $env = getenv(); 1648*04fd306cSNickeau $env = ('\\' === \DIRECTORY_SEPARATOR ? array_intersect_ukey($env, $_SERVER, 'strcasecmp') : array_intersect_key($env, $_SERVER)) ?: $env; 1649*04fd306cSNickeau 1650*04fd306cSNickeau return $_ENV + ('\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($env, $_ENV, 'strcasecmp') : $env); 1651*04fd306cSNickeau } 1652*04fd306cSNickeau} 1653