xref: /plugin/combo/vendor/symfony/process/Process.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
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