GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( 5cefd1...492078 )
by Anton
04:08
created

Process::clearErrorOutput()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
nc 1
nop 0
dl 0
loc 7
c 1
b 0
f 0
cc 1
rs 10
1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\Process;
13
14
use Symfony\Component\Process\Exception\InvalidArgumentException;
15
use Symfony\Component\Process\Exception\LogicException;
16
use Symfony\Component\Process\Exception\ProcessFailedException;
17
use Symfony\Component\Process\Exception\ProcessSignaledException;
18
use Symfony\Component\Process\Exception\ProcessTimedOutException;
19
use Symfony\Component\Process\Exception\RuntimeException;
20
use Symfony\Component\Process\Pipes\PipesInterface;
21
use Symfony\Component\Process\Pipes\UnixPipes;
22
use Symfony\Component\Process\Pipes\WindowsPipes;
23
24
/**
25
 * Process is a thin wrapper around proc_* functions to easily
26
 * start independent PHP processes.
27
 *
28
 * @author Fabien Potencier <[email protected]>
29
 * @author Romain Neutron <[email protected]>
30
 *
31
 * @implements \IteratorAggregate<string, string>
32
 */
33
class Process implements \IteratorAggregate
34
{
35
    public const ERR = 'err';
36
    public const OUT = 'out';
37
38
    public const STATUS_READY = 'ready';
39
    public const STATUS_STARTED = 'started';
40
    public const STATUS_TERMINATED = 'terminated';
41
42
    public const STDIN = 0;
43
    public const STDOUT = 1;
44
    public const STDERR = 2;
45
46
    // Timeout Precision in seconds.
47
    public const TIMEOUT_PRECISION = 0.2;
48
49
    public const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking
50
    public const ITER_KEEP_OUTPUT = 2;  // By default, outputs are cleared while iterating, use this flag to keep them in memory
51
    public const ITER_SKIP_OUT = 4;     // Use this flag to skip STDOUT while iterating
52
    public const ITER_SKIP_ERR = 8;     // Use this flag to skip STDERR while iterating
53
54
    private $callback;
55
    private $hasCallback = false;
56
    private $commandline;
57
    private $cwd;
58
    private $env;
59
    private $input;
60
    private $starttime;
61
    private $lastOutputTime;
62
    private $timeout;
63
    private $idleTimeout;
64
    private $exitcode;
65
    private $fallbackStatus = [];
66
    private $processInformation;
67
    private $outputDisabled = false;
68
    private $stdout;
69
    private $stderr;
70
    private $process;
71
    private $status = self::STATUS_READY;
72
    private $incrementalOutputOffset = 0;
73
    private $incrementalErrorOutputOffset = 0;
74
    private $tty = false;
75
    private $pty;
76
    private $options = ['suppress_errors' => true, 'bypass_shell' => true];
77
78
    private $useFileHandles = false;
79
    /** @var PipesInterface */
80
    private $processPipes;
81
82
    private $latestSignal;
83
84
    private static $sigchild;
85
86
    /**
87
     * Exit codes translation table.
88
     *
89
     * User-defined errors must use exit codes in the 64-113 range.
90
     */
91
    public static $exitCodes = [
92
        0 => 'OK',
93
        1 => 'General error',
94
        2 => 'Misuse of shell builtins',
95
96
        126 => 'Invoked command cannot execute',
97
        127 => 'Command not found',
98
        128 => 'Invalid exit argument',
99
100
        // signals
101
        129 => 'Hangup',
102
        130 => 'Interrupt',
103
        131 => 'Quit and dump core',
104
        132 => 'Illegal instruction',
105
        133 => 'Trace/breakpoint trap',
106
        134 => 'Process aborted',
107
        135 => 'Bus error: "access to undefined portion of memory object"',
108
        136 => 'Floating point exception: "erroneous arithmetic operation"',
109
        137 => 'Kill (terminate immediately)',
110
        138 => 'User-defined 1',
111
        139 => 'Segmentation violation',
112
        140 => 'User-defined 2',
113
        141 => 'Write to pipe with no one reading',
114
        142 => 'Signal raised by alarm',
115
        143 => 'Termination (request to terminate)',
116
        // 144 - not defined
117
        145 => 'Child process terminated, stopped (or continued*)',
118
        146 => 'Continue if stopped',
119
        147 => 'Stop executing temporarily',
120
        148 => 'Terminal stop signal',
121
        149 => 'Background process attempting to read from tty ("in")',
122
        150 => 'Background process attempting to write to tty ("out")',
123
        151 => 'Urgent data available on socket',
124
        152 => 'CPU time limit exceeded',
125
        153 => 'File size limit exceeded',
126
        154 => 'Signal raised by timer counting virtual time: "virtual timer expired"',
127
        155 => 'Profiling timer expired',
128
        // 156 - not defined
129
        157 => 'Pollable event',
130
        // 158 - not defined
131
        159 => 'Bad syscall',
132
    ];
133
134
    /**
135
     * @param array          $command The command to run and its arguments listed as separate entries
136
     * @param string|null    $cwd     The working directory or null to use the working dir of the current PHP process
137
     * @param array|null     $env     The environment variables or null to use the same environment as the current PHP process
138
     * @param mixed          $input   The input as stream resource, scalar or \Traversable, or null for no input
139
     * @param int|float|null $timeout The timeout in seconds or null to disable
140
     *
141
     * @throws LogicException When proc_open is not installed
142
     */
143
    public function __construct(array $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60)
144
    {
145
        if (!\function_exists('proc_open')) {
146
            throw new LogicException('The Process class relies on proc_open, which is not available on your PHP installation.');
147
        }
148
149
        $this->commandline = $command;
150
        $this->cwd = $cwd;
151
152
        // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started
153
        // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected
154
        // @see : https://bugs.php.net/51800
155
        // @see : https://bugs.php.net/50524
156
        if (null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR)) {
157
            $this->cwd = getcwd();
158
        }
159
        if (null !== $env) {
160
            $this->setEnv($env);
161
        }
162
163
        $this->setInput($input);
164
        $this->setTimeout($timeout);
165
        $this->useFileHandles = '\\' === \DIRECTORY_SEPARATOR;
166
        $this->pty = false;
167
    }
168
169
    /**
170
     * Creates a Process instance as a command-line to be run in a shell wrapper.
171
     *
172
     * Command-lines are parsed by the shell of your OS (/bin/sh on Unix-like, cmd.exe on Windows.)
173
     * This allows using e.g. pipes or conditional execution. In this mode, signals are sent to the
174
     * shell wrapper and not to your commands.
175
     *
176
     * In order to inject dynamic values into command-lines, we strongly recommend using placeholders.
177
     * This will save escaping values, which is not portable nor secure anyway:
178
     *
179
     *   $process = Process::fromShellCommandline('my_command "${:MY_VAR}"');
180
     *   $process->run(null, ['MY_VAR' => $theValue]);
181
     *
182
     * @param string         $command The command line to pass to the shell of the OS
183
     * @param string|null    $cwd     The working directory or null to use the working dir of the current PHP process
184
     * @param array|null     $env     The environment variables or null to use the same environment as the current PHP process
185
     * @param mixed          $input   The input as stream resource, scalar or \Traversable, or null for no input
186
     * @param int|float|null $timeout The timeout in seconds or null to disable
187
     *
188
     * @return static
189
     *
190
     * @throws LogicException When proc_open is not installed
191
     */
192
    public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60)
193
    {
194
        $process = new static([], $cwd, $env, $input, $timeout);
195
        $process->commandline = $command;
196
197
        return $process;
198
    }
199
200
    /**
201
     * @return array
202
     */
203
    public function __sleep()
204
    {
205
        throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
206
    }
207
208
    public function __wakeup()
209
    {
210
        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
211
    }
212
213
    public function __destruct()
214
    {
215
        if ($this->options['create_new_console'] ?? false) {
216
            $this->processPipes->close();
217
        } else {
218
            $this->stop(0);
219
        }
220
    }
221
222
    public function __clone()
223
    {
224
        $this->resetProcessData();
225
    }
226
227
    /**
228
     * Runs the process.
229
     *
230
     * The callback receives the type of output (out or err) and
231
     * some bytes from the output in real-time. It allows to have feedback
232
     * from the independent process during execution.
233
     *
234
     * The STDOUT and STDERR are also available after the process is finished
235
     * via the getOutput() and getErrorOutput() methods.
236
     *
237
     * @param callable|null $callback A PHP callback to run whenever there is some
238
     *                                output available on STDOUT or STDERR
239
     *
240
     * @return int The exit status code
241
     *
242
     * @throws RuntimeException         When process can't be launched
243
     * @throws RuntimeException         When process is already running
244
     * @throws ProcessTimedOutException When process timed out
245
     * @throws ProcessSignaledException When process stopped after receiving signal
246
     * @throws LogicException           In case a callback is provided and output has been disabled
247
     *
248
     * @final
249
     */
250
    public function run(callable $callback = null, array $env = []): int
251
    {
252
        $this->start($callback, $env);
253
254
        return $this->wait();
255
    }
256
257
    /**
258
     * Runs the process.
259
     *
260
     * This is identical to run() except that an exception is thrown if the process
261
     * exits with a non-zero exit code.
262
     *
263
     * @return $this
264
     *
265
     * @throws ProcessFailedException if the process didn't terminate successfully
266
     *
267
     * @final
268
     */
269
    public function mustRun(callable $callback = null, array $env = []): self
270
    {
271
        if (0 !== $this->run($callback, $env)) {
272
            throw new ProcessFailedException($this);
273
        }
274
275
        return $this;
276
    }
277
278
    /**
279
     * Starts the process and returns after writing the input to STDIN.
280
     *
281
     * This method blocks until all STDIN data is sent to the process then it
282
     * returns while the process runs in the background.
283
     *
284
     * The termination of the process can be awaited with wait().
285
     *
286
     * The callback receives the type of output (out or err) and some bytes from
287
     * the output in real-time while writing the standard input to the process.
288
     * It allows to have feedback from the independent process during execution.
289
     *
290
     * @param callable|null $callback A PHP callback to run whenever there is some
291
     *                                output available on STDOUT or STDERR
292
     *
293
     * @throws RuntimeException When process can't be launched
294
     * @throws RuntimeException When process is already running
295
     * @throws LogicException   In case a callback is provided and output has been disabled
296
     */
297
    public function start(callable $callback = null, array $env = [])
298
    {
299
        if ($this->isRunning()) {
300
            throw new RuntimeException('Process is already running.');
301
        }
302
303
        $this->resetProcessData();
304
        $this->starttime = $this->lastOutputTime = microtime(true);
305
        $this->callback = $this->buildCallback($callback);
306
        $this->hasCallback = null !== $callback;
307
        $descriptors = $this->getDescriptors();
0 ignored issues
show
Bug introduced by
The call to Symfony\Component\Proces...ocess::getDescriptors() has too few arguments starting with hasCallback. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

307
        /** @scrutinizer ignore-call */ 
308
        $descriptors = $this->getDescriptors();

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
308
309
        if ($this->env) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->env of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
310
            $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->env, $env, 'strcasecmp') : $this->env;
311
        }
312
313
        $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->getDefaultEnv(), $env, 'strcasecmp') : $this->getDefaultEnv();
314
315
        if (\is_array($commandline = $this->commandline)) {
316
            $commandline = implode(' ', array_map([$this, 'escapeArgument'], $commandline));
317
318
            if ('\\' !== \DIRECTORY_SEPARATOR) {
319
                // exec is mandatory to deal with sending a signal to the process
320
                $commandline = 'exec '.$commandline;
321
            }
322
        } else {
323
            $commandline = $this->replacePlaceholders($commandline, $env);
324
        }
325
326
        if ('\\' === \DIRECTORY_SEPARATOR) {
327
            $commandline = $this->prepareWindowsCommandLine($commandline, $env);
328
        } elseif (!$this->useFileHandles && $this->isSigchildEnabled()) {
0 ignored issues
show
Bug Best Practice introduced by
The property useFileHandles does not exist on Symfony\Component\Process\Process. Did you maybe forget to declare it?
Loading history...
329
            // last exit code is output on the fourth pipe and caught to work around --enable-sigchild
330
            $descriptors[3] = ['pipe', 'w'];
331
332
            // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input
333
            $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;';
334
            $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code';
335
336
            // Workaround for the bug, when PTS functionality is enabled.
337
            // @see : https://bugs.php.net/69442
338
            $ptsWorkaround = fopen(__FILE__, 'r');
0 ignored issues
show
Unused Code introduced by
The assignment to $ptsWorkaround is dead and can be removed.
Loading history...
339
        }
340
341
        $envPairs = [];
342
        foreach ($env as $k => $v) {
343
            if (false !== $v && false === \in_array($k, ['argc', 'argv', 'ARGC', 'ARGV'], true)) {
344
                $envPairs[] = $k.'='.$v;
345
            }
346
        }
347
348
        if (!is_dir($this->cwd)) {
0 ignored issues
show
Bug introduced by
It seems like $this->cwd can also be of type null; however, parameter $filename of is_dir() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

348
        if (!is_dir(/** @scrutinizer ignore-type */ $this->cwd)) {
Loading history...
349
            throw new RuntimeException(sprintf('The provided cwd "%s" does not exist.', $this->cwd));
350
        }
351
352
        $this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options);
0 ignored issues
show
Documentation Bug introduced by
It seems like @proc_open($commandline,...vPairs, $this->options) can also be of type false. However, the property $process is declared as type null|resource. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
353
354
        if (!\is_resource($this->process)) {
355
            throw new RuntimeException('Unable to launch a new process.');
356
        }
357
        $this->status = self::STATUS_STARTED;
358
359
        if (isset($descriptors[3])) {
360
            $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]);
361
        }
362
363
        if ($this->tty) {
364
            return;
365
        }
366
367
        $this->updateStatus(false);
368
        $this->checkTimeout();
369
    }
370
371
    /**
372
     * Restarts the process.
373
     *
374
     * Be warned that the process is cloned before being started.
375
     *
376
     * @param callable|null $callback A PHP callback to run whenever there is some
377
     *                                output available on STDOUT or STDERR
378
     *
379
     * @return static
380
     *
381
     * @throws RuntimeException When process can't be launched
382
     * @throws RuntimeException When process is already running
383
     *
384
     * @see start()
385
     *
386
     * @final
387
     */
388
    public function restart(callable $callback = null, array $env = []): self
389
    {
390
        if ($this->isRunning()) {
391
            throw new RuntimeException('Process is already running.');
392
        }
393
394
        $process = clone $this;
395
        $process->start($callback, $env);
396
397
        return $process;
398
    }
399
400
    /**
401
     * Waits for the process to terminate.
402
     *
403
     * The callback receives the type of output (out or err) and some bytes
404
     * from the output in real-time while writing the standard input to the process.
405
     * It allows to have feedback from the independent process during execution.
406
     *
407
     * @param callable|null $callback A valid PHP callback
408
     *
409
     * @return int The exitcode of the process
410
     *
411
     * @throws ProcessTimedOutException When process timed out
412
     * @throws ProcessSignaledException When process stopped after receiving signal
413
     * @throws LogicException           When process is not yet started
414
     */
415
    public function wait(callable $callback = null)
416
    {
417
        $this->requireProcessIsStarted(__FUNCTION__);
418
419
        $this->updateStatus(false);
420
421
        if (null !== $callback) {
422
            if (!$this->processPipes->haveReadSupport()) {
423
                $this->stop(0);
424
                throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait".');
425
            }
426
            $this->callback = $this->buildCallback($callback);
427
        }
428
429
        do {
430
            $this->checkTimeout();
431
            $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
432
            $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
433
        } while ($running);
434
435
        while ($this->isRunning()) {
436
            $this->checkTimeout();
437
            usleep(1000);
438
        }
439
440
        if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) {
441
            throw new ProcessSignaledException($this);
442
        }
443
444
        return $this->exitcode;
445
    }
446
447
    /**
448
     * Waits until the callback returns true.
449
     *
450
     * The callback receives the type of output (out or err) and some bytes
451
     * from the output in real-time while writing the standard input to the process.
452
     * It allows to have feedback from the independent process during execution.
453
     *
454
     * @throws RuntimeException         When process timed out
455
     * @throws LogicException           When process is not yet started
456
     * @throws ProcessTimedOutException In case the timeout was reached
457
     */
458
    public function waitUntil(callable $callback): bool
459
    {
460
        $this->requireProcessIsStarted(__FUNCTION__);
461
        $this->updateStatus(false);
462
463
        if (!$this->processPipes->haveReadSupport()) {
464
            $this->stop(0);
465
            throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::waitUntil".');
466
        }
467
        $callback = $this->buildCallback($callback);
468
469
        $ready = false;
470
        while (true) {
471
            $this->checkTimeout();
472
            $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
473
            $output = $this->processPipes->readAndWrite($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
474
475
            foreach ($output as $type => $data) {
476
                if (3 !== $type) {
477
                    $ready = $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data) || $ready;
478
                } elseif (!isset($this->fallbackStatus['signaled'])) {
479
                    $this->fallbackStatus['exitcode'] = (int) $data;
480
                }
481
            }
482
            if ($ready) {
483
                return true;
484
            }
485
            if (!$running) {
486
                return false;
487
            }
488
489
            usleep(1000);
490
        }
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return boolean. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
491
    }
492
493
    /**
494
     * Returns the Pid (process identifier), if applicable.
495
     *
496
     * @return int|null The process id if running, null otherwise
497
     */
498
    public function getPid()
499
    {
500
        return $this->isRunning() ? $this->processInformation['pid'] : null;
501
    }
502
503
    /**
504
     * Sends a POSIX signal to the process.
505
     *
506
     * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants)
507
     *
508
     * @return $this
509
     *
510
     * @throws LogicException   In case the process is not running
511
     * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
512
     * @throws RuntimeException In case of failure
513
     */
514
    public function signal(int $signal)
515
    {
516
        $this->doSignal($signal, true);
517
518
        return $this;
519
    }
520
521
    /**
522
     * Disables fetching output and error output from the underlying process.
523
     *
524
     * @return $this
525
     *
526
     * @throws RuntimeException In case the process is already running
527
     * @throws LogicException   if an idle timeout is set
528
     */
529
    public function disableOutput()
530
    {
531
        if ($this->isRunning()) {
532
            throw new RuntimeException('Disabling output while the process is running is not possible.');
533
        }
534
        if (null !== $this->idleTimeout) {
535
            throw new LogicException('Output cannot be disabled while an idle timeout is set.');
536
        }
537
538
        $this->outputDisabled = true;
539
540
        return $this;
541
    }
542
543
    /**
544
     * Enables fetching output and error output from the underlying process.
545
     *
546
     * @return $this
547
     *
548
     * @throws RuntimeException In case the process is already running
549
     */
550
    public function enableOutput()
551
    {
552
        if ($this->isRunning()) {
553
            throw new RuntimeException('Enabling output while the process is running is not possible.');
554
        }
555
556
        $this->outputDisabled = false;
557
558
        return $this;
559
    }
560
561
    /**
562
     * Returns true in case the output is disabled, false otherwise.
563
     *
564
     * @return bool
565
     */
566
    public function isOutputDisabled()
567
    {
568
        return $this->outputDisabled;
569
    }
570
571
    /**
572
     * Returns the current output of the process (STDOUT).
573
     *
574
     * @return string
575
     *
576
     * @throws LogicException in case the output has been disabled
577
     * @throws LogicException In case the process is not started
578
     */
579
    public function getOutput()
580
    {
581
        $this->readPipesForOutput(__FUNCTION__);
582
583
        if (false === $ret = stream_get_contents($this->stdout, -1, 0)) {
584
            return '';
585
        }
586
587
        return $ret;
588
    }
589
590
    /**
591
     * Returns the output incrementally.
592
     *
593
     * In comparison with the getOutput method which always return the whole
594
     * output, this one returns the new output since the last call.
595
     *
596
     * @return string
597
     *
598
     * @throws LogicException in case the output has been disabled
599
     * @throws LogicException In case the process is not started
600
     */
601
    public function getIncrementalOutput()
602
    {
603
        $this->readPipesForOutput(__FUNCTION__);
604
605
        $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
606
        $this->incrementalOutputOffset = ftell($this->stdout);
607
608
        if (false === $latest) {
609
            return '';
610
        }
611
612
        return $latest;
613
    }
614
615
    /**
616
     * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR).
617
     *
618
     * @param int $flags A bit field of Process::ITER_* flags
619
     *
620
     * @throws LogicException in case the output has been disabled
621
     * @throws LogicException In case the process is not started
622
     *
623
     * @return \Generator<string, string>
624
     */
625
    #[\ReturnTypeWillChange]
626
    public function getIterator(int $flags = 0)
627
    {
628
        $this->readPipesForOutput(__FUNCTION__, false);
629
630
        $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags);
631
        $blocking = !(self::ITER_NON_BLOCKING & $flags);
632
        $yieldOut = !(self::ITER_SKIP_OUT & $flags);
633
        $yieldErr = !(self::ITER_SKIP_ERR & $flags);
634
635
        while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) {
636
            if ($yieldOut) {
637
                $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
638
639
                if (isset($out[0])) {
640
                    if ($clearOutput) {
641
                        $this->clearOutput();
642
                    } else {
643
                        $this->incrementalOutputOffset = ftell($this->stdout);
644
                    }
645
646
                    yield self::OUT => $out;
647
                }
648
            }
649
650
            if ($yieldErr) {
651
                $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
652
653
                if (isset($err[0])) {
654
                    if ($clearOutput) {
655
                        $this->clearErrorOutput();
656
                    } else {
657
                        $this->incrementalErrorOutputOffset = ftell($this->stderr);
658
                    }
659
660
                    yield self::ERR => $err;
661
                }
662
            }
663
664
            if (!$blocking && !isset($out[0]) && !isset($err[0])) {
665
                yield self::OUT => '';
666
            }
667
668
            $this->checkTimeout();
669
            $this->readPipesForOutput(__FUNCTION__, $blocking);
670
        }
671
    }
672
673
    /**
674
     * Clears the process output.
675
     *
676
     * @return $this
677
     */
678
    public function clearOutput()
679
    {
680
        ftruncate($this->stdout, 0);
681
        fseek($this->stdout, 0);
682
        $this->incrementalOutputOffset = 0;
683
684
        return $this;
685
    }
686
687
    /**
688
     * Returns the current error output of the process (STDERR).
689
     *
690
     * @return string
691
     *
692
     * @throws LogicException in case the output has been disabled
693
     * @throws LogicException In case the process is not started
694
     */
695
    public function getErrorOutput()
696
    {
697
        $this->readPipesForOutput(__FUNCTION__);
698
699
        if (false === $ret = stream_get_contents($this->stderr, -1, 0)) {
700
            return '';
701
        }
702
703
        return $ret;
704
    }
705
706
    /**
707
     * Returns the errorOutput incrementally.
708
     *
709
     * In comparison with the getErrorOutput method which always return the
710
     * whole error output, this one returns the new error output since the last
711
     * call.
712
     *
713
     * @return string
714
     *
715
     * @throws LogicException in case the output has been disabled
716
     * @throws LogicException In case the process is not started
717
     */
718
    public function getIncrementalErrorOutput()
719
    {
720
        $this->readPipesForOutput(__FUNCTION__);
721
722
        $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
723
        $this->incrementalErrorOutputOffset = ftell($this->stderr);
724
725
        if (false === $latest) {
726
            return '';
727
        }
728
729
        return $latest;
730
    }
731
732
    /**
733
     * Clears the process output.
734
     *
735
     * @return $this
736
     */
737
    public function clearErrorOutput()
738
    {
739
        ftruncate($this->stderr, 0);
740
        fseek($this->stderr, 0);
741
        $this->incrementalErrorOutputOffset = 0;
742
743
        return $this;
744
    }
745
746
    /**
747
     * Returns the exit code returned by the process.
748
     *
749
     * @return int|null The exit status code, null if the Process is not terminated
750
     */
751
    public function getExitCode()
752
    {
753
        $this->updateStatus(false);
754
755
        return $this->exitcode;
756
    }
757
758
    /**
759
     * Returns a string representation for the exit code returned by the process.
760
     *
761
     * This method relies on the Unix exit code status standardization
762
     * and might not be relevant for other operating systems.
763
     *
764
     * @return string|null A string representation for the exit status code, null if the Process is not terminated
765
     *
766
     * @see http://tldp.org/LDP/abs/html/exitcodes.html
767
     * @see http://en.wikipedia.org/wiki/Unix_signal
768
     */
769
    public function getExitCodeText()
770
    {
771
        if (null === $exitcode = $this->getExitCode()) {
772
            return null;
773
        }
774
775
        return self::$exitCodes[$exitcode] ?? 'Unknown error';
776
    }
777
778
    /**
779
     * Checks if the process ended successfully.
780
     *
781
     * @return bool
782
     */
783
    public function isSuccessful()
784
    {
785
        return 0 === $this->getExitCode();
786
    }
787
788
    /**
789
     * Returns true if the child process has been terminated by an uncaught signal.
790
     *
791
     * It always returns false on Windows.
792
     *
793
     * @return bool
794
     *
795
     * @throws LogicException In case the process is not terminated
796
     */
797
    public function hasBeenSignaled()
798
    {
799
        $this->requireProcessIsTerminated(__FUNCTION__);
800
801
        return $this->processInformation['signaled'];
802
    }
803
804
    /**
805
     * Returns the number of the signal that caused the child process to terminate its execution.
806
     *
807
     * It is only meaningful if hasBeenSignaled() returns true.
808
     *
809
     * @return int
810
     *
811
     * @throws RuntimeException In case --enable-sigchild is activated
812
     * @throws LogicException   In case the process is not terminated
813
     */
814
    public function getTermSignal()
815
    {
816
        $this->requireProcessIsTerminated(__FUNCTION__);
817
818
        if ($this->isSigchildEnabled() && -1 === $this->processInformation['termsig']) {
819
            throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal cannot be retrieved.');
820
        }
821
822
        return $this->processInformation['termsig'];
823
    }
824
825
    /**
826
     * Returns true if the child process has been stopped by a signal.
827
     *
828
     * It always returns false on Windows.
829
     *
830
     * @return bool
831
     *
832
     * @throws LogicException In case the process is not terminated
833
     */
834
    public function hasBeenStopped()
835
    {
836
        $this->requireProcessIsTerminated(__FUNCTION__);
837
838
        return $this->processInformation['stopped'];
839
    }
840
841
    /**
842
     * Returns the number of the signal that caused the child process to stop its execution.
843
     *
844
     * It is only meaningful if hasBeenStopped() returns true.
845
     *
846
     * @return int
847
     *
848
     * @throws LogicException In case the process is not terminated
849
     */
850
    public function getStopSignal()
851
    {
852
        $this->requireProcessIsTerminated(__FUNCTION__);
853
854
        return $this->processInformation['stopsig'];
855
    }
856
857
    /**
858
     * Checks if the process is currently running.
859
     *
860
     * @return bool
861
     */
862
    public function isRunning()
863
    {
864
        if (self::STATUS_STARTED !== $this->status) {
865
            return false;
866
        }
867
868
        $this->updateStatus(false);
869
870
        return $this->processInformation['running'];
871
    }
872
873
    /**
874
     * Checks if the process has been started with no regard to the current state.
875
     *
876
     * @return bool
877
     */
878
    public function isStarted()
879
    {
880
        return self::STATUS_READY != $this->status;
881
    }
882
883
    /**
884
     * Checks if the process is terminated.
885
     *
886
     * @return bool
887
     */
888
    public function isTerminated()
889
    {
890
        $this->updateStatus(false);
891
892
        return self::STATUS_TERMINATED == $this->status;
893
    }
894
895
    /**
896
     * Gets the process status.
897
     *
898
     * The status is one of: ready, started, terminated.
899
     *
900
     * @return string
901
     */
902
    public function getStatus()
903
    {
904
        $this->updateStatus(false);
905
906
        return $this->status;
907
    }
908
909
    /**
910
     * Stops the process.
911
     *
912
     * @param int|float $timeout The timeout in seconds
913
     * @param int       $signal  A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9)
914
     *
915
     * @return int|null The exit-code of the process or null if it's not running
916
     */
917
    public function stop(float $timeout = 10, int $signal = null)
918
    {
919
        $timeoutMicro = microtime(true) + $timeout;
920
        if ($this->isRunning()) {
921
            // 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
            $this->doSignal(15, false);
923
            do {
924
                usleep(1000);
925
            } while ($this->isRunning() && microtime(true) < $timeoutMicro);
926
927
            if ($this->isRunning()) {
928
                // Avoid exception here: process is supposed to be running, but it might have stopped just
929
                // after this line. In any case, let's silently discard the error, we cannot do anything.
930
                $this->doSignal($signal ?: 9, false);
931
            }
932
        }
933
934
        if ($this->isRunning()) {
935
            if (isset($this->fallbackStatus['pid'])) {
936
                unset($this->fallbackStatus['pid']);
937
938
                return $this->stop(0, $signal);
939
            }
940
            $this->close();
941
        }
942
943
        return $this->exitcode;
944
    }
945
946
    /**
947
     * Adds a line to the STDOUT stream.
948
     *
949
     * @internal
950
     */
951
    public function addOutput(string $line)
952
    {
953
        $this->lastOutputTime = microtime(true);
954
955
        fseek($this->stdout, 0, \SEEK_END);
956
        fwrite($this->stdout, $line);
957
        fseek($this->stdout, $this->incrementalOutputOffset);
958
    }
959
960
    /**
961
     * Adds a line to the STDERR stream.
962
     *
963
     * @internal
964
     */
965
    public function addErrorOutput(string $line)
966
    {
967
        $this->lastOutputTime = microtime(true);
968
969
        fseek($this->stderr, 0, \SEEK_END);
970
        fwrite($this->stderr, $line);
971
        fseek($this->stderr, $this->incrementalErrorOutputOffset);
972
    }
973
974
    /**
975
     * Gets the last output time in seconds.
976
     */
977
    public function getLastOutputTime(): ?float
978
    {
979
        return $this->lastOutputTime;
980
    }
981
982
    /**
983
     * Gets the command line to be executed.
984
     *
985
     * @return string
986
     */
987
    public function getCommandLine()
988
    {
989
        return \is_array($this->commandline) ? implode(' ', array_map([$this, 'escapeArgument'], $this->commandline)) : $this->commandline;
990
    }
991
992
    /**
993
     * Gets the process timeout in seconds (max. runtime).
994
     *
995
     * @return float|null
996
     */
997
    public function getTimeout()
998
    {
999
        return $this->timeout;
1000
    }
1001
1002
    /**
1003
     * Gets the process idle timeout in seconds (max. time since last output).
1004
     *
1005
     * @return float|null
1006
     */
1007
    public function getIdleTimeout()
1008
    {
1009
        return $this->idleTimeout;
1010
    }
1011
1012
    /**
1013
     * Sets the process timeout (max. runtime) in seconds.
1014
     *
1015
     * To disable the timeout, set this value to null.
1016
     *
1017
     * @return $this
1018
     *
1019
     * @throws InvalidArgumentException if the timeout is negative
1020
     */
1021
    public function setTimeout(?float $timeout)
1022
    {
1023
        $this->timeout = $this->validateTimeout($timeout);
1024
1025
        return $this;
1026
    }
1027
1028
    /**
1029
     * Sets the process idle timeout (max. time since last output) in seconds.
1030
     *
1031
     * To disable the timeout, set this value to null.
1032
     *
1033
     * @return $this
1034
     *
1035
     * @throws LogicException           if the output is disabled
1036
     * @throws InvalidArgumentException if the timeout is negative
1037
     */
1038
    public function setIdleTimeout(?float $timeout)
1039
    {
1040
        if (null !== $timeout && $this->outputDisabled) {
1041
            throw new LogicException('Idle timeout cannot be set while the output is disabled.');
1042
        }
1043
1044
        $this->idleTimeout = $this->validateTimeout($timeout);
1045
1046
        return $this;
1047
    }
1048
1049
    /**
1050
     * Enables or disables the TTY mode.
1051
     *
1052
     * @return $this
1053
     *
1054
     * @throws RuntimeException In case the TTY mode is not supported
1055
     */
1056
    public function setTty(bool $tty)
1057
    {
1058
        if ('\\' === \DIRECTORY_SEPARATOR && $tty) {
1059
            throw new RuntimeException('TTY mode is not supported on Windows platform.');
1060
        }
1061
1062
        if ($tty && !self::isTtySupported()) {
1063
            throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.');
1064
        }
1065
1066
        $this->tty = $tty;
1067
1068
        return $this;
1069
    }
1070
1071
    /**
1072
     * Checks if the TTY mode is enabled.
1073
     *
1074
     * @return bool
1075
     */
1076
    public function isTty()
1077
    {
1078
        return $this->tty;
1079
    }
1080
1081
    /**
1082
     * Sets PTY mode.
1083
     *
1084
     * @return $this
1085
     */
1086
    public function setPty(bool $bool)
1087
    {
1088
        $this->pty = $bool;
1089
1090
        return $this;
1091
    }
1092
1093
    /**
1094
     * Returns PTY state.
1095
     *
1096
     * @return bool
1097
     */
1098
    public function isPty()
1099
    {
1100
        return $this->pty;
1101
    }
1102
1103
    /**
1104
     * Gets the working directory.
1105
     *
1106
     * @return string|null
1107
     */
1108
    public function getWorkingDirectory()
1109
    {
1110
        if (null === $this->cwd) {
1111
            // getcwd() will return false if any one of the parent directories does not have
1112
            // the readable or search mode set, even if the current directory does
1113
            return getcwd() ?: null;
1114
        }
1115
1116
        return $this->cwd;
1117
    }
1118
1119
    /**
1120
     * Sets the current working directory.
1121
     *
1122
     * @return $this
1123
     */
1124
    public function setWorkingDirectory(string $cwd)
1125
    {
1126
        $this->cwd = $cwd;
1127
1128
        return $this;
1129
    }
1130
1131
    /**
1132
     * Gets the environment variables.
1133
     *
1134
     * @return array
1135
     */
1136
    public function getEnv()
1137
    {
1138
        return $this->env;
1139
    }
1140
1141
    /**
1142
     * Sets the environment variables.
1143
     *
1144
     * @param array<string|\Stringable> $env The new environment variables
1145
     *
1146
     * @return $this
1147
     */
1148
    public function setEnv(array $env)
1149
    {
1150
        $this->env = $env;
1151
1152
        return $this;
1153
    }
1154
1155
    /**
1156
     * Gets the Process input.
1157
     *
1158
     * @return resource|string|\Iterator|null
1159
     */
1160
    public function getInput()
1161
    {
1162
        return $this->input;
1163
    }
1164
1165
    /**
1166
     * Sets the input.
1167
     *
1168
     * This content will be passed to the underlying process standard input.
1169
     *
1170
     * @param string|int|float|bool|resource|\Traversable|null $input The content
1171
     *
1172
     * @return $this
1173
     *
1174
     * @throws LogicException In case the process is running
1175
     */
1176
    public function setInput($input)
1177
    {
1178
        if ($this->isRunning()) {
1179
            throw new LogicException('Input cannot be set while the process is running.');
1180
        }
1181
1182
        $this->input = ProcessUtils::validateInput(__METHOD__, $input);
1183
1184
        return $this;
1185
    }
1186
1187
    /**
1188
     * Performs a check between the timeout definition and the time the process started.
1189
     *
1190
     * In case you run a background process (with the start method), you should
1191
     * trigger this method regularly to ensure the process timeout
1192
     *
1193
     * @throws ProcessTimedOutException In case the timeout was reached
1194
     */
1195
    public function checkTimeout()
1196
    {
1197
        if (self::STATUS_STARTED !== $this->status) {
1198
            return;
1199
        }
1200
1201
        if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
1202
            $this->stop(0);
1203
1204
            throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL);
1205
        }
1206
1207
        if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) {
1208
            $this->stop(0);
1209
1210
            throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE);
1211
        }
1212
    }
1213
1214
    /**
1215
     * @throws LogicException in case process is not started
1216
     */
1217
    public function getStartTime(): float
1218
    {
1219
        if (!$this->isStarted()) {
1220
            throw new LogicException('Start time is only available after process start.');
1221
        }
1222
1223
        return $this->starttime;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->starttime could return the type null which is incompatible with the type-hinted return double. Consider adding an additional type-check to rule them out.
Loading history...
1224
    }
1225
1226
    /**
1227
     * Defines options to pass to the underlying proc_open().
1228
     *
1229
     * @see https://php.net/proc_open for the options supported by PHP.
1230
     *
1231
     * Enabling the "create_new_console" option allows a subprocess to continue
1232
     * to run after the main process exited, on both Windows and *nix
1233
     */
1234
    public function setOptions(array $options)
1235
    {
1236
        if ($this->isRunning()) {
1237
            throw new RuntimeException('Setting options while the process is running is not possible.');
1238
        }
1239
1240
        $defaultOptions = $this->options;
1241
        $existingOptions = ['blocking_pipes', 'create_process_group', 'create_new_console'];
1242
1243
        foreach ($options as $key => $value) {
1244
            if (!\in_array($key, $existingOptions)) {
1245
                $this->options = $defaultOptions;
1246
                throw new LogicException(sprintf('Invalid option "%s" passed to "%s()". Supported options are "%s".', $key, __METHOD__, implode('", "', $existingOptions)));
1247
            }
1248
            $this->options[$key] = $value;
1249
        }
1250
    }
1251
1252
    /**
1253
     * Returns whether TTY is supported on the current operating system.
1254
     */
1255
    public static function isTtySupported(): bool
1256
    {
1257
        static $isTtySupported;
1258
1259
        if (null === $isTtySupported) {
1260
            $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', [['file', '/dev/tty', 'r'], ['file', '/dev/tty', 'w'], ['file', '/dev/tty', 'w']], $pipes);
1261
        }
1262
1263
        return $isTtySupported;
1264
    }
1265
1266
    /**
1267
     * Returns whether PTY is supported on the current operating system.
1268
     *
1269
     * @return bool
1270
     */
1271
    public static function isPtySupported()
1272
    {
1273
        static $result;
1274
1275
        if (null !== $result) {
1276
            return $result;
1277
        }
1278
1279
        if ('\\' === \DIRECTORY_SEPARATOR) {
1280
            return $result = false;
1281
        }
1282
1283
        return $result = (bool) @proc_open('echo 1 >/dev/null', [['pty'], ['pty'], ['pty']], $pipes);
1284
    }
1285
1286
    /**
1287
     * Creates the descriptors needed by the proc_open.
1288
     */
1289
    private function getDescriptors(): array
1290
    {
1291
        if ($this->input instanceof \Iterator) {
1292
            $this->input->rewind();
1293
        }
1294
        if ('\\' === \DIRECTORY_SEPARATOR) {
1295
            $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $this->hasCallback);
0 ignored issues
show
Bug Best Practice introduced by
The property hasCallback does not exist on Symfony\Component\Process\Process. Did you maybe forget to declare it?
Loading history...
1296
        } else {
1297
            $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->hasCallback);
1298
        }
1299
1300
        return $this->processPipes->getDescriptors();
1301
    }
1302
1303
    /**
1304
     * Builds up the callback used by wait().
1305
     *
1306
     * The callbacks adds all occurred output to the specific buffer and calls
1307
     * the user callback (if present) with the received output.
1308
     *
1309
     * @param callable|null $callback The user defined PHP callback
1310
     *
1311
     * @return \Closure
1312
     */
1313
    protected function buildCallback(callable $callback = null)
1314
    {
1315
        if ($this->outputDisabled) {
1316
            return function ($type, $data) use ($callback): bool {
1317
                return null !== $callback && $callback($type, $data);
1318
            };
1319
        }
1320
1321
        $out = self::OUT;
1322
1323
        return function ($type, $data) use ($callback, $out): bool {
1324
            if ($out == $type) {
1325
                $this->addOutput($data);
1326
            } else {
1327
                $this->addErrorOutput($data);
1328
            }
1329
1330
            return null !== $callback && $callback($type, $data);
1331
        };
1332
    }
1333
1334
    /**
1335
     * Updates the status of the process, reads pipes.
1336
     *
1337
     * @param bool $blocking Whether to use a blocking read call
1338
     */
1339
    protected function updateStatus(bool $blocking)
1340
    {
1341
        if (self::STATUS_STARTED !== $this->status) {
1342
            return;
1343
        }
1344
1345
        $this->processInformation = proc_get_status($this->process);
1346
        $running = $this->processInformation['running'];
1347
1348
        $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running);
1349
1350
        if ($this->fallbackStatus && $this->isSigchildEnabled()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->fallbackStatus of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1351
            $this->processInformation = $this->fallbackStatus + $this->processInformation;
1352
        }
1353
1354
        if (!$running) {
1355
            $this->close();
1356
        }
1357
    }
1358
1359
    /**
1360
     * Returns whether PHP has been compiled with the '--enable-sigchild' option or not.
1361
     *
1362
     * @return bool
1363
     */
1364
    protected function isSigchildEnabled()
1365
    {
1366
        if (null !== self::$sigchild) {
1367
            return self::$sigchild;
1368
        }
1369
1370
        if (!\function_exists('phpinfo')) {
1371
            return self::$sigchild = false;
1372
        }
1373
1374
        ob_start();
1375
        phpinfo(\INFO_GENERAL);
1376
1377
        return self::$sigchild = str_contains(ob_get_clean(), '--enable-sigchild');
1378
    }
1379
1380
    /**
1381
     * Reads pipes for the freshest output.
1382
     *
1383
     * @param string $caller   The name of the method that needs fresh outputs
1384
     * @param bool   $blocking Whether to use blocking calls or not
1385
     *
1386
     * @throws LogicException in case output has been disabled or process is not started
1387
     */
1388
    private function readPipesForOutput(string $caller, bool $blocking = false)
1389
    {
1390
        if ($this->outputDisabled) {
1391
            throw new LogicException('Output has been disabled.');
1392
        }
1393
1394
        $this->requireProcessIsStarted($caller);
1395
1396
        $this->updateStatus($blocking);
1397
    }
1398
1399
    /**
1400
     * Validates and returns the filtered timeout.
1401
     *
1402
     * @throws InvalidArgumentException if the given timeout is a negative number
1403
     */
1404
    private function validateTimeout(?float $timeout): ?float
1405
    {
1406
        $timeout = (float) $timeout;
1407
1408
        if (0.0 === $timeout) {
0 ignored issues
show
introduced by
The condition 0.0 === $timeout is always false.
Loading history...
1409
            $timeout = null;
1410
        } elseif ($timeout < 0) {
1411
            throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
1412
        }
1413
1414
        return $timeout;
1415
    }
1416
1417
    /**
1418
     * Reads pipes, executes callback.
1419
     *
1420
     * @param bool $blocking Whether to use blocking calls or not
1421
     * @param bool $close    Whether to close file handles or not
1422
     */
1423
    private function readPipes(bool $blocking, bool $close)
1424
    {
1425
        $result = $this->processPipes->readAndWrite($blocking, $close);
1426
1427
        $callback = $this->callback;
1428
        foreach ($result as $type => $data) {
1429
            if (3 !== $type) {
1430
                $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data);
1431
            } elseif (!isset($this->fallbackStatus['signaled'])) {
1432
                $this->fallbackStatus['exitcode'] = (int) $data;
1433
            }
1434
        }
1435
    }
1436
1437
    /**
1438
     * Closes process resource, closes file handles, sets the exitcode.
1439
     *
1440
     * @return int The exitcode
1441
     */
1442
    private function close(): int
1443
    {
1444
        $this->processPipes->close();
1445
        if (\is_resource($this->process)) {
1446
            proc_close($this->process);
1447
        }
1448
        $this->exitcode = $this->processInformation['exitcode'];
1449
        $this->status = self::STATUS_TERMINATED;
1450
1451
        if (-1 === $this->exitcode) {
1452
            if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) {
1453
                // if process has been signaled, no exitcode but a valid termsig, apply Unix convention
1454
                $this->exitcode = 128 + $this->processInformation['termsig'];
1455
            } elseif ($this->isSigchildEnabled()) {
1456
                $this->processInformation['signaled'] = true;
1457
                $this->processInformation['termsig'] = -1;
1458
            }
1459
        }
1460
1461
        // Free memory from self-reference callback created by buildCallback
1462
        // Doing so in other contexts like __destruct or by garbage collector is ineffective
1463
        // Now pipes are closed, so the callback is no longer necessary
1464
        $this->callback = null;
1465
1466
        return $this->exitcode;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->exitcode could return the type null which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
1467
    }
1468
1469
    /**
1470
     * Resets data related to the latest run of the process.
1471
     */
1472
    private function resetProcessData()
1473
    {
1474
        $this->starttime = null;
1475
        $this->callback = null;
1476
        $this->exitcode = null;
1477
        $this->fallbackStatus = [];
1478
        $this->processInformation = null;
1479
        $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+');
1480
        $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+');
1481
        $this->process = null;
1482
        $this->latestSignal = null;
1483
        $this->status = self::STATUS_READY;
1484
        $this->incrementalOutputOffset = 0;
1485
        $this->incrementalErrorOutputOffset = 0;
1486
    }
1487
1488
    /**
1489
     * Sends a POSIX signal to the process.
1490
     *
1491
     * @param int  $signal         A valid POSIX signal (see https://php.net/pcntl.constants)
1492
     * @param bool $throwException Whether to throw exception in case signal failed
1493
     *
1494
     * @throws LogicException   In case the process is not running
1495
     * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
1496
     * @throws RuntimeException In case of failure
1497
     */
1498
    private function doSignal(int $signal, bool $throwException): bool
1499
    {
1500
        if (null === $pid = $this->getPid()) {
1501
            if ($throwException) {
1502
                throw new LogicException('Cannot send signal on a non running process.');
1503
            }
1504
1505
            return false;
1506
        }
1507
1508
        if ('\\' === \DIRECTORY_SEPARATOR) {
1509
            exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode);
1510
            if ($exitCode && $this->isRunning()) {
1511
                if ($throwException) {
1512
                    throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output)));
1513
                }
1514
1515
                return false;
1516
            }
1517
        } else {
1518
            if (!$this->isSigchildEnabled()) {
1519
                $ok = @proc_terminate($this->process, $signal);
1520
            } elseif (\function_exists('posix_kill')) {
1521
                $ok = @posix_kill($pid, $signal);
1522
            } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) {
1523
                $ok = false === fgets($pipes[2]);
1524
            }
1525
            if (!$ok) {
1526
                if ($throwException) {
1527
                    throw new RuntimeException(sprintf('Error while sending signal "%s".', $signal));
1528
                }
1529
1530
                return false;
1531
            }
1532
        }
1533
1534
        $this->latestSignal = $signal;
1535
        $this->fallbackStatus['signaled'] = true;
1536
        $this->fallbackStatus['exitcode'] = -1;
1537
        $this->fallbackStatus['termsig'] = $this->latestSignal;
1538
1539
        return true;
1540
    }
1541
1542
    private function prepareWindowsCommandLine(string $cmd, array &$env): string
1543
    {
1544
        $uid = uniqid('', true);
1545
        $varCount = 0;
1546
        $varCache = [];
1547
        $cmd = preg_replace_callback(
1548
            '/"(?:(
1549
                [^"%!^]*+
1550
                (?:
1551
                    (?: !LF! | "(?:\^[%!^])?+" )
1552
                    [^"%!^]*+
1553
                )++
1554
            ) | [^"]*+ )"/x',
1555
            function ($m) use (&$env, &$varCache, &$varCount, $uid) {
1556
                if (!isset($m[1])) {
1557
                    return $m[0];
1558
                }
1559
                if (isset($varCache[$m[0]])) {
1560
                    return $varCache[$m[0]];
1561
                }
1562
                if (str_contains($value = $m[1], "\0")) {
1563
                    $value = str_replace("\0", '?', $value);
1564
                }
1565
                if (false === strpbrk($value, "\"%!\n")) {
1566
                    return '"'.$value.'"';
1567
                }
1568
1569
                $value = str_replace(['!LF!', '"^!"', '"^%"', '"^^"', '""'], ["\n", '!', '%', '^', '"'], $value);
1570
                $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"';
1571
                $var = $uid.++$varCount;
1572
1573
                $env[$var] = $value;
1574
1575
                return $varCache[$m[0]] = '!'.$var.'!';
1576
            },
1577
            $cmd
1578
        );
1579
1580
        $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')';
1581
        foreach ($this->processPipes->getFiles() as $offset => $filename) {
1582
            $cmd .= ' '.$offset.'>"'.$filename.'"';
1583
        }
1584
1585
        return $cmd;
1586
    }
1587
1588
    /**
1589
     * Ensures the process is running or terminated, throws a LogicException if the process has a not started.
1590
     *
1591
     * @throws LogicException if the process has not run
1592
     */
1593
    private function requireProcessIsStarted(string $functionName)
1594
    {
1595
        if (!$this->isStarted()) {
1596
            throw new LogicException(sprintf('Process must be started before calling "%s()".', $functionName));
1597
        }
1598
    }
1599
1600
    /**
1601
     * Ensures the process is terminated, throws a LogicException if the process has a status different than "terminated".
1602
     *
1603
     * @throws LogicException if the process is not yet terminated
1604
     */
1605
    private function requireProcessIsTerminated(string $functionName)
1606
    {
1607
        if (!$this->isTerminated()) {
1608
            throw new LogicException(sprintf('Process must be terminated before calling "%s()".', $functionName));
1609
        }
1610
    }
1611
1612
    /**
1613
     * Escapes a string to be used as a shell argument.
1614
     */
1615
    private function escapeArgument(?string $argument): string
1616
    {
1617
        if ('' === $argument || null === $argument) {
1618
            return '""';
1619
        }
1620
        if ('\\' !== \DIRECTORY_SEPARATOR) {
1621
            return "'".str_replace("'", "'\\''", $argument)."'";
1622
        }
1623
        if (str_contains($argument, "\0")) {
1624
            $argument = str_replace("\0", '?', $argument);
1625
        }
1626
        if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) {
1627
            return $argument;
1628
        }
1629
        $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument);
1630
1631
        return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"';
1632
    }
1633
1634
    private function replacePlaceholders(string $commandline, array $env)
1635
    {
1636
        return preg_replace_callback('/"\$\{:([_a-zA-Z]++[_a-zA-Z0-9]*+)\}"/', function ($matches) use ($commandline, $env) {
1637
            if (!isset($env[$matches[1]]) || false === $env[$matches[1]]) {
1638
                throw new InvalidArgumentException(sprintf('Command line is missing a value for parameter "%s": ', $matches[1]).$commandline);
1639
            }
1640
1641
            return $this->escapeArgument($env[$matches[1]]);
1642
        }, $commandline);
1643
    }
1644
1645
    private function getDefaultEnv(): array
1646
    {
1647
        $env = getenv();
1648
        $env = ('\\' === \DIRECTORY_SEPARATOR ? array_intersect_ukey($env, $_SERVER, 'strcasecmp') : array_intersect_key($env, $_SERVER)) ?: $env;
1649
1650
        return $_ENV + ('\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($env, $_ENV, 'strcasecmp') : $env);
1651
    }
1652
}
1653