Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Details | Vergleich mit vorheriger | 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
     *
688 lars 607
     * @return \Generator<string, string>
608
     *
148 lars 609
     * @throws LogicException in case the output has been disabled
610
     * @throws LogicException In case the process is not started
611
     */
612
    public function getIterator(int $flags = 0): \Generator
613
    {
614
        $this->readPipesForOutput(__FUNCTION__, false);
615
 
616
        $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags);
617
        $blocking = !(self::ITER_NON_BLOCKING & $flags);
618
        $yieldOut = !(self::ITER_SKIP_OUT & $flags);
619
        $yieldErr = !(self::ITER_SKIP_ERR & $flags);
620
 
621
        while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) {
622
            if ($yieldOut) {
623
                $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
624
 
625
                if (isset($out[0])) {
626
                    if ($clearOutput) {
627
                        $this->clearOutput();
628
                    } else {
629
                        $this->incrementalOutputOffset = ftell($this->stdout);
630
                    }
631
 
632
                    yield self::OUT => $out;
633
                }
634
            }
635
 
636
            if ($yieldErr) {
637
                $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
638
 
639
                if (isset($err[0])) {
640
                    if ($clearOutput) {
641
                        $this->clearErrorOutput();
642
                    } else {
643
                        $this->incrementalErrorOutputOffset = ftell($this->stderr);
644
                    }
645
 
646
                    yield self::ERR => $err;
647
                }
648
            }
649
 
650
            if (!$blocking && !isset($out[0]) && !isset($err[0])) {
651
                yield self::OUT => '';
652
            }
653
 
654
            $this->checkTimeout();
655
            $this->readPipesForOutput(__FUNCTION__, $blocking);
656
        }
657
    }
658
 
659
    /**
660
     * Clears the process output.
661
     *
662
     * @return $this
663
     */
664
    public function clearOutput(): static
665
    {
666
        ftruncate($this->stdout, 0);
667
        fseek($this->stdout, 0);
668
        $this->incrementalOutputOffset = 0;
669
 
670
        return $this;
671
    }
672
 
673
    /**
674
     * Returns the current error output of the process (STDERR).
675
     *
676
     * @throws LogicException in case the output has been disabled
677
     * @throws LogicException In case the process is not started
678
     */
679
    public function getErrorOutput(): string
680
    {
681
        $this->readPipesForOutput(__FUNCTION__);
682
 
683
        if (false === $ret = stream_get_contents($this->stderr, -1, 0)) {
684
            return '';
685
        }
686
 
687
        return $ret;
688
    }
689
 
690
    /**
691
     * Returns the errorOutput incrementally.
692
     *
693
     * In comparison with the getErrorOutput method which always return the
694
     * whole error output, this one returns the new error output since the last
695
     * call.
696
     *
697
     * @throws LogicException in case the output has been disabled
698
     * @throws LogicException In case the process is not started
699
     */
700
    public function getIncrementalErrorOutput(): string
701
    {
702
        $this->readPipesForOutput(__FUNCTION__);
703
 
704
        $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset);
705
        $this->incrementalErrorOutputOffset = ftell($this->stderr);
706
 
707
        if (false === $latest) {
708
            return '';
709
        }
710
 
711
        return $latest;
712
    }
713
 
714
    /**
715
     * Clears the process output.
716
     *
717
     * @return $this
718
     */
719
    public function clearErrorOutput(): static
720
    {
721
        ftruncate($this->stderr, 0);
722
        fseek($this->stderr, 0);
723
        $this->incrementalErrorOutputOffset = 0;
724
 
725
        return $this;
726
    }
727
 
728
    /**
729
     * Returns the exit code returned by the process.
730
     *
731
     * @return int|null The exit status code, null if the Process is not terminated
732
     */
733
    public function getExitCode(): ?int
734
    {
735
        $this->updateStatus(false);
736
 
737
        return $this->exitcode;
738
    }
739
 
740
    /**
741
     * Returns a string representation for the exit code returned by the process.
742
     *
743
     * This method relies on the Unix exit code status standardization
744
     * and might not be relevant for other operating systems.
745
     *
746
     * @return string|null A string representation for the exit status code, null if the Process is not terminated
747
     *
748
     * @see http://tldp.org/LDP/abs/html/exitcodes.html
749
     * @see http://en.wikipedia.org/wiki/Unix_signal
750
     */
751
    public function getExitCodeText(): ?string
752
    {
753
        if (null === $exitcode = $this->getExitCode()) {
754
            return null;
755
        }
756
 
757
        return self::$exitCodes[$exitcode] ?? 'Unknown error';
758
    }
759
 
760
    /**
761
     * Checks if the process ended successfully.
762
     */
763
    public function isSuccessful(): bool
764
    {
765
        return 0 === $this->getExitCode();
766
    }
767
 
768
    /**
769
     * Returns true if the child process has been terminated by an uncaught signal.
770
     *
771
     * It always returns false on Windows.
772
     *
773
     * @throws LogicException In case the process is not terminated
774
     */
775
    public function hasBeenSignaled(): bool
776
    {
777
        $this->requireProcessIsTerminated(__FUNCTION__);
778
 
779
        return $this->processInformation['signaled'];
780
    }
781
 
782
    /**
783
     * Returns the number of the signal that caused the child process to terminate its execution.
784
     *
785
     * It is only meaningful if hasBeenSignaled() returns true.
786
     *
787
     * @throws RuntimeException In case --enable-sigchild is activated
788
     * @throws LogicException   In case the process is not terminated
789
     */
790
    public function getTermSignal(): int
791
    {
792
        $this->requireProcessIsTerminated(__FUNCTION__);
793
 
794
        if ($this->isSigchildEnabled() && -1 === $this->processInformation['termsig']) {
795
            throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal cannot be retrieved.');
796
        }
797
 
798
        return $this->processInformation['termsig'];
799
    }
800
 
801
    /**
802
     * Returns true if the child process has been stopped by a signal.
803
     *
804
     * It always returns false on Windows.
805
     *
806
     * @throws LogicException In case the process is not terminated
807
     */
808
    public function hasBeenStopped(): bool
809
    {
810
        $this->requireProcessIsTerminated(__FUNCTION__);
811
 
812
        return $this->processInformation['stopped'];
813
    }
814
 
815
    /**
816
     * Returns the number of the signal that caused the child process to stop its execution.
817
     *
818
     * It is only meaningful if hasBeenStopped() returns true.
819
     *
820
     * @throws LogicException In case the process is not terminated
821
     */
822
    public function getStopSignal(): int
823
    {
824
        $this->requireProcessIsTerminated(__FUNCTION__);
825
 
826
        return $this->processInformation['stopsig'];
827
    }
828
 
829
    /**
830
     * Checks if the process is currently running.
831
     */
832
    public function isRunning(): bool
833
    {
834
        if (self::STATUS_STARTED !== $this->status) {
835
            return false;
836
        }
837
 
838
        $this->updateStatus(false);
839
 
840
        return $this->processInformation['running'];
841
    }
842
 
843
    /**
844
     * Checks if the process has been started with no regard to the current state.
845
     */
846
    public function isStarted(): bool
847
    {
848
        return self::STATUS_READY != $this->status;
849
    }
850
 
851
    /**
852
     * Checks if the process is terminated.
853
     */
854
    public function isTerminated(): bool
855
    {
856
        $this->updateStatus(false);
857
 
858
        return self::STATUS_TERMINATED == $this->status;
859
    }
860
 
861
    /**
862
     * Gets the process status.
863
     *
864
     * The status is one of: ready, started, terminated.
865
     */
866
    public function getStatus(): string
867
    {
868
        $this->updateStatus(false);
869
 
870
        return $this->status;
871
    }
872
 
873
    /**
874
     * Stops the process.
875
     *
876
     * @param int|float $timeout The timeout in seconds
688 lars 877
     * @param int|null  $signal  A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9)
148 lars 878
     *
879
     * @return int|null The exit-code of the process or null if it's not running
880
     */
881
    public function stop(float $timeout = 10, int $signal = null): ?int
882
    {
883
        $timeoutMicro = microtime(true) + $timeout;
884
        if ($this->isRunning()) {
885
            // given SIGTERM may not be defined and that "proc_terminate" uses the constant value and not the constant itself, we use the same here
886
            $this->doSignal(15, false);
887
            do {
888
                usleep(1000);
889
            } while ($this->isRunning() && microtime(true) < $timeoutMicro);
890
 
891
            if ($this->isRunning()) {
892
                // Avoid exception here: process is supposed to be running, but it might have stopped just
893
                // after this line. In any case, let's silently discard the error, we cannot do anything.
894
                $this->doSignal($signal ?: 9, false);
895
            }
896
        }
897
 
898
        if ($this->isRunning()) {
899
            if (isset($this->fallbackStatus['pid'])) {
900
                unset($this->fallbackStatus['pid']);
901
 
902
                return $this->stop(0, $signal);
903
            }
904
            $this->close();
905
        }
906
 
907
        return $this->exitcode;
908
    }
909
 
910
    /**
911
     * Adds a line to the STDOUT stream.
912
     *
913
     * @internal
914
     */
915
    public function addOutput(string $line)
916
    {
917
        $this->lastOutputTime = microtime(true);
918
 
919
        fseek($this->stdout, 0, \SEEK_END);
920
        fwrite($this->stdout, $line);
921
        fseek($this->stdout, $this->incrementalOutputOffset);
922
    }
923
 
924
    /**
925
     * Adds a line to the STDERR stream.
926
     *
927
     * @internal
928
     */
929
    public function addErrorOutput(string $line)
930
    {
931
        $this->lastOutputTime = microtime(true);
932
 
933
        fseek($this->stderr, 0, \SEEK_END);
934
        fwrite($this->stderr, $line);
935
        fseek($this->stderr, $this->incrementalErrorOutputOffset);
936
    }
937
 
938
    /**
939
     * Gets the last output time in seconds.
940
     */
941
    public function getLastOutputTime(): ?float
942
    {
943
        return $this->lastOutputTime;
944
    }
945
 
946
    /**
947
     * Gets the command line to be executed.
948
     */
949
    public function getCommandLine(): string
950
    {
951
        return \is_array($this->commandline) ? implode(' ', array_map($this->escapeArgument(...), $this->commandline)) : $this->commandline;
952
    }
953
 
954
    /**
955
     * Gets the process timeout in seconds (max. runtime).
956
     */
957
    public function getTimeout(): ?float
958
    {
959
        return $this->timeout;
960
    }
961
 
962
    /**
963
     * Gets the process idle timeout in seconds (max. time since last output).
964
     */
965
    public function getIdleTimeout(): ?float
966
    {
967
        return $this->idleTimeout;
968
    }
969
 
970
    /**
971
     * Sets the process timeout (max. runtime) in seconds.
972
     *
973
     * To disable the timeout, set this value to null.
974
     *
975
     * @return $this
976
     *
977
     * @throws InvalidArgumentException if the timeout is negative
978
     */
979
    public function setTimeout(?float $timeout): static
980
    {
981
        $this->timeout = $this->validateTimeout($timeout);
982
 
983
        return $this;
984
    }
985
 
986
    /**
987
     * Sets the process idle timeout (max. time since last output) in seconds.
988
     *
989
     * To disable the timeout, set this value to null.
990
     *
991
     * @return $this
992
     *
993
     * @throws LogicException           if the output is disabled
994
     * @throws InvalidArgumentException if the timeout is negative
995
     */
996
    public function setIdleTimeout(?float $timeout): static
997
    {
998
        if (null !== $timeout && $this->outputDisabled) {
999
            throw new LogicException('Idle timeout cannot be set while the output is disabled.');
1000
        }
1001
 
1002
        $this->idleTimeout = $this->validateTimeout($timeout);
1003
 
1004
        return $this;
1005
    }
1006
 
1007
    /**
1008
     * Enables or disables the TTY mode.
1009
     *
1010
     * @return $this
1011
     *
1012
     * @throws RuntimeException In case the TTY mode is not supported
1013
     */
1014
    public function setTty(bool $tty): static
1015
    {
1016
        if ('\\' === \DIRECTORY_SEPARATOR && $tty) {
1017
            throw new RuntimeException('TTY mode is not supported on Windows platform.');
1018
        }
1019
 
1020
        if ($tty && !self::isTtySupported()) {
1021
            throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.');
1022
        }
1023
 
1024
        $this->tty = $tty;
1025
 
1026
        return $this;
1027
    }
1028
 
1029
    /**
1030
     * Checks if the TTY mode is enabled.
1031
     */
1032
    public function isTty(): bool
1033
    {
1034
        return $this->tty;
1035
    }
1036
 
1037
    /**
1038
     * Sets PTY mode.
1039
     *
1040
     * @return $this
1041
     */
1042
    public function setPty(bool $bool): static
1043
    {
1044
        $this->pty = $bool;
1045
 
1046
        return $this;
1047
    }
1048
 
1049
    /**
1050
     * Returns PTY state.
1051
     */
1052
    public function isPty(): bool
1053
    {
1054
        return $this->pty;
1055
    }
1056
 
1057
    /**
1058
     * Gets the working directory.
1059
     */
1060
    public function getWorkingDirectory(): ?string
1061
    {
1062
        if (null === $this->cwd) {
1063
            // getcwd() will return false if any one of the parent directories does not have
1064
            // the readable or search mode set, even if the current directory does
1065
            return getcwd() ?: null;
1066
        }
1067
 
1068
        return $this->cwd;
1069
    }
1070
 
1071
    /**
1072
     * Sets the current working directory.
1073
     *
1074
     * @return $this
1075
     */
1076
    public function setWorkingDirectory(string $cwd): static
1077
    {
1078
        $this->cwd = $cwd;
1079
 
1080
        return $this;
1081
    }
1082
 
1083
    /**
1084
     * Gets the environment variables.
1085
     */
1086
    public function getEnv(): array
1087
    {
1088
        return $this->env;
1089
    }
1090
 
1091
    /**
1092
     * Sets the environment variables.
1093
     *
1094
     * @param array<string|\Stringable> $env The new environment variables
1095
     *
1096
     * @return $this
1097
     */
1098
    public function setEnv(array $env): static
1099
    {
1100
        $this->env = $env;
1101
 
1102
        return $this;
1103
    }
1104
 
1105
    /**
1106
     * Gets the Process input.
1107
     *
1108
     * @return resource|string|\Iterator|null
1109
     */
1110
    public function getInput()
1111
    {
1112
        return $this->input;
1113
    }
1114
 
1115
    /**
1116
     * Sets the input.
1117
     *
1118
     * This content will be passed to the underlying process standard input.
1119
     *
1120
     * @param string|int|float|bool|resource|\Traversable|null $input The content
1121
     *
1122
     * @return $this
1123
     *
1124
     * @throws LogicException In case the process is running
1125
     */
1126
    public function setInput(mixed $input): static
1127
    {
1128
        if ($this->isRunning()) {
1129
            throw new LogicException('Input cannot be set while the process is running.');
1130
        }
1131
 
1132
        $this->input = ProcessUtils::validateInput(__METHOD__, $input);
1133
 
1134
        return $this;
1135
    }
1136
 
1137
    /**
1138
     * Performs a check between the timeout definition and the time the process started.
1139
     *
1140
     * In case you run a background process (with the start method), you should
1141
     * trigger this method regularly to ensure the process timeout
1142
     *
1143
     * @throws ProcessTimedOutException In case the timeout was reached
1144
     */
1145
    public function checkTimeout()
1146
    {
1147
        if (self::STATUS_STARTED !== $this->status) {
1148
            return;
1149
        }
1150
 
1151
        if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
1152
            $this->stop(0);
1153
 
1154
            throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL);
1155
        }
1156
 
1157
        if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) {
1158
            $this->stop(0);
1159
 
1160
            throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE);
1161
        }
1162
    }
1163
 
1164
    /**
1165
     * @throws LogicException in case process is not started
1166
     */
1167
    public function getStartTime(): float
1168
    {
1169
        if (!$this->isStarted()) {
1170
            throw new LogicException('Start time is only available after process start.');
1171
        }
1172
 
1173
        return $this->starttime;
1174
    }
1175
 
1176
    /**
1177
     * Defines options to pass to the underlying proc_open().
1178
     *
1179
     * @see https://php.net/proc_open for the options supported by PHP.
1180
     *
1181
     * Enabling the "create_new_console" option allows a subprocess to continue
1182
     * to run after the main process exited, on both Windows and *nix
1183
     */
1184
    public function setOptions(array $options)
1185
    {
1186
        if ($this->isRunning()) {
1187
            throw new RuntimeException('Setting options while the process is running is not possible.');
1188
        }
1189
 
1190
        $defaultOptions = $this->options;
1191
        $existingOptions = ['blocking_pipes', 'create_process_group', 'create_new_console'];
1192
 
1193
        foreach ($options as $key => $value) {
1194
            if (!\in_array($key, $existingOptions)) {
1195
                $this->options = $defaultOptions;
1196
                throw new LogicException(sprintf('Invalid option "%s" passed to "%s()". Supported options are "%s".', $key, __METHOD__, implode('", "', $existingOptions)));
1197
            }
1198
            $this->options[$key] = $value;
1199
        }
1200
    }
1201
 
1202
    /**
1203
     * Returns whether TTY is supported on the current operating system.
1204
     */
1205
    public static function isTtySupported(): bool
1206
    {
1207
        static $isTtySupported;
1208
 
1209
        return $isTtySupported ??= ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT));
1210
    }
1211
 
1212
    /**
1213
     * Returns whether PTY is supported on the current operating system.
1214
     */
1215
    public static function isPtySupported(): bool
1216
    {
1217
        static $result;
1218
 
1219
        if (null !== $result) {
1220
            return $result;
1221
        }
1222
 
1223
        if ('\\' === \DIRECTORY_SEPARATOR) {
1224
            return $result = false;
1225
        }
1226
 
1227
        return $result = (bool) @proc_open('echo 1 >/dev/null', [['pty'], ['pty'], ['pty']], $pipes);
1228
    }
1229
 
1230
    /**
1231
     * Creates the descriptors needed by the proc_open.
1232
     */
1233
    private function getDescriptors(): array
1234
    {
1235
        if ($this->input instanceof \Iterator) {
1236
            $this->input->rewind();
1237
        }
1238
        if ('\\' === \DIRECTORY_SEPARATOR) {
1239
            $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $this->hasCallback);
1240
        } else {
1241
            $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $this->hasCallback);
1242
        }
1243
 
1244
        return $this->processPipes->getDescriptors();
1245
    }
1246
 
1247
    /**
1248
     * Builds up the callback used by wait().
1249
     *
1250
     * The callbacks adds all occurred output to the specific buffer and calls
1251
     * the user callback (if present) with the received output.
1252
     *
1253
     * @param callable|null $callback The user defined PHP callback
1254
     */
1255
    protected function buildCallback(callable $callback = null): \Closure
1256
    {
1257
        if ($this->outputDisabled) {
1258
            return function ($type, $data) use ($callback): bool {
1259
                return null !== $callback && $callback($type, $data);
1260
            };
1261
        }
1262
 
1263
        $out = self::OUT;
1264
 
1265
        return function ($type, $data) use ($callback, $out): bool {
1266
            if ($out == $type) {
1267
                $this->addOutput($data);
1268
            } else {
1269
                $this->addErrorOutput($data);
1270
            }
1271
 
1272
            return null !== $callback && $callback($type, $data);
1273
        };
1274
    }
1275
 
1276
    /**
1277
     * Updates the status of the process, reads pipes.
1278
     *
1279
     * @param bool $blocking Whether to use a blocking read call
1280
     */
1281
    protected function updateStatus(bool $blocking)
1282
    {
1283
        if (self::STATUS_STARTED !== $this->status) {
1284
            return;
1285
        }
1286
 
1287
        $this->processInformation = proc_get_status($this->process);
1288
        $running = $this->processInformation['running'];
1289
 
1290
        $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running);
1291
 
1292
        if ($this->fallbackStatus && $this->isSigchildEnabled()) {
1293
            $this->processInformation = $this->fallbackStatus + $this->processInformation;
1294
        }
1295
 
1296
        if (!$running) {
1297
            $this->close();
1298
        }
1299
    }
1300
 
1301
    /**
1302
     * Returns whether PHP has been compiled with the '--enable-sigchild' option or not.
1303
     */
1304
    protected function isSigchildEnabled(): bool
1305
    {
1306
        if (null !== self::$sigchild) {
1307
            return self::$sigchild;
1308
        }
1309
 
1310
        if (!\function_exists('phpinfo')) {
1311
            return self::$sigchild = false;
1312
        }
1313
 
1314
        ob_start();
1315
        phpinfo(\INFO_GENERAL);
1316
 
1317
        return self::$sigchild = str_contains(ob_get_clean(), '--enable-sigchild');
1318
    }
1319
 
1320
    /**
1321
     * Reads pipes for the freshest output.
1322
     *
1323
     * @param string $caller   The name of the method that needs fresh outputs
1324
     * @param bool   $blocking Whether to use blocking calls or not
1325
     *
1326
     * @throws LogicException in case output has been disabled or process is not started
1327
     */
1328
    private function readPipesForOutput(string $caller, bool $blocking = false)
1329
    {
1330
        if ($this->outputDisabled) {
1331
            throw new LogicException('Output has been disabled.');
1332
        }
1333
 
1334
        $this->requireProcessIsStarted($caller);
1335
 
1336
        $this->updateStatus($blocking);
1337
    }
1338
 
1339
    /**
1340
     * Validates and returns the filtered timeout.
1341
     *
1342
     * @throws InvalidArgumentException if the given timeout is a negative number
1343
     */
1344
    private function validateTimeout(?float $timeout): ?float
1345
    {
1346
        $timeout = (float) $timeout;
1347
 
1348
        if (0.0 === $timeout) {
1349
            $timeout = null;
1350
        } elseif ($timeout < 0) {
1351
            throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
1352
        }
1353
 
1354
        return $timeout;
1355
    }
1356
 
1357
    /**
1358
     * Reads pipes, executes callback.
1359
     *
1360
     * @param bool $blocking Whether to use blocking calls or not
1361
     * @param bool $close    Whether to close file handles or not
1362
     */
1363
    private function readPipes(bool $blocking, bool $close)
1364
    {
1365
        $result = $this->processPipes->readAndWrite($blocking, $close);
1366
 
1367
        $callback = $this->callback;
1368
        foreach ($result as $type => $data) {
1369
            if (3 !== $type) {
1370
                $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data);
1371
            } elseif (!isset($this->fallbackStatus['signaled'])) {
1372
                $this->fallbackStatus['exitcode'] = (int) $data;
1373
            }
1374
        }
1375
    }
1376
 
1377
    /**
1378
     * Closes process resource, closes file handles, sets the exitcode.
1379
     *
1380
     * @return int The exitcode
1381
     */
1382
    private function close(): int
1383
    {
1384
        $this->processPipes->close();
1385
        if (\is_resource($this->process)) {
1386
            proc_close($this->process);
1387
        }
1388
        $this->exitcode = $this->processInformation['exitcode'];
1389
        $this->status = self::STATUS_TERMINATED;
1390
 
1391
        if (-1 === $this->exitcode) {
1392
            if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) {
1393
                // if process has been signaled, no exitcode but a valid termsig, apply Unix convention
1394
                $this->exitcode = 128 + $this->processInformation['termsig'];
1395
            } elseif ($this->isSigchildEnabled()) {
1396
                $this->processInformation['signaled'] = true;
1397
                $this->processInformation['termsig'] = -1;
1398
            }
1399
        }
1400
 
1401
        // Free memory from self-reference callback created by buildCallback
1402
        // Doing so in other contexts like __destruct or by garbage collector is ineffective
1403
        // Now pipes are closed, so the callback is no longer necessary
1404
        $this->callback = null;
1405
 
1406
        return $this->exitcode;
1407
    }
1408
 
1409
    /**
1410
     * Resets data related to the latest run of the process.
1411
     */
1412
    private function resetProcessData()
1413
    {
1414
        $this->starttime = null;
1415
        $this->callback = null;
1416
        $this->exitcode = null;
1417
        $this->fallbackStatus = [];
1418
        $this->processInformation = null;
1419
        $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+');
1420
        $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+');
1421
        $this->process = null;
1422
        $this->latestSignal = null;
1423
        $this->status = self::STATUS_READY;
1424
        $this->incrementalOutputOffset = 0;
1425
        $this->incrementalErrorOutputOffset = 0;
1426
    }
1427
 
1428
    /**
1429
     * Sends a POSIX signal to the process.
1430
     *
1431
     * @param int  $signal         A valid POSIX signal (see https://php.net/pcntl.constants)
1432
     * @param bool $throwException Whether to throw exception in case signal failed
1433
     *
1434
     * @throws LogicException   In case the process is not running
1435
     * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
1436
     * @throws RuntimeException In case of failure
1437
     */
1438
    private function doSignal(int $signal, bool $throwException): bool
1439
    {
1440
        if (null === $pid = $this->getPid()) {
1441
            if ($throwException) {
1442
                throw new LogicException('Cannot send signal on a non running process.');
1443
            }
1444
 
1445
            return false;
1446
        }
1447
 
1448
        if ('\\' === \DIRECTORY_SEPARATOR) {
1449
            exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode);
1450
            if ($exitCode && $this->isRunning()) {
1451
                if ($throwException) {
1452
                    throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output)));
1453
                }
1454
 
1455
                return false;
1456
            }
1457
        } else {
1458
            if (!$this->isSigchildEnabled()) {
1459
                $ok = @proc_terminate($this->process, $signal);
1460
            } elseif (\function_exists('posix_kill')) {
1461
                $ok = @posix_kill($pid, $signal);
1462
            } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) {
1463
                $ok = false === fgets($pipes[2]);
1464
            }
1465
            if (!$ok) {
1466
                if ($throwException) {
1467
                    throw new RuntimeException(sprintf('Error while sending signal "%s".', $signal));
1468
                }
1469
 
1470
                return false;
1471
            }
1472
        }
1473
 
1474
        $this->latestSignal = $signal;
1475
        $this->fallbackStatus['signaled'] = true;
1476
        $this->fallbackStatus['exitcode'] = -1;
1477
        $this->fallbackStatus['termsig'] = $this->latestSignal;
1478
 
1479
        return true;
1480
    }
1481
 
1482
    private function prepareWindowsCommandLine(string $cmd, array &$env): string
1483
    {
1484
        $uid = uniqid('', true);
1485
        $varCount = 0;
1486
        $varCache = [];
1487
        $cmd = preg_replace_callback(
1488
            '/"(?:(
1489
                [^"%!^]*+
1490
                (?:
1491
                    (?: !LF! | "(?:\^[%!^])?+" )
1492
                    [^"%!^]*+
1493
                )++
1494
            ) | [^"]*+ )"/x',
1495
            function ($m) use (&$env, &$varCache, &$varCount, $uid) {
1496
                if (!isset($m[1])) {
1497
                    return $m[0];
1498
                }
1499
                if (isset($varCache[$m[0]])) {
1500
                    return $varCache[$m[0]];
1501
                }
1502
                if (str_contains($value = $m[1], "\0")) {
1503
                    $value = str_replace("\0", '?', $value);
1504
                }
1505
                if (false === strpbrk($value, "\"%!\n")) {
1506
                    return '"'.$value.'"';
1507
                }
1508
 
1509
                $value = str_replace(['!LF!', '"^!"', '"^%"', '"^^"', '""'], ["\n", '!', '%', '^', '"'], $value);
1510
                $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"';
1511
                $var = $uid.++$varCount;
1512
 
1513
                $env[$var] = $value;
1514
 
1515
                return $varCache[$m[0]] = '!'.$var.'!';
1516
            },
1517
            $cmd
1518
        );
1519
 
1520
        $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')';
1521
        foreach ($this->processPipes->getFiles() as $offset => $filename) {
1522
            $cmd .= ' '.$offset.'>"'.$filename.'"';
1523
        }
1524
 
1525
        return $cmd;
1526
    }
1527
 
1528
    /**
1529
     * Ensures the process is running or terminated, throws a LogicException if the process has a not started.
1530
     *
1531
     * @throws LogicException if the process has not run
1532
     */
1533
    private function requireProcessIsStarted(string $functionName)
1534
    {
1535
        if (!$this->isStarted()) {
1536
            throw new LogicException(sprintf('Process must be started before calling "%s()".', $functionName));
1537
        }
1538
    }
1539
 
1540
    /**
1541
     * Ensures the process is terminated, throws a LogicException if the process has a status different than "terminated".
1542
     *
1543
     * @throws LogicException if the process is not yet terminated
1544
     */
1545
    private function requireProcessIsTerminated(string $functionName)
1546
    {
1547
        if (!$this->isTerminated()) {
1548
            throw new LogicException(sprintf('Process must be terminated before calling "%s()".', $functionName));
1549
        }
1550
    }
1551
 
1552
    /**
1553
     * Escapes a string to be used as a shell argument.
1554
     */
1555
    private function escapeArgument(?string $argument): string
1556
    {
1557
        if ('' === $argument || null === $argument) {
1558
            return '""';
1559
        }
1560
        if ('\\' !== \DIRECTORY_SEPARATOR) {
1561
            return "'".str_replace("'", "'\\''", $argument)."'";
1562
        }
1563
        if (str_contains($argument, "\0")) {
1564
            $argument = str_replace("\0", '?', $argument);
1565
        }
1566
        if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) {
1567
            return $argument;
1568
        }
1569
        $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument);
1570
 
1571
        return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"';
1572
    }
1573
 
1574
    private function replacePlaceholders(string $commandline, array $env)
1575
    {
1576
        return preg_replace_callback('/"\$\{:([_a-zA-Z]++[_a-zA-Z0-9]*+)\}"/', function ($matches) use ($commandline, $env) {
1577
            if (!isset($env[$matches[1]]) || false === $env[$matches[1]]) {
1578
                throw new InvalidArgumentException(sprintf('Command line is missing a value for parameter "%s": ', $matches[1]).$commandline);
1579
            }
1580
 
1581
            return $this->escapeArgument($env[$matches[1]]);
1582
        }, $commandline);
1583
    }
1584
 
1585
    private function getDefaultEnv(): array
1586
    {
1587
        $env = getenv();
1588
        $env = ('\\' === \DIRECTORY_SEPARATOR ? array_intersect_ukey($env, $_SERVER, 'strcasecmp') : array_intersect_key($env, $_SERVER)) ?: $env;
1589
 
1590
        return $_ENV + ('\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($env, $_ENV, 'strcasecmp') : $env);
1591
    }
1592
}