xref: /plugin/combo/vendor/symfony/process/Process.php (revision 83c6863253ba0b92605aa8dceca974358d439aaa)
104fd306cSNickeau<?php
204fd306cSNickeau
304fd306cSNickeau/*
404fd306cSNickeau * This file is part of the Symfony package.
504fd306cSNickeau *
604fd306cSNickeau * (c) Fabien Potencier <fabien@symfony.com>
704fd306cSNickeau *
804fd306cSNickeau * For the full copyright and license information, please view the LICENSE
904fd306cSNickeau * file that was distributed with this source code.
1004fd306cSNickeau */
1104fd306cSNickeau
1204fd306cSNickeaunamespace Symfony\Component\Process;
1304fd306cSNickeau
1404fd306cSNickeauuse Symfony\Component\Process\Exception\InvalidArgumentException;
1504fd306cSNickeauuse Symfony\Component\Process\Exception\LogicException;
1604fd306cSNickeauuse Symfony\Component\Process\Exception\ProcessFailedException;
1704fd306cSNickeauuse Symfony\Component\Process\Exception\ProcessSignaledException;
1804fd306cSNickeauuse Symfony\Component\Process\Exception\ProcessTimedOutException;
1904fd306cSNickeauuse Symfony\Component\Process\Exception\RuntimeException;
2004fd306cSNickeauuse Symfony\Component\Process\Pipes\PipesInterface;
2104fd306cSNickeauuse Symfony\Component\Process\Pipes\UnixPipes;
2204fd306cSNickeauuse Symfony\Component\Process\Pipes\WindowsPipes;
2304fd306cSNickeau
2404fd306cSNickeau/**
2504fd306cSNickeau * Process is a thin wrapper around proc_* functions to easily
2604fd306cSNickeau * start independent PHP processes.
2704fd306cSNickeau *
2804fd306cSNickeau * @author Fabien Potencier <fabien@symfony.com>
2904fd306cSNickeau * @author Romain Neutron <imprec@gmail.com>
3004fd306cSNickeau *
3104fd306cSNickeau * @implements \IteratorAggregate<string, string>
3204fd306cSNickeau */
3304fd306cSNickeauclass Process implements \IteratorAggregate
3404fd306cSNickeau{
3504fd306cSNickeau    public const ERR = 'err';
3604fd306cSNickeau    public const OUT = 'out';
3704fd306cSNickeau
3804fd306cSNickeau    public const STATUS_READY = 'ready';
3904fd306cSNickeau    public const STATUS_STARTED = 'started';
4004fd306cSNickeau    public const STATUS_TERMINATED = 'terminated';
4104fd306cSNickeau
4204fd306cSNickeau    public const STDIN = 0;
4304fd306cSNickeau    public const STDOUT = 1;
4404fd306cSNickeau    public const STDERR = 2;
4504fd306cSNickeau
4604fd306cSNickeau    // Timeout Precision in seconds.
4704fd306cSNickeau    public const TIMEOUT_PRECISION = 0.2;
4804fd306cSNickeau
4904fd306cSNickeau    public const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking
5004fd306cSNickeau    public const ITER_KEEP_OUTPUT = 2;  // By default, outputs are cleared while iterating, use this flag to keep them in memory
5104fd306cSNickeau    public const ITER_SKIP_OUT = 4;     // Use this flag to skip STDOUT while iterating
5204fd306cSNickeau    public const ITER_SKIP_ERR = 8;     // Use this flag to skip STDERR while iterating
5304fd306cSNickeau
5404fd306cSNickeau    private $callback;
5504fd306cSNickeau    private $hasCallback = false;
5604fd306cSNickeau    private $commandline;
5704fd306cSNickeau    private $cwd;
5804fd306cSNickeau    private $env = [];
5904fd306cSNickeau    private $input;
6004fd306cSNickeau    private $starttime;
6104fd306cSNickeau    private $lastOutputTime;
6204fd306cSNickeau    private $timeout;
6304fd306cSNickeau    private $idleTimeout;
6404fd306cSNickeau    private $exitcode;
6504fd306cSNickeau    private $fallbackStatus = [];
6604fd306cSNickeau    private $processInformation;
6704fd306cSNickeau    private $outputDisabled = false;
6804fd306cSNickeau    private $stdout;
6904fd306cSNickeau    private $stderr;
7004fd306cSNickeau    private $process;
7104fd306cSNickeau    private $status = self::STATUS_READY;
7204fd306cSNickeau    private $incrementalOutputOffset = 0;
7304fd306cSNickeau    private $incrementalErrorOutputOffset = 0;
7404fd306cSNickeau    private $tty = false;
7504fd306cSNickeau    private $pty;
7604fd306cSNickeau    private $options = ['suppress_errors' => true, 'bypass_shell' => true];
7704fd306cSNickeau
7804fd306cSNickeau    private $useFileHandles = false;
7904fd306cSNickeau    /** @var PipesInterface */
8004fd306cSNickeau    private $processPipes;
8104fd306cSNickeau
8204fd306cSNickeau    private $latestSignal;
83*83c68632SNico    private $cachedExitCode;
8404fd306cSNickeau
8504fd306cSNickeau    private static $sigchild;
8604fd306cSNickeau
8704fd306cSNickeau    /**
8804fd306cSNickeau     * Exit codes translation table.
8904fd306cSNickeau     *
9004fd306cSNickeau     * User-defined errors must use exit codes in the 64-113 range.
9104fd306cSNickeau     */
9204fd306cSNickeau    public static $exitCodes = [
9304fd306cSNickeau        0 => 'OK',
9404fd306cSNickeau        1 => 'General error',
9504fd306cSNickeau        2 => 'Misuse of shell builtins',
9604fd306cSNickeau
9704fd306cSNickeau        126 => 'Invoked command cannot execute',
9804fd306cSNickeau        127 => 'Command not found',
9904fd306cSNickeau        128 => 'Invalid exit argument',
10004fd306cSNickeau
10104fd306cSNickeau        // signals
10204fd306cSNickeau        129 => 'Hangup',
10304fd306cSNickeau        130 => 'Interrupt',
10404fd306cSNickeau        131 => 'Quit and dump core',
10504fd306cSNickeau        132 => 'Illegal instruction',
10604fd306cSNickeau        133 => 'Trace/breakpoint trap',
10704fd306cSNickeau        134 => 'Process aborted',
10804fd306cSNickeau        135 => 'Bus error: "access to undefined portion of memory object"',
10904fd306cSNickeau        136 => 'Floating point exception: "erroneous arithmetic operation"',
11004fd306cSNickeau        137 => 'Kill (terminate immediately)',
11104fd306cSNickeau        138 => 'User-defined 1',
11204fd306cSNickeau        139 => 'Segmentation violation',
11304fd306cSNickeau        140 => 'User-defined 2',
11404fd306cSNickeau        141 => 'Write to pipe with no one reading',
11504fd306cSNickeau        142 => 'Signal raised by alarm',
11604fd306cSNickeau        143 => 'Termination (request to terminate)',
11704fd306cSNickeau        // 144 - not defined
11804fd306cSNickeau        145 => 'Child process terminated, stopped (or continued*)',
11904fd306cSNickeau        146 => 'Continue if stopped',
12004fd306cSNickeau        147 => 'Stop executing temporarily',
12104fd306cSNickeau        148 => 'Terminal stop signal',
12204fd306cSNickeau        149 => 'Background process attempting to read from tty ("in")',
12304fd306cSNickeau        150 => 'Background process attempting to write to tty ("out")',
12404fd306cSNickeau        151 => 'Urgent data available on socket',
12504fd306cSNickeau        152 => 'CPU time limit exceeded',
12604fd306cSNickeau        153 => 'File size limit exceeded',
12704fd306cSNickeau        154 => 'Signal raised by timer counting virtual time: "virtual timer expired"',
12804fd306cSNickeau        155 => 'Profiling timer expired',
12904fd306cSNickeau        // 156 - not defined
13004fd306cSNickeau        157 => 'Pollable event',
13104fd306cSNickeau        // 158 - not defined
13204fd306cSNickeau        159 => 'Bad syscall',
13304fd306cSNickeau    ];
13404fd306cSNickeau
13504fd306cSNickeau    /**
13604fd306cSNickeau     * @param array          $command The command to run and its arguments listed as separate entries
13704fd306cSNickeau     * @param string|null    $cwd     The working directory or null to use the working dir of the current PHP process
13804fd306cSNickeau     * @param array|null     $env     The environment variables or null to use the same environment as the current PHP process
13904fd306cSNickeau     * @param mixed          $input   The input as stream resource, scalar or \Traversable, or null for no input
14004fd306cSNickeau     * @param int|float|null $timeout The timeout in seconds or null to disable
14104fd306cSNickeau     *
14204fd306cSNickeau     * @throws LogicException When proc_open is not installed
14304fd306cSNickeau     */
144*83c68632SNico    public function __construct(array $command, ?string $cwd = null, ?array $env = null, $input = null, ?float $timeout = 60)
14504fd306cSNickeau    {
14604fd306cSNickeau        if (!\function_exists('proc_open')) {
14704fd306cSNickeau            throw new LogicException('The Process class relies on proc_open, which is not available on your PHP installation.');
14804fd306cSNickeau        }
14904fd306cSNickeau
15004fd306cSNickeau        $this->commandline = $command;
15104fd306cSNickeau        $this->cwd = $cwd;
15204fd306cSNickeau
15304fd306cSNickeau        // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started
15404fd306cSNickeau        // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected
15504fd306cSNickeau        // @see : https://bugs.php.net/51800
15604fd306cSNickeau        // @see : https://bugs.php.net/50524
15704fd306cSNickeau        if (null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR)) {
15804fd306cSNickeau            $this->cwd = getcwd();
15904fd306cSNickeau        }
16004fd306cSNickeau        if (null !== $env) {
16104fd306cSNickeau            $this->setEnv($env);
16204fd306cSNickeau        }
16304fd306cSNickeau
16404fd306cSNickeau        $this->setInput($input);
16504fd306cSNickeau        $this->setTimeout($timeout);
16604fd306cSNickeau        $this->useFileHandles = '\\' === \DIRECTORY_SEPARATOR;
16704fd306cSNickeau        $this->pty = false;
16804fd306cSNickeau    }
16904fd306cSNickeau
17004fd306cSNickeau    /**
17104fd306cSNickeau     * Creates a Process instance as a command-line to be run in a shell wrapper.
17204fd306cSNickeau     *
17304fd306cSNickeau     * Command-lines are parsed by the shell of your OS (/bin/sh on Unix-like, cmd.exe on Windows.)
17404fd306cSNickeau     * This allows using e.g. pipes or conditional execution. In this mode, signals are sent to the
17504fd306cSNickeau     * shell wrapper and not to your commands.
17604fd306cSNickeau     *
17704fd306cSNickeau     * In order to inject dynamic values into command-lines, we strongly recommend using placeholders.
17804fd306cSNickeau     * This will save escaping values, which is not portable nor secure anyway:
17904fd306cSNickeau     *
18004fd306cSNickeau     *   $process = Process::fromShellCommandline('my_command "${:MY_VAR}"');
18104fd306cSNickeau     *   $process->run(null, ['MY_VAR' => $theValue]);
18204fd306cSNickeau     *
18304fd306cSNickeau     * @param string         $command The command line to pass to the shell of the OS
18404fd306cSNickeau     * @param string|null    $cwd     The working directory or null to use the working dir of the current PHP process
18504fd306cSNickeau     * @param array|null     $env     The environment variables or null to use the same environment as the current PHP process
18604fd306cSNickeau     * @param mixed          $input   The input as stream resource, scalar or \Traversable, or null for no input
18704fd306cSNickeau     * @param int|float|null $timeout The timeout in seconds or null to disable
18804fd306cSNickeau     *
18904fd306cSNickeau     * @return static
19004fd306cSNickeau     *
19104fd306cSNickeau     * @throws LogicException When proc_open is not installed
19204fd306cSNickeau     */
193*83c68632SNico    public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, $input = null, ?float $timeout = 60)
19404fd306cSNickeau    {
19504fd306cSNickeau        $process = new static([], $cwd, $env, $input, $timeout);
19604fd306cSNickeau        $process->commandline = $command;
19704fd306cSNickeau
19804fd306cSNickeau        return $process;
19904fd306cSNickeau    }
20004fd306cSNickeau
20104fd306cSNickeau    /**
20204fd306cSNickeau     * @return array
20304fd306cSNickeau     */
20404fd306cSNickeau    public function __sleep()
20504fd306cSNickeau    {
20604fd306cSNickeau        throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
20704fd306cSNickeau    }
20804fd306cSNickeau
20904fd306cSNickeau    public function __wakeup()
21004fd306cSNickeau    {
21104fd306cSNickeau        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
21204fd306cSNickeau    }
21304fd306cSNickeau
21404fd306cSNickeau    public function __destruct()
21504fd306cSNickeau    {
21604fd306cSNickeau        if ($this->options['create_new_console'] ?? false) {
21704fd306cSNickeau            $this->processPipes->close();
21804fd306cSNickeau        } else {
21904fd306cSNickeau            $this->stop(0);
22004fd306cSNickeau        }
22104fd306cSNickeau    }
22204fd306cSNickeau
22304fd306cSNickeau    public function __clone()
22404fd306cSNickeau    {
22504fd306cSNickeau        $this->resetProcessData();
22604fd306cSNickeau    }
22704fd306cSNickeau
22804fd306cSNickeau    /**
22904fd306cSNickeau     * Runs the process.
23004fd306cSNickeau     *
23104fd306cSNickeau     * The callback receives the type of output (out or err) and
23204fd306cSNickeau     * some bytes from the output in real-time. It allows to have feedback
23304fd306cSNickeau     * from the independent process during execution.
23404fd306cSNickeau     *
23504fd306cSNickeau     * The STDOUT and STDERR are also available after the process is finished
23604fd306cSNickeau     * via the getOutput() and getErrorOutput() methods.
23704fd306cSNickeau     *
23804fd306cSNickeau     * @param callable|null $callback A PHP callback to run whenever there is some
23904fd306cSNickeau     *                                output available on STDOUT or STDERR
24004fd306cSNickeau     *
24104fd306cSNickeau     * @return int The exit status code
24204fd306cSNickeau     *
24304fd306cSNickeau     * @throws RuntimeException         When process can't be launched
24404fd306cSNickeau     * @throws RuntimeException         When process is already running
24504fd306cSNickeau     * @throws ProcessTimedOutException When process timed out
24604fd306cSNickeau     * @throws ProcessSignaledException When process stopped after receiving signal
24704fd306cSNickeau     * @throws LogicException           In case a callback is provided and output has been disabled
24804fd306cSNickeau     *
24904fd306cSNickeau     * @final
25004fd306cSNickeau     */
251*83c68632SNico    public function run(?callable $callback = null, array $env = []): int
25204fd306cSNickeau    {
25304fd306cSNickeau        $this->start($callback, $env);
25404fd306cSNickeau
25504fd306cSNickeau        return $this->wait();
25604fd306cSNickeau    }
25704fd306cSNickeau
25804fd306cSNickeau    /**
25904fd306cSNickeau     * Runs the process.
26004fd306cSNickeau     *
26104fd306cSNickeau     * This is identical to run() except that an exception is thrown if the process
26204fd306cSNickeau     * exits with a non-zero exit code.
26304fd306cSNickeau     *
26404fd306cSNickeau     * @return $this
26504fd306cSNickeau     *
26604fd306cSNickeau     * @throws ProcessFailedException if the process didn't terminate successfully
26704fd306cSNickeau     *
26804fd306cSNickeau     * @final
26904fd306cSNickeau     */
270*83c68632SNico    public function mustRun(?callable $callback = null, array $env = []): self
27104fd306cSNickeau    {
27204fd306cSNickeau        if (0 !== $this->run($callback, $env)) {
27304fd306cSNickeau            throw new ProcessFailedException($this);
27404fd306cSNickeau        }
27504fd306cSNickeau
27604fd306cSNickeau        return $this;
27704fd306cSNickeau    }
27804fd306cSNickeau
27904fd306cSNickeau    /**
28004fd306cSNickeau     * Starts the process and returns after writing the input to STDIN.
28104fd306cSNickeau     *
28204fd306cSNickeau     * This method blocks until all STDIN data is sent to the process then it
28304fd306cSNickeau     * returns while the process runs in the background.
28404fd306cSNickeau     *
28504fd306cSNickeau     * The termination of the process can be awaited with wait().
28604fd306cSNickeau     *
28704fd306cSNickeau     * The callback receives the type of output (out or err) and some bytes from
28804fd306cSNickeau     * the output in real-time while writing the standard input to the process.
28904fd306cSNickeau     * It allows to have feedback from the independent process during execution.
29004fd306cSNickeau     *
29104fd306cSNickeau     * @param callable|null $callback A PHP callback to run whenever there is some
29204fd306cSNickeau     *                                output available on STDOUT or STDERR
29304fd306cSNickeau     *
29404fd306cSNickeau     * @throws RuntimeException When process can't be launched
29504fd306cSNickeau     * @throws RuntimeException When process is already running
29604fd306cSNickeau     * @throws LogicException   In case a callback is provided and output has been disabled
29704fd306cSNickeau     */
298*83c68632SNico    public function start(?callable $callback = null, array $env = [])
29904fd306cSNickeau    {
30004fd306cSNickeau        if ($this->isRunning()) {
30104fd306cSNickeau            throw new RuntimeException('Process is already running.');
30204fd306cSNickeau        }
30304fd306cSNickeau
30404fd306cSNickeau        $this->resetProcessData();
30504fd306cSNickeau        $this->starttime = $this->lastOutputTime = microtime(true);
30604fd306cSNickeau        $this->callback = $this->buildCallback($callback);
30704fd306cSNickeau        $this->hasCallback = null !== $callback;
30804fd306cSNickeau        $descriptors = $this->getDescriptors();
30904fd306cSNickeau
31004fd306cSNickeau        if ($this->env) {
31104fd306cSNickeau            $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->env, $env, 'strcasecmp') : $this->env;
31204fd306cSNickeau        }
31304fd306cSNickeau
31404fd306cSNickeau        $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->getDefaultEnv(), $env, 'strcasecmp') : $this->getDefaultEnv();
31504fd306cSNickeau
31604fd306cSNickeau        if (\is_array($commandline = $this->commandline)) {
31704fd306cSNickeau            $commandline = implode(' ', array_map([$this, 'escapeArgument'], $commandline));
31804fd306cSNickeau
31904fd306cSNickeau            if ('\\' !== \DIRECTORY_SEPARATOR) {
32004fd306cSNickeau                // exec is mandatory to deal with sending a signal to the process
32104fd306cSNickeau                $commandline = 'exec '.$commandline;
32204fd306cSNickeau            }
32304fd306cSNickeau        } else {
32404fd306cSNickeau            $commandline = $this->replacePlaceholders($commandline, $env);
32504fd306cSNickeau        }
32604fd306cSNickeau
32704fd306cSNickeau        if ('\\' === \DIRECTORY_SEPARATOR) {
32804fd306cSNickeau            $commandline = $this->prepareWindowsCommandLine($commandline, $env);
32904fd306cSNickeau        } elseif (!$this->useFileHandles && $this->isSigchildEnabled()) {
33004fd306cSNickeau            // last exit code is output on the fourth pipe and caught to work around --enable-sigchild
33104fd306cSNickeau            $descriptors[3] = ['pipe', 'w'];
33204fd306cSNickeau
33304fd306cSNickeau            // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input
33404fd306cSNickeau            $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;';
335*83c68632SNico            $commandline .= 'pid=$!; echo $pid >&3; wait $pid 2>/dev/null; code=$?; echo $code >&3; exit $code';
33604fd306cSNickeau
33704fd306cSNickeau            // Workaround for the bug, when PTS functionality is enabled.
33804fd306cSNickeau            // @see : https://bugs.php.net/69442
33904fd306cSNickeau            $ptsWorkaround = fopen(__FILE__, 'r');
34004fd306cSNickeau        }
34104fd306cSNickeau
34204fd306cSNickeau        $envPairs = [];
34304fd306cSNickeau        foreach ($env as $k => $v) {
34404fd306cSNickeau            if (false !== $v && false === \in_array($k, ['argc', 'argv', 'ARGC', 'ARGV'], true)) {
34504fd306cSNickeau                $envPairs[] = $k.'='.$v;
34604fd306cSNickeau            }
34704fd306cSNickeau        }
34804fd306cSNickeau
34904fd306cSNickeau        if (!is_dir($this->cwd)) {
35004fd306cSNickeau            throw new RuntimeException(sprintf('The provided cwd "%s" does not exist.', $this->cwd));
35104fd306cSNickeau        }
35204fd306cSNickeau
35304fd306cSNickeau        $this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options);
35404fd306cSNickeau
35504fd306cSNickeau        if (!\is_resource($this->process)) {
35604fd306cSNickeau            throw new RuntimeException('Unable to launch a new process.');
35704fd306cSNickeau        }
35804fd306cSNickeau        $this->status = self::STATUS_STARTED;
35904fd306cSNickeau
36004fd306cSNickeau        if (isset($descriptors[3])) {
36104fd306cSNickeau            $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]);
36204fd306cSNickeau        }
36304fd306cSNickeau
36404fd306cSNickeau        if ($this->tty) {
36504fd306cSNickeau            return;
36604fd306cSNickeau        }
36704fd306cSNickeau
36804fd306cSNickeau        $this->updateStatus(false);
36904fd306cSNickeau        $this->checkTimeout();
37004fd306cSNickeau    }
37104fd306cSNickeau
37204fd306cSNickeau    /**
37304fd306cSNickeau     * Restarts the process.
37404fd306cSNickeau     *
37504fd306cSNickeau     * Be warned that the process is cloned before being started.
37604fd306cSNickeau     *
37704fd306cSNickeau     * @param callable|null $callback A PHP callback to run whenever there is some
37804fd306cSNickeau     *                                output available on STDOUT or STDERR
37904fd306cSNickeau     *
38004fd306cSNickeau     * @return static
38104fd306cSNickeau     *
38204fd306cSNickeau     * @throws RuntimeException When process can't be launched
38304fd306cSNickeau     * @throws RuntimeException When process is already running
38404fd306cSNickeau     *
38504fd306cSNickeau     * @see start()
38604fd306cSNickeau     *
38704fd306cSNickeau     * @final
38804fd306cSNickeau     */
389*83c68632SNico    public function restart(?callable $callback = null, array $env = []): self
39004fd306cSNickeau    {
39104fd306cSNickeau        if ($this->isRunning()) {
39204fd306cSNickeau            throw new RuntimeException('Process is already running.');
39304fd306cSNickeau        }
39404fd306cSNickeau
39504fd306cSNickeau        $process = clone $this;
39604fd306cSNickeau        $process->start($callback, $env);
39704fd306cSNickeau
39804fd306cSNickeau        return $process;
39904fd306cSNickeau    }
40004fd306cSNickeau
40104fd306cSNickeau    /**
40204fd306cSNickeau     * Waits for the process to terminate.
40304fd306cSNickeau     *
40404fd306cSNickeau     * The callback receives the type of output (out or err) and some bytes
40504fd306cSNickeau     * from the output in real-time while writing the standard input to the process.
40604fd306cSNickeau     * It allows to have feedback from the independent process during execution.
40704fd306cSNickeau     *
40804fd306cSNickeau     * @param callable|null $callback A valid PHP callback
40904fd306cSNickeau     *
41004fd306cSNickeau     * @return int The exitcode of the process
41104fd306cSNickeau     *
41204fd306cSNickeau     * @throws ProcessTimedOutException When process timed out
41304fd306cSNickeau     * @throws ProcessSignaledException When process stopped after receiving signal
41404fd306cSNickeau     * @throws LogicException           When process is not yet started
41504fd306cSNickeau     */
416*83c68632SNico    public function wait(?callable $callback = null)
41704fd306cSNickeau    {
41804fd306cSNickeau        $this->requireProcessIsStarted(__FUNCTION__);
41904fd306cSNickeau
42004fd306cSNickeau        $this->updateStatus(false);
42104fd306cSNickeau
42204fd306cSNickeau        if (null !== $callback) {
42304fd306cSNickeau            if (!$this->processPipes->haveReadSupport()) {
42404fd306cSNickeau                $this->stop(0);
42504fd306cSNickeau                throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait".');
42604fd306cSNickeau            }
42704fd306cSNickeau            $this->callback = $this->buildCallback($callback);
42804fd306cSNickeau        }
42904fd306cSNickeau
43004fd306cSNickeau        do {
43104fd306cSNickeau            $this->checkTimeout();
432*83c68632SNico            $running = $this->isRunning() && ('\\' === \DIRECTORY_SEPARATOR || $this->processPipes->areOpen());
43304fd306cSNickeau            $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
43404fd306cSNickeau        } while ($running);
43504fd306cSNickeau
43604fd306cSNickeau        while ($this->isRunning()) {
43704fd306cSNickeau            $this->checkTimeout();
43804fd306cSNickeau            usleep(1000);
43904fd306cSNickeau        }
44004fd306cSNickeau
44104fd306cSNickeau        if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) {
44204fd306cSNickeau            throw new ProcessSignaledException($this);
44304fd306cSNickeau        }
44404fd306cSNickeau
44504fd306cSNickeau        return $this->exitcode;
44604fd306cSNickeau    }
44704fd306cSNickeau
44804fd306cSNickeau    /**
44904fd306cSNickeau     * Waits until the callback returns true.
45004fd306cSNickeau     *
45104fd306cSNickeau     * The callback receives the type of output (out or err) and some bytes
45204fd306cSNickeau     * from the output in real-time while writing the standard input to the process.
45304fd306cSNickeau     * It allows to have feedback from the independent process during execution.
45404fd306cSNickeau     *
45504fd306cSNickeau     * @throws RuntimeException         When process timed out
45604fd306cSNickeau     * @throws LogicException           When process is not yet started
45704fd306cSNickeau     * @throws ProcessTimedOutException In case the timeout was reached
45804fd306cSNickeau     */
45904fd306cSNickeau    public function waitUntil(callable $callback): bool
46004fd306cSNickeau    {
46104fd306cSNickeau        $this->requireProcessIsStarted(__FUNCTION__);
46204fd306cSNickeau        $this->updateStatus(false);
46304fd306cSNickeau
46404fd306cSNickeau        if (!$this->processPipes->haveReadSupport()) {
46504fd306cSNickeau            $this->stop(0);
46604fd306cSNickeau            throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::waitUntil".');
46704fd306cSNickeau        }
46804fd306cSNickeau        $callback = $this->buildCallback($callback);
46904fd306cSNickeau
47004fd306cSNickeau        $ready = false;
47104fd306cSNickeau        while (true) {
47204fd306cSNickeau            $this->checkTimeout();
47304fd306cSNickeau            $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
47404fd306cSNickeau            $output = $this->processPipes->readAndWrite($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
47504fd306cSNickeau
47604fd306cSNickeau            foreach ($output as $type => $data) {
47704fd306cSNickeau                if (3 !== $type) {
47804fd306cSNickeau                    $ready = $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data) || $ready;
47904fd306cSNickeau                } elseif (!isset($this->fallbackStatus['signaled'])) {
48004fd306cSNickeau                    $this->fallbackStatus['exitcode'] = (int) $data;
48104fd306cSNickeau                }
48204fd306cSNickeau            }
48304fd306cSNickeau            if ($ready) {
48404fd306cSNickeau                return true;
48504fd306cSNickeau            }
48604fd306cSNickeau            if (!$running) {
48704fd306cSNickeau                return false;
48804fd306cSNickeau            }
48904fd306cSNickeau
49004fd306cSNickeau            usleep(1000);
49104fd306cSNickeau        }
49204fd306cSNickeau    }
49304fd306cSNickeau
49404fd306cSNickeau    /**
49504fd306cSNickeau     * Returns the Pid (process identifier), if applicable.
49604fd306cSNickeau     *
49704fd306cSNickeau     * @return int|null The process id if running, null otherwise
49804fd306cSNickeau     */
49904fd306cSNickeau    public function getPid()
50004fd306cSNickeau    {
50104fd306cSNickeau        return $this->isRunning() ? $this->processInformation['pid'] : null;
50204fd306cSNickeau    }
50304fd306cSNickeau
50404fd306cSNickeau    /**
50504fd306cSNickeau     * Sends a POSIX signal to the process.
50604fd306cSNickeau     *
50704fd306cSNickeau     * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants)
50804fd306cSNickeau     *
50904fd306cSNickeau     * @return $this
51004fd306cSNickeau     *
51104fd306cSNickeau     * @throws LogicException   In case the process is not running
51204fd306cSNickeau     * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
51304fd306cSNickeau     * @throws RuntimeException In case of failure
51404fd306cSNickeau     */
51504fd306cSNickeau    public function signal(int $signal)
51604fd306cSNickeau    {
51704fd306cSNickeau        $this->doSignal($signal, true);
51804fd306cSNickeau
51904fd306cSNickeau        return $this;
52004fd306cSNickeau    }
52104fd306cSNickeau
52204fd306cSNickeau    /**
52304fd306cSNickeau     * Disables fetching output and error output from the underlying process.
52404fd306cSNickeau     *
52504fd306cSNickeau     * @return $this
52604fd306cSNickeau     *
52704fd306cSNickeau     * @throws RuntimeException In case the process is already running
52804fd306cSNickeau     * @throws LogicException   if an idle timeout is set
52904fd306cSNickeau     */
53004fd306cSNickeau    public function disableOutput()
53104fd306cSNickeau    {
53204fd306cSNickeau        if ($this->isRunning()) {
53304fd306cSNickeau            throw new RuntimeException('Disabling output while the process is running is not possible.');
53404fd306cSNickeau        }
53504fd306cSNickeau        if (null !== $this->idleTimeout) {
53604fd306cSNickeau            throw new LogicException('Output cannot be disabled while an idle timeout is set.');
53704fd306cSNickeau        }
53804fd306cSNickeau
53904fd306cSNickeau        $this->outputDisabled = true;
54004fd306cSNickeau
54104fd306cSNickeau        return $this;
54204fd306cSNickeau    }
54304fd306cSNickeau
54404fd306cSNickeau    /**
54504fd306cSNickeau     * Enables fetching output and error output from the underlying process.
54604fd306cSNickeau     *
54704fd306cSNickeau     * @return $this
54804fd306cSNickeau     *
54904fd306cSNickeau     * @throws RuntimeException In case the process is already running
55004fd306cSNickeau     */
55104fd306cSNickeau    public function enableOutput()
55204fd306cSNickeau    {
55304fd306cSNickeau        if ($this->isRunning()) {
55404fd306cSNickeau            throw new RuntimeException('Enabling output while the process is running is not possible.');
55504fd306cSNickeau        }
55604fd306cSNickeau
55704fd306cSNickeau        $this->outputDisabled = false;
55804fd306cSNickeau
55904fd306cSNickeau        return $this;
56004fd306cSNickeau    }
56104fd306cSNickeau
56204fd306cSNickeau    /**
56304fd306cSNickeau     * Returns true in case the output is disabled, false otherwise.
56404fd306cSNickeau     *
56504fd306cSNickeau     * @return bool
56604fd306cSNickeau     */
56704fd306cSNickeau    public function isOutputDisabled()
56804fd306cSNickeau    {
56904fd306cSNickeau        return $this->outputDisabled;
57004fd306cSNickeau    }
57104fd306cSNickeau
57204fd306cSNickeau    /**
57304fd306cSNickeau     * Returns the current output of the process (STDOUT).
57404fd306cSNickeau     *
57504fd306cSNickeau     * @return string
57604fd306cSNickeau     *
57704fd306cSNickeau     * @throws LogicException in case the output has been disabled
57804fd306cSNickeau     * @throws LogicException In case the process is not started
57904fd306cSNickeau     */
58004fd306cSNickeau    public function getOutput()
58104fd306cSNickeau    {
58204fd306cSNickeau        $this->readPipesForOutput(__FUNCTION__);
58304fd306cSNickeau
58404fd306cSNickeau        if (false === $ret = stream_get_contents($this->stdout, -1, 0)) {
58504fd306cSNickeau            return '';
58604fd306cSNickeau        }
58704fd306cSNickeau
58804fd306cSNickeau        return $ret;
58904fd306cSNickeau    }
59004fd306cSNickeau
59104fd306cSNickeau    /**
59204fd306cSNickeau     * Returns the output incrementally.
59304fd306cSNickeau     *
59404fd306cSNickeau     * In comparison with the getOutput method which always return the whole
59504fd306cSNickeau     * output, this one returns the new output since the last call.
59604fd306cSNickeau     *
59704fd306cSNickeau     * @return string
59804fd306cSNickeau     *
59904fd306cSNickeau     * @throws LogicException in case the output has been disabled
60004fd306cSNickeau     * @throws LogicException In case the process is not started
60104fd306cSNickeau     */
60204fd306cSNickeau    public function getIncrementalOutput()
60304fd306cSNickeau    {
60404fd306cSNickeau        $this->readPipesForOutput(__FUNCTION__);
60504fd306cSNickeau
60604fd306cSNickeau        $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
60704fd306cSNickeau        $this->incrementalOutputOffset = ftell($this->stdout);
60804fd306cSNickeau
60904fd306cSNickeau        if (false === $latest) {
61004fd306cSNickeau            return '';
61104fd306cSNickeau        }
61204fd306cSNickeau
61304fd306cSNickeau        return $latest;
61404fd306cSNickeau    }
61504fd306cSNickeau
61604fd306cSNickeau    /**
61704fd306cSNickeau     * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR).
61804fd306cSNickeau     *
61904fd306cSNickeau     * @param int $flags A bit field of Process::ITER_* flags
62004fd306cSNickeau     *
621*83c68632SNico     * @return \Generator<string, string>
622*83c68632SNico     *
62304fd306cSNickeau     * @throws LogicException in case the output has been disabled
62404fd306cSNickeau     * @throws LogicException In case the process is not started
62504fd306cSNickeau     */
62604fd306cSNickeau    #[\ReturnTypeWillChange]
62704fd306cSNickeau    public function getIterator(int $flags = 0)
62804fd306cSNickeau    {
62904fd306cSNickeau        $this->readPipesForOutput(__FUNCTION__, false);
63004fd306cSNickeau
63104fd306cSNickeau        $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags);
63204fd306cSNickeau        $blocking = !(self::ITER_NON_BLOCKING & $flags);
63304fd306cSNickeau        $yieldOut = !(self::ITER_SKIP_OUT & $flags);
63404fd306cSNickeau        $yieldErr = !(self::ITER_SKIP_ERR & $flags);
63504fd306cSNickeau
63604fd306cSNickeau        while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) {
63704fd306cSNickeau            if ($yieldOut) {
63804fd306cSNickeau                $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
63904fd306cSNickeau
64004fd306cSNickeau                if (isset($out[0])) {
64104fd306cSNickeau                    if ($clearOutput) {
64204fd306cSNickeau                        $this->clearOutput();
64304fd306cSNickeau                    } else {
64404fd306cSNickeau                        $this->incrementalOutputOffset = ftell($this->stdout);
64504fd306cSNickeau                    }
64604fd306cSNickeau
64704fd306cSNickeau                    yield self::OUT => $out;
64804fd306cSNickeau                }
64904fd306cSNickeau            }
65004fd306cSNickeau
65104fd306cSNickeau            if ($yieldErr) {
65204fd306cSNickeau                $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
65304fd306cSNickeau
65404fd306cSNickeau                if (isset($err[0])) {
65504fd306cSNickeau                    if ($clearOutput) {
65604fd306cSNickeau                        $this->clearErrorOutput();
65704fd306cSNickeau                    } else {
65804fd306cSNickeau                        $this->incrementalErrorOutputOffset = ftell($this->stderr);
65904fd306cSNickeau                    }
66004fd306cSNickeau
66104fd306cSNickeau                    yield self::ERR => $err;
66204fd306cSNickeau                }
66304fd306cSNickeau            }
66404fd306cSNickeau
66504fd306cSNickeau            if (!$blocking && !isset($out[0]) && !isset($err[0])) {
66604fd306cSNickeau                yield self::OUT => '';
66704fd306cSNickeau            }
66804fd306cSNickeau
66904fd306cSNickeau            $this->checkTimeout();
67004fd306cSNickeau            $this->readPipesForOutput(__FUNCTION__, $blocking);
67104fd306cSNickeau        }
67204fd306cSNickeau    }
67304fd306cSNickeau
67404fd306cSNickeau    /**
67504fd306cSNickeau     * Clears the process output.
67604fd306cSNickeau     *
67704fd306cSNickeau     * @return $this
67804fd306cSNickeau     */
67904fd306cSNickeau    public function clearOutput()
68004fd306cSNickeau    {
68104fd306cSNickeau        ftruncate($this->stdout, 0);
68204fd306cSNickeau        fseek($this->stdout, 0);
68304fd306cSNickeau        $this->incrementalOutputOffset = 0;
68404fd306cSNickeau
68504fd306cSNickeau        return $this;
68604fd306cSNickeau    }
68704fd306cSNickeau
68804fd306cSNickeau    /**
68904fd306cSNickeau     * Returns the current error output of the process (STDERR).
69004fd306cSNickeau     *
69104fd306cSNickeau     * @return string
69204fd306cSNickeau     *
69304fd306cSNickeau     * @throws LogicException in case the output has been disabled
69404fd306cSNickeau     * @throws LogicException In case the process is not started
69504fd306cSNickeau     */
69604fd306cSNickeau    public function getErrorOutput()
69704fd306cSNickeau    {
69804fd306cSNickeau        $this->readPipesForOutput(__FUNCTION__);
69904fd306cSNickeau
70004fd306cSNickeau        if (false === $ret = stream_get_contents($this->stderr, -1, 0)) {
70104fd306cSNickeau            return '';
70204fd306cSNickeau        }
70304fd306cSNickeau
70404fd306cSNickeau        return $ret;
70504fd306cSNickeau    }
70604fd306cSNickeau
70704fd306cSNickeau    /**
70804fd306cSNickeau     * Returns the errorOutput incrementally.
70904fd306cSNickeau     *
71004fd306cSNickeau     * In comparison with the getErrorOutput method which always return the
71104fd306cSNickeau     * whole error output, this one returns the new error output since the last
71204fd306cSNickeau     * call.
71304fd306cSNickeau     *
71404fd306cSNickeau     * @return string
71504fd306cSNickeau     *
71604fd306cSNickeau     * @throws LogicException in case the output has been disabled
71704fd306cSNickeau     * @throws LogicException In case the process is not started
71804fd306cSNickeau     */
71904fd306cSNickeau    public function getIncrementalErrorOutput()
72004fd306cSNickeau    {
72104fd306cSNickeau        $this->readPipesForOutput(__FUNCTION__);
72204fd306cSNickeau
72304fd306cSNickeau        $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
72404fd306cSNickeau        $this->incrementalErrorOutputOffset = ftell($this->stderr);
72504fd306cSNickeau
72604fd306cSNickeau        if (false === $latest) {
72704fd306cSNickeau            return '';
72804fd306cSNickeau        }
72904fd306cSNickeau
73004fd306cSNickeau        return $latest;
73104fd306cSNickeau    }
73204fd306cSNickeau
73304fd306cSNickeau    /**
73404fd306cSNickeau     * Clears the process output.
73504fd306cSNickeau     *
73604fd306cSNickeau     * @return $this
73704fd306cSNickeau     */
73804fd306cSNickeau    public function clearErrorOutput()
73904fd306cSNickeau    {
74004fd306cSNickeau        ftruncate($this->stderr, 0);
74104fd306cSNickeau        fseek($this->stderr, 0);
74204fd306cSNickeau        $this->incrementalErrorOutputOffset = 0;
74304fd306cSNickeau
74404fd306cSNickeau        return $this;
74504fd306cSNickeau    }
74604fd306cSNickeau
74704fd306cSNickeau    /**
74804fd306cSNickeau     * Returns the exit code returned by the process.
74904fd306cSNickeau     *
75004fd306cSNickeau     * @return int|null The exit status code, null if the Process is not terminated
75104fd306cSNickeau     */
75204fd306cSNickeau    public function getExitCode()
75304fd306cSNickeau    {
75404fd306cSNickeau        $this->updateStatus(false);
75504fd306cSNickeau
75604fd306cSNickeau        return $this->exitcode;
75704fd306cSNickeau    }
75804fd306cSNickeau
75904fd306cSNickeau    /**
76004fd306cSNickeau     * Returns a string representation for the exit code returned by the process.
76104fd306cSNickeau     *
76204fd306cSNickeau     * This method relies on the Unix exit code status standardization
76304fd306cSNickeau     * and might not be relevant for other operating systems.
76404fd306cSNickeau     *
76504fd306cSNickeau     * @return string|null A string representation for the exit status code, null if the Process is not terminated
76604fd306cSNickeau     *
76704fd306cSNickeau     * @see http://tldp.org/LDP/abs/html/exitcodes.html
76804fd306cSNickeau     * @see http://en.wikipedia.org/wiki/Unix_signal
76904fd306cSNickeau     */
77004fd306cSNickeau    public function getExitCodeText()
77104fd306cSNickeau    {
77204fd306cSNickeau        if (null === $exitcode = $this->getExitCode()) {
77304fd306cSNickeau            return null;
77404fd306cSNickeau        }
77504fd306cSNickeau
77604fd306cSNickeau        return self::$exitCodes[$exitcode] ?? 'Unknown error';
77704fd306cSNickeau    }
77804fd306cSNickeau
77904fd306cSNickeau    /**
78004fd306cSNickeau     * Checks if the process ended successfully.
78104fd306cSNickeau     *
78204fd306cSNickeau     * @return bool
78304fd306cSNickeau     */
78404fd306cSNickeau    public function isSuccessful()
78504fd306cSNickeau    {
78604fd306cSNickeau        return 0 === $this->getExitCode();
78704fd306cSNickeau    }
78804fd306cSNickeau
78904fd306cSNickeau    /**
79004fd306cSNickeau     * Returns true if the child process has been terminated by an uncaught signal.
79104fd306cSNickeau     *
79204fd306cSNickeau     * It always returns false on Windows.
79304fd306cSNickeau     *
79404fd306cSNickeau     * @return bool
79504fd306cSNickeau     *
79604fd306cSNickeau     * @throws LogicException In case the process is not terminated
79704fd306cSNickeau     */
79804fd306cSNickeau    public function hasBeenSignaled()
79904fd306cSNickeau    {
80004fd306cSNickeau        $this->requireProcessIsTerminated(__FUNCTION__);
80104fd306cSNickeau
80204fd306cSNickeau        return $this->processInformation['signaled'];
80304fd306cSNickeau    }
80404fd306cSNickeau
80504fd306cSNickeau    /**
80604fd306cSNickeau     * Returns the number of the signal that caused the child process to terminate its execution.
80704fd306cSNickeau     *
80804fd306cSNickeau     * It is only meaningful if hasBeenSignaled() returns true.
80904fd306cSNickeau     *
81004fd306cSNickeau     * @return int
81104fd306cSNickeau     *
81204fd306cSNickeau     * @throws RuntimeException In case --enable-sigchild is activated
81304fd306cSNickeau     * @throws LogicException   In case the process is not terminated
81404fd306cSNickeau     */
81504fd306cSNickeau    public function getTermSignal()
81604fd306cSNickeau    {
81704fd306cSNickeau        $this->requireProcessIsTerminated(__FUNCTION__);
81804fd306cSNickeau
81904fd306cSNickeau        if ($this->isSigchildEnabled() && -1 === $this->processInformation['termsig']) {
82004fd306cSNickeau            throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal cannot be retrieved.');
82104fd306cSNickeau        }
82204fd306cSNickeau
82304fd306cSNickeau        return $this->processInformation['termsig'];
82404fd306cSNickeau    }
82504fd306cSNickeau
82604fd306cSNickeau    /**
82704fd306cSNickeau     * Returns true if the child process has been stopped by a signal.
82804fd306cSNickeau     *
82904fd306cSNickeau     * It always returns false on Windows.
83004fd306cSNickeau     *
83104fd306cSNickeau     * @return bool
83204fd306cSNickeau     *
83304fd306cSNickeau     * @throws LogicException In case the process is not terminated
83404fd306cSNickeau     */
83504fd306cSNickeau    public function hasBeenStopped()
83604fd306cSNickeau    {
83704fd306cSNickeau        $this->requireProcessIsTerminated(__FUNCTION__);
83804fd306cSNickeau
83904fd306cSNickeau        return $this->processInformation['stopped'];
84004fd306cSNickeau    }
84104fd306cSNickeau
84204fd306cSNickeau    /**
84304fd306cSNickeau     * Returns the number of the signal that caused the child process to stop its execution.
84404fd306cSNickeau     *
84504fd306cSNickeau     * It is only meaningful if hasBeenStopped() returns true.
84604fd306cSNickeau     *
84704fd306cSNickeau     * @return int
84804fd306cSNickeau     *
84904fd306cSNickeau     * @throws LogicException In case the process is not terminated
85004fd306cSNickeau     */
85104fd306cSNickeau    public function getStopSignal()
85204fd306cSNickeau    {
85304fd306cSNickeau        $this->requireProcessIsTerminated(__FUNCTION__);
85404fd306cSNickeau
85504fd306cSNickeau        return $this->processInformation['stopsig'];
85604fd306cSNickeau    }
85704fd306cSNickeau
85804fd306cSNickeau    /**
85904fd306cSNickeau     * Checks if the process is currently running.
86004fd306cSNickeau     *
86104fd306cSNickeau     * @return bool
86204fd306cSNickeau     */
86304fd306cSNickeau    public function isRunning()
86404fd306cSNickeau    {
86504fd306cSNickeau        if (self::STATUS_STARTED !== $this->status) {
86604fd306cSNickeau            return false;
86704fd306cSNickeau        }
86804fd306cSNickeau
86904fd306cSNickeau        $this->updateStatus(false);
87004fd306cSNickeau
87104fd306cSNickeau        return $this->processInformation['running'];
87204fd306cSNickeau    }
87304fd306cSNickeau
87404fd306cSNickeau    /**
87504fd306cSNickeau     * Checks if the process has been started with no regard to the current state.
87604fd306cSNickeau     *
87704fd306cSNickeau     * @return bool
87804fd306cSNickeau     */
87904fd306cSNickeau    public function isStarted()
88004fd306cSNickeau    {
88104fd306cSNickeau        return self::STATUS_READY != $this->status;
88204fd306cSNickeau    }
88304fd306cSNickeau
88404fd306cSNickeau    /**
88504fd306cSNickeau     * Checks if the process is terminated.
88604fd306cSNickeau     *
88704fd306cSNickeau     * @return bool
88804fd306cSNickeau     */
88904fd306cSNickeau    public function isTerminated()
89004fd306cSNickeau    {
89104fd306cSNickeau        $this->updateStatus(false);
89204fd306cSNickeau
89304fd306cSNickeau        return self::STATUS_TERMINATED == $this->status;
89404fd306cSNickeau    }
89504fd306cSNickeau
89604fd306cSNickeau    /**
89704fd306cSNickeau     * Gets the process status.
89804fd306cSNickeau     *
89904fd306cSNickeau     * The status is one of: ready, started, terminated.
90004fd306cSNickeau     *
90104fd306cSNickeau     * @return string
90204fd306cSNickeau     */
90304fd306cSNickeau    public function getStatus()
90404fd306cSNickeau    {
90504fd306cSNickeau        $this->updateStatus(false);
90604fd306cSNickeau
90704fd306cSNickeau        return $this->status;
90804fd306cSNickeau    }
90904fd306cSNickeau
91004fd306cSNickeau    /**
91104fd306cSNickeau     * Stops the process.
91204fd306cSNickeau     *
91304fd306cSNickeau     * @param int|float $timeout The timeout in seconds
914*83c68632SNico     * @param int|null  $signal  A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9)
91504fd306cSNickeau     *
91604fd306cSNickeau     * @return int|null The exit-code of the process or null if it's not running
91704fd306cSNickeau     */
918*83c68632SNico    public function stop(float $timeout = 10, ?int $signal = null)
91904fd306cSNickeau    {
92004fd306cSNickeau        $timeoutMicro = microtime(true) + $timeout;
92104fd306cSNickeau        if ($this->isRunning()) {
92204fd306cSNickeau            // given SIGTERM may not be defined and that "proc_terminate" uses the constant value and not the constant itself, we use the same here
92304fd306cSNickeau            $this->doSignal(15, false);
92404fd306cSNickeau            do {
92504fd306cSNickeau                usleep(1000);
92604fd306cSNickeau            } while ($this->isRunning() && microtime(true) < $timeoutMicro);
92704fd306cSNickeau
92804fd306cSNickeau            if ($this->isRunning()) {
92904fd306cSNickeau                // Avoid exception here: process is supposed to be running, but it might have stopped just
93004fd306cSNickeau                // after this line. In any case, let's silently discard the error, we cannot do anything.
93104fd306cSNickeau                $this->doSignal($signal ?: 9, false);
93204fd306cSNickeau            }
93304fd306cSNickeau        }
93404fd306cSNickeau
93504fd306cSNickeau        if ($this->isRunning()) {
93604fd306cSNickeau            if (isset($this->fallbackStatus['pid'])) {
93704fd306cSNickeau                unset($this->fallbackStatus['pid']);
93804fd306cSNickeau
93904fd306cSNickeau                return $this->stop(0, $signal);
94004fd306cSNickeau            }
94104fd306cSNickeau            $this->close();
94204fd306cSNickeau        }
94304fd306cSNickeau
94404fd306cSNickeau        return $this->exitcode;
94504fd306cSNickeau    }
94604fd306cSNickeau
94704fd306cSNickeau    /**
94804fd306cSNickeau     * Adds a line to the STDOUT stream.
94904fd306cSNickeau     *
95004fd306cSNickeau     * @internal
95104fd306cSNickeau     */
95204fd306cSNickeau    public function addOutput(string $line)
95304fd306cSNickeau    {
95404fd306cSNickeau        $this->lastOutputTime = microtime(true);
95504fd306cSNickeau
95604fd306cSNickeau        fseek($this->stdout, 0, \SEEK_END);
95704fd306cSNickeau        fwrite($this->stdout, $line);
95804fd306cSNickeau        fseek($this->stdout, $this->incrementalOutputOffset);
95904fd306cSNickeau    }
96004fd306cSNickeau
96104fd306cSNickeau    /**
96204fd306cSNickeau     * Adds a line to the STDERR stream.
96304fd306cSNickeau     *
96404fd306cSNickeau     * @internal
96504fd306cSNickeau     */
96604fd306cSNickeau    public function addErrorOutput(string $line)
96704fd306cSNickeau    {
96804fd306cSNickeau        $this->lastOutputTime = microtime(true);
96904fd306cSNickeau
97004fd306cSNickeau        fseek($this->stderr, 0, \SEEK_END);
97104fd306cSNickeau        fwrite($this->stderr, $line);
97204fd306cSNickeau        fseek($this->stderr, $this->incrementalErrorOutputOffset);
97304fd306cSNickeau    }
97404fd306cSNickeau
97504fd306cSNickeau    /**
97604fd306cSNickeau     * Gets the last output time in seconds.
97704fd306cSNickeau     */
97804fd306cSNickeau    public function getLastOutputTime(): ?float
97904fd306cSNickeau    {
98004fd306cSNickeau        return $this->lastOutputTime;
98104fd306cSNickeau    }
98204fd306cSNickeau
98304fd306cSNickeau    /**
98404fd306cSNickeau     * Gets the command line to be executed.
98504fd306cSNickeau     *
98604fd306cSNickeau     * @return string
98704fd306cSNickeau     */
98804fd306cSNickeau    public function getCommandLine()
98904fd306cSNickeau    {
99004fd306cSNickeau        return \is_array($this->commandline) ? implode(' ', array_map([$this, 'escapeArgument'], $this->commandline)) : $this->commandline;
99104fd306cSNickeau    }
99204fd306cSNickeau
99304fd306cSNickeau    /**
99404fd306cSNickeau     * Gets the process timeout in seconds (max. runtime).
99504fd306cSNickeau     *
99604fd306cSNickeau     * @return float|null
99704fd306cSNickeau     */
99804fd306cSNickeau    public function getTimeout()
99904fd306cSNickeau    {
100004fd306cSNickeau        return $this->timeout;
100104fd306cSNickeau    }
100204fd306cSNickeau
100304fd306cSNickeau    /**
100404fd306cSNickeau     * Gets the process idle timeout in seconds (max. time since last output).
100504fd306cSNickeau     *
100604fd306cSNickeau     * @return float|null
100704fd306cSNickeau     */
100804fd306cSNickeau    public function getIdleTimeout()
100904fd306cSNickeau    {
101004fd306cSNickeau        return $this->idleTimeout;
101104fd306cSNickeau    }
101204fd306cSNickeau
101304fd306cSNickeau    /**
101404fd306cSNickeau     * Sets the process timeout (max. runtime) in seconds.
101504fd306cSNickeau     *
101604fd306cSNickeau     * To disable the timeout, set this value to null.
101704fd306cSNickeau     *
101804fd306cSNickeau     * @return $this
101904fd306cSNickeau     *
102004fd306cSNickeau     * @throws InvalidArgumentException if the timeout is negative
102104fd306cSNickeau     */
102204fd306cSNickeau    public function setTimeout(?float $timeout)
102304fd306cSNickeau    {
102404fd306cSNickeau        $this->timeout = $this->validateTimeout($timeout);
102504fd306cSNickeau
102604fd306cSNickeau        return $this;
102704fd306cSNickeau    }
102804fd306cSNickeau
102904fd306cSNickeau    /**
103004fd306cSNickeau     * Sets the process idle timeout (max. time since last output) in seconds.
103104fd306cSNickeau     *
103204fd306cSNickeau     * To disable the timeout, set this value to null.
103304fd306cSNickeau     *
103404fd306cSNickeau     * @return $this
103504fd306cSNickeau     *
103604fd306cSNickeau     * @throws LogicException           if the output is disabled
103704fd306cSNickeau     * @throws InvalidArgumentException if the timeout is negative
103804fd306cSNickeau     */
103904fd306cSNickeau    public function setIdleTimeout(?float $timeout)
104004fd306cSNickeau    {
104104fd306cSNickeau        if (null !== $timeout && $this->outputDisabled) {
104204fd306cSNickeau            throw new LogicException('Idle timeout cannot be set while the output is disabled.');
104304fd306cSNickeau        }
104404fd306cSNickeau
104504fd306cSNickeau        $this->idleTimeout = $this->validateTimeout($timeout);
104604fd306cSNickeau
104704fd306cSNickeau        return $this;
104804fd306cSNickeau    }
104904fd306cSNickeau
105004fd306cSNickeau    /**
105104fd306cSNickeau     * Enables or disables the TTY mode.
105204fd306cSNickeau     *
105304fd306cSNickeau     * @return $this
105404fd306cSNickeau     *
105504fd306cSNickeau     * @throws RuntimeException In case the TTY mode is not supported
105604fd306cSNickeau     */
105704fd306cSNickeau    public function setTty(bool $tty)
105804fd306cSNickeau    {
105904fd306cSNickeau        if ('\\' === \DIRECTORY_SEPARATOR && $tty) {
106004fd306cSNickeau            throw new RuntimeException('TTY mode is not supported on Windows platform.');
106104fd306cSNickeau        }
106204fd306cSNickeau
106304fd306cSNickeau        if ($tty && !self::isTtySupported()) {
106404fd306cSNickeau            throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.');
106504fd306cSNickeau        }
106604fd306cSNickeau
106704fd306cSNickeau        $this->tty = $tty;
106804fd306cSNickeau
106904fd306cSNickeau        return $this;
107004fd306cSNickeau    }
107104fd306cSNickeau
107204fd306cSNickeau    /**
107304fd306cSNickeau     * Checks if the TTY mode is enabled.
107404fd306cSNickeau     *
107504fd306cSNickeau     * @return bool
107604fd306cSNickeau     */
107704fd306cSNickeau    public function isTty()
107804fd306cSNickeau    {
107904fd306cSNickeau        return $this->tty;
108004fd306cSNickeau    }
108104fd306cSNickeau
108204fd306cSNickeau    /**
108304fd306cSNickeau     * Sets PTY mode.
108404fd306cSNickeau     *
108504fd306cSNickeau     * @return $this
108604fd306cSNickeau     */
108704fd306cSNickeau    public function setPty(bool $bool)
108804fd306cSNickeau    {
108904fd306cSNickeau        $this->pty = $bool;
109004fd306cSNickeau
109104fd306cSNickeau        return $this;
109204fd306cSNickeau    }
109304fd306cSNickeau
109404fd306cSNickeau    /**
109504fd306cSNickeau     * Returns PTY state.
109604fd306cSNickeau     *
109704fd306cSNickeau     * @return bool
109804fd306cSNickeau     */
109904fd306cSNickeau    public function isPty()
110004fd306cSNickeau    {
110104fd306cSNickeau        return $this->pty;
110204fd306cSNickeau    }
110304fd306cSNickeau
110404fd306cSNickeau    /**
110504fd306cSNickeau     * Gets the working directory.
110604fd306cSNickeau     *
110704fd306cSNickeau     * @return string|null
110804fd306cSNickeau     */
110904fd306cSNickeau    public function getWorkingDirectory()
111004fd306cSNickeau    {
111104fd306cSNickeau        if (null === $this->cwd) {
111204fd306cSNickeau            // getcwd() will return false if any one of the parent directories does not have
111304fd306cSNickeau            // the readable or search mode set, even if the current directory does
111404fd306cSNickeau            return getcwd() ?: null;
111504fd306cSNickeau        }
111604fd306cSNickeau
111704fd306cSNickeau        return $this->cwd;
111804fd306cSNickeau    }
111904fd306cSNickeau
112004fd306cSNickeau    /**
112104fd306cSNickeau     * Sets the current working directory.
112204fd306cSNickeau     *
112304fd306cSNickeau     * @return $this
112404fd306cSNickeau     */
112504fd306cSNickeau    public function setWorkingDirectory(string $cwd)
112604fd306cSNickeau    {
112704fd306cSNickeau        $this->cwd = $cwd;
112804fd306cSNickeau
112904fd306cSNickeau        return $this;
113004fd306cSNickeau    }
113104fd306cSNickeau
113204fd306cSNickeau    /**
113304fd306cSNickeau     * Gets the environment variables.
113404fd306cSNickeau     *
113504fd306cSNickeau     * @return array
113604fd306cSNickeau     */
113704fd306cSNickeau    public function getEnv()
113804fd306cSNickeau    {
113904fd306cSNickeau        return $this->env;
114004fd306cSNickeau    }
114104fd306cSNickeau
114204fd306cSNickeau    /**
114304fd306cSNickeau     * Sets the environment variables.
114404fd306cSNickeau     *
114504fd306cSNickeau     * @param array<string|\Stringable> $env The new environment variables
114604fd306cSNickeau     *
114704fd306cSNickeau     * @return $this
114804fd306cSNickeau     */
114904fd306cSNickeau    public function setEnv(array $env)
115004fd306cSNickeau    {
115104fd306cSNickeau        $this->env = $env;
115204fd306cSNickeau
115304fd306cSNickeau        return $this;
115404fd306cSNickeau    }
115504fd306cSNickeau
115604fd306cSNickeau    /**
115704fd306cSNickeau     * Gets the Process input.
115804fd306cSNickeau     *
115904fd306cSNickeau     * @return resource|string|\Iterator|null
116004fd306cSNickeau     */
116104fd306cSNickeau    public function getInput()
116204fd306cSNickeau    {
116304fd306cSNickeau        return $this->input;
116404fd306cSNickeau    }
116504fd306cSNickeau
116604fd306cSNickeau    /**
116704fd306cSNickeau     * Sets the input.
116804fd306cSNickeau     *
116904fd306cSNickeau     * This content will be passed to the underlying process standard input.
117004fd306cSNickeau     *
117104fd306cSNickeau     * @param string|int|float|bool|resource|\Traversable|null $input The content
117204fd306cSNickeau     *
117304fd306cSNickeau     * @return $this
117404fd306cSNickeau     *
117504fd306cSNickeau     * @throws LogicException In case the process is running
117604fd306cSNickeau     */
117704fd306cSNickeau    public function setInput($input)
117804fd306cSNickeau    {
117904fd306cSNickeau        if ($this->isRunning()) {
118004fd306cSNickeau            throw new LogicException('Input cannot be set while the process is running.');
118104fd306cSNickeau        }
118204fd306cSNickeau
118304fd306cSNickeau        $this->input = ProcessUtils::validateInput(__METHOD__, $input);
118404fd306cSNickeau
118504fd306cSNickeau        return $this;
118604fd306cSNickeau    }
118704fd306cSNickeau
118804fd306cSNickeau    /**
118904fd306cSNickeau     * Performs a check between the timeout definition and the time the process started.
119004fd306cSNickeau     *
119104fd306cSNickeau     * In case you run a background process (with the start method), you should
119204fd306cSNickeau     * trigger this method regularly to ensure the process timeout
119304fd306cSNickeau     *
119404fd306cSNickeau     * @throws ProcessTimedOutException In case the timeout was reached
119504fd306cSNickeau     */
119604fd306cSNickeau    public function checkTimeout()
119704fd306cSNickeau    {
119804fd306cSNickeau        if (self::STATUS_STARTED !== $this->status) {
119904fd306cSNickeau            return;
120004fd306cSNickeau        }
120104fd306cSNickeau
120204fd306cSNickeau        if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
120304fd306cSNickeau            $this->stop(0);
120404fd306cSNickeau
120504fd306cSNickeau            throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL);
120604fd306cSNickeau        }
120704fd306cSNickeau
120804fd306cSNickeau        if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) {
120904fd306cSNickeau            $this->stop(0);
121004fd306cSNickeau
121104fd306cSNickeau            throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE);
121204fd306cSNickeau        }
121304fd306cSNickeau    }
121404fd306cSNickeau
121504fd306cSNickeau    /**
121604fd306cSNickeau     * @throws LogicException in case process is not started
121704fd306cSNickeau     */
121804fd306cSNickeau    public function getStartTime(): float
121904fd306cSNickeau    {
122004fd306cSNickeau        if (!$this->isStarted()) {
122104fd306cSNickeau            throw new LogicException('Start time is only available after process start.');
122204fd306cSNickeau        }
122304fd306cSNickeau
122404fd306cSNickeau        return $this->starttime;
122504fd306cSNickeau    }
122604fd306cSNickeau
122704fd306cSNickeau    /**
122804fd306cSNickeau     * Defines options to pass to the underlying proc_open().
122904fd306cSNickeau     *
123004fd306cSNickeau     * @see https://php.net/proc_open for the options supported by PHP.
123104fd306cSNickeau     *
123204fd306cSNickeau     * Enabling the "create_new_console" option allows a subprocess to continue
123304fd306cSNickeau     * to run after the main process exited, on both Windows and *nix
123404fd306cSNickeau     */
123504fd306cSNickeau    public function setOptions(array $options)
123604fd306cSNickeau    {
123704fd306cSNickeau        if ($this->isRunning()) {
123804fd306cSNickeau            throw new RuntimeException('Setting options while the process is running is not possible.');
123904fd306cSNickeau        }
124004fd306cSNickeau
124104fd306cSNickeau        $defaultOptions = $this->options;
124204fd306cSNickeau        $existingOptions = ['blocking_pipes', 'create_process_group', 'create_new_console'];
124304fd306cSNickeau
124404fd306cSNickeau        foreach ($options as $key => $value) {
124504fd306cSNickeau            if (!\in_array($key, $existingOptions)) {
124604fd306cSNickeau                $this->options = $defaultOptions;
124704fd306cSNickeau                throw new LogicException(sprintf('Invalid option "%s" passed to "%s()". Supported options are "%s".', $key, __METHOD__, implode('", "', $existingOptions)));
124804fd306cSNickeau            }
124904fd306cSNickeau            $this->options[$key] = $value;
125004fd306cSNickeau        }
125104fd306cSNickeau    }
125204fd306cSNickeau
125304fd306cSNickeau    /**
125404fd306cSNickeau     * Returns whether TTY is supported on the current operating system.
125504fd306cSNickeau     */
125604fd306cSNickeau    public static function isTtySupported(): bool
125704fd306cSNickeau    {
125804fd306cSNickeau        static $isTtySupported;
125904fd306cSNickeau
126004fd306cSNickeau        if (null === $isTtySupported) {
126104fd306cSNickeau            $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes);
126204fd306cSNickeau        }
126304fd306cSNickeau
126404fd306cSNickeau        return $isTtySupported;
126504fd306cSNickeau    }
126604fd306cSNickeau
126704fd306cSNickeau    /**
126804fd306cSNickeau     * Returns whether PTY is supported on the current operating system.
126904fd306cSNickeau     *
127004fd306cSNickeau     * @return bool
127104fd306cSNickeau     */
127204fd306cSNickeau    public static function isPtySupported()
127304fd306cSNickeau    {
127404fd306cSNickeau        static $result;
127504fd306cSNickeau
127604fd306cSNickeau        if (null !== $result) {
127704fd306cSNickeau            return $result;
127804fd306cSNickeau        }
127904fd306cSNickeau
128004fd306cSNickeau        if ('\\' === \DIRECTORY_SEPARATOR) {
128104fd306cSNickeau            return $result = false;
128204fd306cSNickeau        }
128304fd306cSNickeau
128404fd306cSNickeau        return $result = (bool) @proc_open('echo 1 >/dev/null', [['pty'], ['pty'], ['pty']], $pipes);
128504fd306cSNickeau    }
128604fd306cSNickeau
128704fd306cSNickeau    /**
128804fd306cSNickeau     * Creates the descriptors needed by the proc_open.
128904fd306cSNickeau     */
129004fd306cSNickeau    private function getDescriptors(): array
129104fd306cSNickeau    {
129204fd306cSNickeau        if ($this->input instanceof \Iterator) {
129304fd306cSNickeau            $this->input->rewind();
129404fd306cSNickeau        }
129504fd306cSNickeau        if ('\\' === \DIRECTORY_SEPARATOR) {
129604fd306cSNickeau            $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $this->hasCallback);
129704fd306cSNickeau        } else {
129804fd306cSNickeau            $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->hasCallback);
129904fd306cSNickeau        }
130004fd306cSNickeau
130104fd306cSNickeau        return $this->processPipes->getDescriptors();
130204fd306cSNickeau    }
130304fd306cSNickeau
130404fd306cSNickeau    /**
130504fd306cSNickeau     * Builds up the callback used by wait().
130604fd306cSNickeau     *
130704fd306cSNickeau     * The callbacks adds all occurred output to the specific buffer and calls
130804fd306cSNickeau     * the user callback (if present) with the received output.
130904fd306cSNickeau     *
131004fd306cSNickeau     * @param callable|null $callback The user defined PHP callback
131104fd306cSNickeau     *
131204fd306cSNickeau     * @return \Closure
131304fd306cSNickeau     */
1314*83c68632SNico    protected function buildCallback(?callable $callback = null)
131504fd306cSNickeau    {
131604fd306cSNickeau        if ($this->outputDisabled) {
131704fd306cSNickeau            return function ($type, $data) use ($callback): bool {
131804fd306cSNickeau                return null !== $callback && $callback($type, $data);
131904fd306cSNickeau            };
132004fd306cSNickeau        }
132104fd306cSNickeau
132204fd306cSNickeau        $out = self::OUT;
132304fd306cSNickeau
132404fd306cSNickeau        return function ($type, $data) use ($callback, $out): bool {
132504fd306cSNickeau            if ($out == $type) {
132604fd306cSNickeau                $this->addOutput($data);
132704fd306cSNickeau            } else {
132804fd306cSNickeau                $this->addErrorOutput($data);
132904fd306cSNickeau            }
133004fd306cSNickeau
133104fd306cSNickeau            return null !== $callback && $callback($type, $data);
133204fd306cSNickeau        };
133304fd306cSNickeau    }
133404fd306cSNickeau
133504fd306cSNickeau    /**
133604fd306cSNickeau     * Updates the status of the process, reads pipes.
133704fd306cSNickeau     *
133804fd306cSNickeau     * @param bool $blocking Whether to use a blocking read call
133904fd306cSNickeau     */
134004fd306cSNickeau    protected function updateStatus(bool $blocking)
134104fd306cSNickeau    {
134204fd306cSNickeau        if (self::STATUS_STARTED !== $this->status) {
134304fd306cSNickeau            return;
134404fd306cSNickeau        }
134504fd306cSNickeau
134604fd306cSNickeau        $this->processInformation = proc_get_status($this->process);
134704fd306cSNickeau        $running = $this->processInformation['running'];
134804fd306cSNickeau
1349*83c68632SNico        // In PHP < 8.3, "proc_get_status" only returns the correct exit status on the first call.
1350*83c68632SNico        // Subsequent calls return -1 as the process is discarded. This workaround caches the first
1351*83c68632SNico        // retrieved exit status for consistent results in later calls, mimicking PHP 8.3 behavior.
1352*83c68632SNico        if (\PHP_VERSION_ID < 80300) {
1353*83c68632SNico            if (!isset($this->cachedExitCode) && !$running && -1 !== $this->processInformation['exitcode']) {
1354*83c68632SNico                $this->cachedExitCode = $this->processInformation['exitcode'];
1355*83c68632SNico            }
1356*83c68632SNico
1357*83c68632SNico            if (isset($this->cachedExitCode) && !$running && -1 === $this->processInformation['exitcode']) {
1358*83c68632SNico                $this->processInformation['exitcode'] = $this->cachedExitCode;
1359*83c68632SNico            }
1360*83c68632SNico        }
1361*83c68632SNico
136204fd306cSNickeau        $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running);
136304fd306cSNickeau
136404fd306cSNickeau        if ($this->fallbackStatus && $this->isSigchildEnabled()) {
136504fd306cSNickeau            $this->processInformation = $this->fallbackStatus + $this->processInformation;
136604fd306cSNickeau        }
136704fd306cSNickeau
136804fd306cSNickeau        if (!$running) {
136904fd306cSNickeau            $this->close();
137004fd306cSNickeau        }
137104fd306cSNickeau    }
137204fd306cSNickeau
137304fd306cSNickeau    /**
137404fd306cSNickeau     * Returns whether PHP has been compiled with the '--enable-sigchild' option or not.
137504fd306cSNickeau     *
137604fd306cSNickeau     * @return bool
137704fd306cSNickeau     */
137804fd306cSNickeau    protected function isSigchildEnabled()
137904fd306cSNickeau    {
138004fd306cSNickeau        if (null !== self::$sigchild) {
138104fd306cSNickeau            return self::$sigchild;
138204fd306cSNickeau        }
138304fd306cSNickeau
138404fd306cSNickeau        if (!\function_exists('phpinfo')) {
138504fd306cSNickeau            return self::$sigchild = false;
138604fd306cSNickeau        }
138704fd306cSNickeau
138804fd306cSNickeau        ob_start();
138904fd306cSNickeau        phpinfo(\INFO_GENERAL);
139004fd306cSNickeau
139104fd306cSNickeau        return self::$sigchild = str_contains(ob_get_clean(), '--enable-sigchild');
139204fd306cSNickeau    }
139304fd306cSNickeau
139404fd306cSNickeau    /**
139504fd306cSNickeau     * Reads pipes for the freshest output.
139604fd306cSNickeau     *
139704fd306cSNickeau     * @param string $caller   The name of the method that needs fresh outputs
139804fd306cSNickeau     * @param bool   $blocking Whether to use blocking calls or not
139904fd306cSNickeau     *
140004fd306cSNickeau     * @throws LogicException in case output has been disabled or process is not started
140104fd306cSNickeau     */
140204fd306cSNickeau    private function readPipesForOutput(string $caller, bool $blocking = false)
140304fd306cSNickeau    {
140404fd306cSNickeau        if ($this->outputDisabled) {
140504fd306cSNickeau            throw new LogicException('Output has been disabled.');
140604fd306cSNickeau        }
140704fd306cSNickeau
140804fd306cSNickeau        $this->requireProcessIsStarted($caller);
140904fd306cSNickeau
141004fd306cSNickeau        $this->updateStatus($blocking);
141104fd306cSNickeau    }
141204fd306cSNickeau
141304fd306cSNickeau    /**
141404fd306cSNickeau     * Validates and returns the filtered timeout.
141504fd306cSNickeau     *
141604fd306cSNickeau     * @throws InvalidArgumentException if the given timeout is a negative number
141704fd306cSNickeau     */
141804fd306cSNickeau    private function validateTimeout(?float $timeout): ?float
141904fd306cSNickeau    {
142004fd306cSNickeau        $timeout = (float) $timeout;
142104fd306cSNickeau
142204fd306cSNickeau        if (0.0 === $timeout) {
142304fd306cSNickeau            $timeout = null;
142404fd306cSNickeau        } elseif ($timeout < 0) {
142504fd306cSNickeau            throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
142604fd306cSNickeau        }
142704fd306cSNickeau
142804fd306cSNickeau        return $timeout;
142904fd306cSNickeau    }
143004fd306cSNickeau
143104fd306cSNickeau    /**
143204fd306cSNickeau     * Reads pipes, executes callback.
143304fd306cSNickeau     *
143404fd306cSNickeau     * @param bool $blocking Whether to use blocking calls or not
143504fd306cSNickeau     * @param bool $close    Whether to close file handles or not
143604fd306cSNickeau     */
143704fd306cSNickeau    private function readPipes(bool $blocking, bool $close)
143804fd306cSNickeau    {
143904fd306cSNickeau        $result = $this->processPipes->readAndWrite($blocking, $close);
144004fd306cSNickeau
144104fd306cSNickeau        $callback = $this->callback;
144204fd306cSNickeau        foreach ($result as $type => $data) {
144304fd306cSNickeau            if (3 !== $type) {
144404fd306cSNickeau                $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data);
144504fd306cSNickeau            } elseif (!isset($this->fallbackStatus['signaled'])) {
144604fd306cSNickeau                $this->fallbackStatus['exitcode'] = (int) $data;
144704fd306cSNickeau            }
144804fd306cSNickeau        }
144904fd306cSNickeau    }
145004fd306cSNickeau
145104fd306cSNickeau    /**
145204fd306cSNickeau     * Closes process resource, closes file handles, sets the exitcode.
145304fd306cSNickeau     *
145404fd306cSNickeau     * @return int The exitcode
145504fd306cSNickeau     */
145604fd306cSNickeau    private function close(): int
145704fd306cSNickeau    {
145804fd306cSNickeau        $this->processPipes->close();
145904fd306cSNickeau        if (\is_resource($this->process)) {
146004fd306cSNickeau            proc_close($this->process);
146104fd306cSNickeau        }
146204fd306cSNickeau        $this->exitcode = $this->processInformation['exitcode'];
146304fd306cSNickeau        $this->status = self::STATUS_TERMINATED;
146404fd306cSNickeau
146504fd306cSNickeau        if (-1 === $this->exitcode) {
146604fd306cSNickeau            if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) {
146704fd306cSNickeau                // if process has been signaled, no exitcode but a valid termsig, apply Unix convention
146804fd306cSNickeau                $this->exitcode = 128 + $this->processInformation['termsig'];
146904fd306cSNickeau            } elseif ($this->isSigchildEnabled()) {
147004fd306cSNickeau                $this->processInformation['signaled'] = true;
147104fd306cSNickeau                $this->processInformation['termsig'] = -1;
147204fd306cSNickeau            }
147304fd306cSNickeau        }
147404fd306cSNickeau
147504fd306cSNickeau        // Free memory from self-reference callback created by buildCallback
147604fd306cSNickeau        // Doing so in other contexts like __destruct or by garbage collector is ineffective
147704fd306cSNickeau        // Now pipes are closed, so the callback is no longer necessary
147804fd306cSNickeau        $this->callback = null;
147904fd306cSNickeau
148004fd306cSNickeau        return $this->exitcode;
148104fd306cSNickeau    }
148204fd306cSNickeau
148304fd306cSNickeau    /**
148404fd306cSNickeau     * Resets data related to the latest run of the process.
148504fd306cSNickeau     */
148604fd306cSNickeau    private function resetProcessData()
148704fd306cSNickeau    {
148804fd306cSNickeau        $this->starttime = null;
148904fd306cSNickeau        $this->callback = null;
149004fd306cSNickeau        $this->exitcode = null;
149104fd306cSNickeau        $this->fallbackStatus = [];
149204fd306cSNickeau        $this->processInformation = null;
149304fd306cSNickeau        $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+');
149404fd306cSNickeau        $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+');
149504fd306cSNickeau        $this->process = null;
149604fd306cSNickeau        $this->latestSignal = null;
149704fd306cSNickeau        $this->status = self::STATUS_READY;
149804fd306cSNickeau        $this->incrementalOutputOffset = 0;
149904fd306cSNickeau        $this->incrementalErrorOutputOffset = 0;
150004fd306cSNickeau    }
150104fd306cSNickeau
150204fd306cSNickeau    /**
150304fd306cSNickeau     * Sends a POSIX signal to the process.
150404fd306cSNickeau     *
150504fd306cSNickeau     * @param int  $signal         A valid POSIX signal (see https://php.net/pcntl.constants)
150604fd306cSNickeau     * @param bool $throwException Whether to throw exception in case signal failed
150704fd306cSNickeau     *
150804fd306cSNickeau     * @throws LogicException   In case the process is not running
150904fd306cSNickeau     * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
151004fd306cSNickeau     * @throws RuntimeException In case of failure
151104fd306cSNickeau     */
151204fd306cSNickeau    private function doSignal(int $signal, bool $throwException): bool
151304fd306cSNickeau    {
151404fd306cSNickeau        if (null === $pid = $this->getPid()) {
151504fd306cSNickeau            if ($throwException) {
151604fd306cSNickeau                throw new LogicException('Cannot send signal on a non running process.');
151704fd306cSNickeau            }
151804fd306cSNickeau
151904fd306cSNickeau            return false;
152004fd306cSNickeau        }
152104fd306cSNickeau
152204fd306cSNickeau        if ('\\' === \DIRECTORY_SEPARATOR) {
152304fd306cSNickeau            exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode);
152404fd306cSNickeau            if ($exitCode && $this->isRunning()) {
152504fd306cSNickeau                if ($throwException) {
152604fd306cSNickeau                    throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output)));
152704fd306cSNickeau                }
152804fd306cSNickeau
152904fd306cSNickeau                return false;
153004fd306cSNickeau            }
153104fd306cSNickeau        } else {
153204fd306cSNickeau            if (!$this->isSigchildEnabled()) {
153304fd306cSNickeau                $ok = @proc_terminate($this->process, $signal);
153404fd306cSNickeau            } elseif (\function_exists('posix_kill')) {
153504fd306cSNickeau                $ok = @posix_kill($pid, $signal);
153604fd306cSNickeau            } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) {
153704fd306cSNickeau                $ok = false === fgets($pipes[2]);
153804fd306cSNickeau            }
153904fd306cSNickeau            if (!$ok) {
154004fd306cSNickeau                if ($throwException) {
154104fd306cSNickeau                    throw new RuntimeException(sprintf('Error while sending signal "%s".', $signal));
154204fd306cSNickeau                }
154304fd306cSNickeau
154404fd306cSNickeau                return false;
154504fd306cSNickeau            }
154604fd306cSNickeau        }
154704fd306cSNickeau
154804fd306cSNickeau        $this->latestSignal = $signal;
154904fd306cSNickeau        $this->fallbackStatus['signaled'] = true;
155004fd306cSNickeau        $this->fallbackStatus['exitcode'] = -1;
155104fd306cSNickeau        $this->fallbackStatus['termsig'] = $this->latestSignal;
155204fd306cSNickeau
155304fd306cSNickeau        return true;
155404fd306cSNickeau    }
155504fd306cSNickeau
155604fd306cSNickeau    private function prepareWindowsCommandLine(string $cmd, array &$env): string
155704fd306cSNickeau    {
155804fd306cSNickeau        $uid = uniqid('', true);
155904fd306cSNickeau        $varCount = 0;
156004fd306cSNickeau        $varCache = [];
156104fd306cSNickeau        $cmd = preg_replace_callback(
156204fd306cSNickeau            '/"(?:(
156304fd306cSNickeau                [^"%!^]*+
156404fd306cSNickeau                (?:
156504fd306cSNickeau                    (?: !LF! | "(?:\^[%!^])?+" )
156604fd306cSNickeau                    [^"%!^]*+
156704fd306cSNickeau                )++
156804fd306cSNickeau            ) | [^"]*+ )"/x',
156904fd306cSNickeau            function ($m) use (&$env, &$varCache, &$varCount, $uid) {
157004fd306cSNickeau                if (!isset($m[1])) {
157104fd306cSNickeau                    return $m[0];
157204fd306cSNickeau                }
157304fd306cSNickeau                if (isset($varCache[$m[0]])) {
157404fd306cSNickeau                    return $varCache[$m[0]];
157504fd306cSNickeau                }
157604fd306cSNickeau                if (str_contains($value = $m[1], "\0")) {
157704fd306cSNickeau                    $value = str_replace("\0", '?', $value);
157804fd306cSNickeau                }
157904fd306cSNickeau                if (false === strpbrk($value, "\"%!\n")) {
158004fd306cSNickeau                    return '"'.$value.'"';
158104fd306cSNickeau                }
158204fd306cSNickeau
158304fd306cSNickeau                $value = str_replace(['!LF!', '"^!"', '"^%"', '"^^"', '""'], ["\n", '!', '%', '^', '"'], $value);
158404fd306cSNickeau                $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"';
158504fd306cSNickeau                $var = $uid.++$varCount;
158604fd306cSNickeau
158704fd306cSNickeau                $env[$var] = $value;
158804fd306cSNickeau
158904fd306cSNickeau                return $varCache[$m[0]] = '!'.$var.'!';
159004fd306cSNickeau            },
159104fd306cSNickeau            $cmd
159204fd306cSNickeau        );
159304fd306cSNickeau
159404fd306cSNickeau        $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')';
159504fd306cSNickeau        foreach ($this->processPipes->getFiles() as $offset => $filename) {
159604fd306cSNickeau            $cmd .= ' '.$offset.'>"'.$filename.'"';
159704fd306cSNickeau        }
159804fd306cSNickeau
159904fd306cSNickeau        return $cmd;
160004fd306cSNickeau    }
160104fd306cSNickeau
160204fd306cSNickeau    /**
160304fd306cSNickeau     * Ensures the process is running or terminated, throws a LogicException if the process has a not started.
160404fd306cSNickeau     *
160504fd306cSNickeau     * @throws LogicException if the process has not run
160604fd306cSNickeau     */
160704fd306cSNickeau    private function requireProcessIsStarted(string $functionName)
160804fd306cSNickeau    {
160904fd306cSNickeau        if (!$this->isStarted()) {
161004fd306cSNickeau            throw new LogicException(sprintf('Process must be started before calling "%s()".', $functionName));
161104fd306cSNickeau        }
161204fd306cSNickeau    }
161304fd306cSNickeau
161404fd306cSNickeau    /**
161504fd306cSNickeau     * Ensures the process is terminated, throws a LogicException if the process has a status different than "terminated".
161604fd306cSNickeau     *
161704fd306cSNickeau     * @throws LogicException if the process is not yet terminated
161804fd306cSNickeau     */
161904fd306cSNickeau    private function requireProcessIsTerminated(string $functionName)
162004fd306cSNickeau    {
162104fd306cSNickeau        if (!$this->isTerminated()) {
162204fd306cSNickeau            throw new LogicException(sprintf('Process must be terminated before calling "%s()".', $functionName));
162304fd306cSNickeau        }
162404fd306cSNickeau    }
162504fd306cSNickeau
162604fd306cSNickeau    /**
162704fd306cSNickeau     * Escapes a string to be used as a shell argument.
162804fd306cSNickeau     */
162904fd306cSNickeau    private function escapeArgument(?string $argument): string
163004fd306cSNickeau    {
163104fd306cSNickeau        if ('' === $argument || null === $argument) {
163204fd306cSNickeau            return '""';
163304fd306cSNickeau        }
163404fd306cSNickeau        if ('\\' !== \DIRECTORY_SEPARATOR) {
163504fd306cSNickeau            return "'".str_replace("'", "'\\''", $argument)."'";
163604fd306cSNickeau        }
163704fd306cSNickeau        if (str_contains($argument, "\0")) {
163804fd306cSNickeau            $argument = str_replace("\0", '?', $argument);
163904fd306cSNickeau        }
164004fd306cSNickeau        if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) {
164104fd306cSNickeau            return $argument;
164204fd306cSNickeau        }
164304fd306cSNickeau        $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument);
164404fd306cSNickeau
164504fd306cSNickeau        return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"';
164604fd306cSNickeau    }
164704fd306cSNickeau
164804fd306cSNickeau    private function replacePlaceholders(string $commandline, array $env)
164904fd306cSNickeau    {
165004fd306cSNickeau        return preg_replace_callback('/"\$\{:([_a-zA-Z]++[_a-zA-Z0-9]*+)\}"/', function ($matches) use ($commandline, $env) {
165104fd306cSNickeau            if (!isset($env[$matches[1]]) || false === $env[$matches[1]]) {
165204fd306cSNickeau                throw new InvalidArgumentException(sprintf('Command line is missing a value for parameter "%s": ', $matches[1]).$commandline);
165304fd306cSNickeau            }
165404fd306cSNickeau
165504fd306cSNickeau            return $this->escapeArgument($env[$matches[1]]);
165604fd306cSNickeau        }, $commandline);
165704fd306cSNickeau    }
165804fd306cSNickeau
165904fd306cSNickeau    private function getDefaultEnv(): array
166004fd306cSNickeau    {
166104fd306cSNickeau        $env = getenv();
166204fd306cSNickeau        $env = ('\\' === \DIRECTORY_SEPARATOR ? array_intersect_ukey($env, $_SERVER, 'strcasecmp') : array_intersect_key($env, $_SERVER)) ?: $env;
166304fd306cSNickeau
166404fd306cSNickeau        return $_ENV + ('\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($env, $_ENV, 'strcasecmp') : $env);
166504fd306cSNickeau    }
166604fd306cSNickeau}
1667