Process::hasBeenStopped()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 5
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\Process;
13
14
use Symfony\Component\Process\Exception\InvalidArgumentException;
15
use Symfony\Component\Process\Exception\LogicException;
16
use Symfony\Component\Process\Exception\ProcessFailedException;
17
use Symfony\Component\Process\Exception\ProcessSignaledException;
18
use Symfony\Component\Process\Exception\ProcessTimedOutException;
19
use Symfony\Component\Process\Exception\RuntimeException;
20
use Symfony\Component\Process\Pipes\PipesInterface;
21
use Symfony\Component\Process\Pipes\UnixPipes;
22
use Symfony\Component\Process\Pipes\WindowsPipes;
23
24
/**
25
 * Process is a thin wrapper around proc_* functions to easily
26
 * start independent PHP processes.
27
 *
28
 * @author Fabien Potencier <[email protected]>
29
 * @author Romain Neutron <[email protected]>
30
 */
31
class Process implements \IteratorAggregate
32
{
33
    public const ERR = 'err';
34
    public const OUT = 'out';
35
36
    public const STATUS_READY = 'ready';
37
    public const STATUS_STARTED = 'started';
38
    public const STATUS_TERMINATED = 'terminated';
39
40
    public const STDIN = 0;
41
    public const STDOUT = 1;
42
    public const STDERR = 2;
43
44
    // Timeout Precision in seconds.
45
    public const TIMEOUT_PRECISION = 0.2;
46
47
    public const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking
48
    public const ITER_KEEP_OUTPUT = 2;  // By default, outputs are cleared while iterating, use this flag to keep them in memory
49
    public const ITER_SKIP_OUT = 4;     // Use this flag to skip STDOUT while iterating
50
    public const ITER_SKIP_ERR = 8;     // Use this flag to skip STDERR while iterating
51
52
    private $callback;
53
    private $hasCallback = false;
54
    private $commandline;
55
    private $cwd;
56
    private $env;
57
    private $input;
58
    private $starttime;
59
    private $lastOutputTime;
60
    private $timeout;
61
    private $idleTimeout;
62
    private $exitcode;
63
    private $fallbackStatus = [];
64
    private $processInformation;
65
    private $outputDisabled = false;
66
    private $stdout;
67
    private $stderr;
68
    private $process;
69
    private $status = self::STATUS_READY;
70
    private $incrementalOutputOffset = 0;
71
    private $incrementalErrorOutputOffset = 0;
72
    private $tty = false;
73
    private $pty;
74
    private $options = ['suppress_errors' => true, 'bypass_shell' => true];
75
76
    private $useFileHandles = false;
77
    /** @var PipesInterface */
78
    private $processPipes;
79
80
    private $latestSignal;
81
82
    private static $sigchild;
83
84
    /**
85
     * Exit codes translation table.
86
     *
87
     * User-defined errors must use exit codes in the 64-113 range.
88
     */
89
    public static $exitCodes = [
90
        0 => 'OK',
91
        1 => 'General error',
92
        2 => 'Misuse of shell builtins',
93
94
        126 => 'Invoked command cannot execute',
95
        127 => 'Command not found',
96
        128 => 'Invalid exit argument',
97
98
        // signals
99
        129 => 'Hangup',
100
        130 => 'Interrupt',
101
        131 => 'Quit and dump core',
102
        132 => 'Illegal instruction',
103
        133 => 'Trace/breakpoint trap',
104
        134 => 'Process aborted',
105
        135 => 'Bus error: "access to undefined portion of memory object"',
106
        136 => 'Floating point exception: "erroneous arithmetic operation"',
107
        137 => 'Kill (terminate immediately)',
108
        138 => 'User-defined 1',
109
        139 => 'Segmentation violation',
110
        140 => 'User-defined 2',
111
        141 => 'Write to pipe with no one reading',
112
        142 => 'Signal raised by alarm',
113
        143 => 'Termination (request to terminate)',
114
        // 144 - not defined
115
        145 => 'Child process terminated, stopped (or continued*)',
116
        146 => 'Continue if stopped',
117
        147 => 'Stop executing temporarily',
118
        148 => 'Terminal stop signal',
119
        149 => 'Background process attempting to read from tty ("in")',
120
        150 => 'Background process attempting to write to tty ("out")',
121
        151 => 'Urgent data available on socket',
122
        152 => 'CPU time limit exceeded',
123
        153 => 'File size limit exceeded',
124
        154 => 'Signal raised by timer counting virtual time: "virtual timer expired"',
125
        155 => 'Profiling timer expired',
126
        // 156 - not defined
127
        157 => 'Pollable event',
128
        // 158 - not defined
129
        159 => 'Bad syscall',
130
    ];
131
132
    /**
133
     * @param array          $command The command to run and its arguments listed as separate entries
134
     * @param string|null    $cwd     The working directory or null to use the working dir of the current PHP process
135
     * @param array|null     $env     The environment variables or null to use the same environment as the current PHP process
136
     * @param mixed          $input   The input as stream resource, scalar or \Traversable, or null for no input
137
     * @param int|float|null $timeout The timeout in seconds or null to disable
138
     *
139
     * @throws LogicException When proc_open is not installed
140
     */
141
    public function __construct(array $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60)
142
    {
143
        if (!\function_exists('proc_open')) {
144
            throw new LogicException('The Process class relies on proc_open, which is not available on your PHP installation.');
145
        }
146
147
        $this->commandline = $command;
148
        $this->cwd = $cwd;
149
150
        // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started
151
        // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected
152
        // @see : https://bugs.php.net/51800
153
        // @see : https://bugs.php.net/50524
154
        if (null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR)) {
155
            $this->cwd = getcwd();
156
        }
157
        if (null !== $env) {
158
            $this->setEnv($env);
159
        }
160
161
        $this->setInput($input);
162
        $this->setTimeout($timeout);
163
        $this->useFileHandles = '\\' === \DIRECTORY_SEPARATOR;
164
        $this->pty = false;
165
    }
166
167
    /**
168
     * Creates a Process instance as a command-line to be run in a shell wrapper.
169
     *
170
     * Command-lines are parsed by the shell of your OS (/bin/sh on Unix-like, cmd.exe on Windows.)
171
     * This allows using e.g. pipes or conditional execution. In this mode, signals are sent to the
172
     * shell wrapper and not to your commands.
173
     *
174
     * In order to inject dynamic values into command-lines, we strongly recommend using placeholders.
175
     * This will save escaping values, which is not portable nor secure anyway:
176
     *
177
     *   $process = Process::fromShellCommandline('my_command "$MY_VAR"');
178
     *   $process->run(null, ['MY_VAR' => $theValue]);
179
     *
180
     * @param string         $command The command line to pass to the shell of the OS
181
     * @param string|null    $cwd     The working directory or null to use the working dir of the current PHP process
182
     * @param array|null     $env     The environment variables or null to use the same environment as the current PHP process
183
     * @param mixed          $input   The input as stream resource, scalar or \Traversable, or null for no input
184
     * @param int|float|null $timeout The timeout in seconds or null to disable
185
     *
186
     * @return static
187
     *
188
     * @throws LogicException When proc_open is not installed
189
     */
190
    public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60)
191
    {
192
        $process = new static([], $cwd, $env, $input, $timeout);
193
        $process->commandline = $command;
194
195
        return $process;
196
    }
197
198
    public function __sleep()
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 = []): self
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 += $this->env;
306
        }
307
308
        $env += $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');
0 ignored issues
show
Unused Code introduced by
The assignment to $ptsWorkaround is dead and can be removed.
Loading history...
334
        }
335
336
        $envPairs = [];
337
        foreach ($env as $k => $v) {
338
            if (false !== $v) {
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
     * @return static
375
     *
376
     * @throws RuntimeException When process can't be launched
377
     * @throws RuntimeException When process is already running
378
     *
379
     * @see start()
380
     *
381
     * @final
382
     */
383
    public function restart(callable $callback = null, array $env = []): self
384
    {
385
        if ($this->isRunning()) {
386
            throw new RuntimeException('Process is already running.');
387
        }
388
389
        $process = clone $this;
390
        $process->start($callback, $env);
391
392
        return $process;
393
    }
394
395
    /**
396
     * Waits for the process to terminate.
397
     *
398
     * The callback receives the type of output (out or err) and some bytes
399
     * from the output in real-time while writing the standard input to the process.
400
     * It allows to have feedback from the independent process during execution.
401
     *
402
     * @param callable|null $callback A valid PHP callback
403
     *
404
     * @return int The exitcode of the process
405
     *
406
     * @throws ProcessTimedOutException When process timed out
407
     * @throws ProcessSignaledException When process stopped after receiving signal
408
     * @throws LogicException           When process is not yet started
409
     */
410
    public function wait(callable $callback = null)
411
    {
412
        $this->requireProcessIsStarted(__FUNCTION__);
413
414
        $this->updateStatus(false);
415
416
        if (null !== $callback) {
417
            if (!$this->processPipes->haveReadSupport()) {
418
                $this->stop(0);
419
                throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait".');
420
            }
421
            $this->callback = $this->buildCallback($callback);
422
        }
423
424
        do {
425
            $this->checkTimeout();
426
            $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
427
            $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
428
        } while ($running);
429
430
        while ($this->isRunning()) {
431
            $this->checkTimeout();
432
            usleep(1000);
433
        }
434
435
        if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) {
436
            throw new ProcessSignaledException($this);
437
        }
438
439
        return $this->exitcode;
440
    }
441
442
    /**
443
     * Waits until the callback returns true.
444
     *
445
     * The callback receives the type of output (out or err) and some bytes
446
     * from the output in real-time while writing the standard input to the process.
447
     * It allows to have feedback from the independent process during execution.
448
     *
449
     * @throws RuntimeException         When process timed out
450
     * @throws LogicException           When process is not yet started
451
     * @throws ProcessTimedOutException In case the timeout was reached
452
     */
453
    public function waitUntil(callable $callback): bool
454
    {
455
        $this->requireProcessIsStarted(__FUNCTION__);
456
        $this->updateStatus(false);
457
458
        if (!$this->processPipes->haveReadSupport()) {
459
            $this->stop(0);
460
            throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::waitUntil".');
461
        }
462
        $callback = $this->buildCallback($callback);
463
464
        $ready = false;
465
        while (true) {
466
            $this->checkTimeout();
467
            $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
468
            $output = $this->processPipes->readAndWrite($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
469
470
            foreach ($output as $type => $data) {
471
                if (3 !== $type) {
472
                    $ready = $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data) || $ready;
473
                } elseif (!isset($this->fallbackStatus['signaled'])) {
474
                    $this->fallbackStatus['exitcode'] = (int) $data;
475
                }
476
            }
477
            if ($ready) {
478
                return true;
479
            }
480
            if (!$running) {
481
                return false;
482
            }
483
484
            usleep(1000);
485
        }
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return boolean. Consider adding a return statement or allowing null as return value.

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

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

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

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

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

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