Subversion-Projekte lars-tiefland.laravel_shop

Revision

Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

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