PluginProbe
SendWP / 1.4.4
SendWP v1.4.4
trunk 0.0.1 0.0.2 0.0.3 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1 1.2.10 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3 1.3.1 1.4.4 1.4.5 1.4.6 1.4.8
sendwp / vendor / symfony / process / Process.php

Process.php in SendWP 1.4.4, at vendor/symfony/process/Process.php

1,591 lines 50.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 0 => 'OK',
93 1 => 'General error',
94 2 => 'Misuse of shell builtins',
95
96 126 => 'Invoked command cannot execute',
97 127 => 'Command not found',
98 128 => 'Invalid exit argument',
99
100 // signals
101 129 => 'Hangup',
102 130 => 'Interrupt',
103 131 => 'Quit and dump core',
104 132 => 'Illegal instruction',
105 133 => 'Trace/breakpoint trap',
106 134 => 'Process aborted',
107 135 => 'Bus error: "access to undefined portion of memory object"',
108 136 => 'Floating point exception: "erroneous arithmetic operation"',
109 137 => 'Kill (terminate immediately)',
110 138 => 'User-defined 1',
111 139 => 'Segmentation violation',
112 140 => 'User-defined 2',
113 141 => 'Write to pipe with no one reading',
114 142 => 'Signal raised by alarm',
115 143 => 'Termination (request to terminate)',
116 // 144 - not defined
117 145 => 'Child process terminated, stopped (or continued*)',
118 146 => 'Continue if stopped',
119 147 => 'Stop executing temporarily',
120 148 => 'Terminal stop signal',
121 149 => 'Background process attempting to read from tty ("in")',
122 150 => 'Background process attempting to write to tty ("out")',
123 151 => 'Urgent data available on socket',
124 152 => 'CPU time limit exceeded',
125 153 => 'File size limit exceeded',
126 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"',
127 155 => 'Profiling timer expired',
128 // 156 - not defined
129 157 => 'Pollable event',
130 // 158 - not defined
131 159 => 'Bad syscall',
132 ];
133
134 /**
135 * @param array $command The command to run and its arguments listed as separate entries
136 * @param string|null $cwd The working directory or null to use the working dir of the current PHP process
137 * @param array|null $env The environment variables or null to use the same environment as the current PHP process
138 * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input
139 * @param int|float|null $timeout The timeout in seconds or null to disable
140 *
141 * @throws LogicException When proc_open is not installed
142 */
143 public function __construct(array $command, string $cwd = null, array $env = null, mixed $input = null, ?float $timeout = 60)
144 {
145 if (!\function_exists('proc_open')) {
146 throw new LogicException('The Process class relies on proc_open, which is not available on your PHP installation.');
147 }
148
149 $this->commandline = $command;
150 $this->cwd = $cwd;
151
152 // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started
153 // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected
154 // @see : https://bugs.php.net/51800
155 // @see : https://bugs.php.net/50524
156 if (null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR)) {
157 $this->cwd = getcwd();
158 }
159 if (null !== $env) {
160 $this->setEnv($env);
161 }
162
163 $this->setInput($input);
164 $this->setTimeout($timeout);
165 $this->useFileHandles = '\\' === \DIRECTORY_SEPARATOR;
166 $this->pty = false;
167 }
168
169 /**
170 * Creates a Process instance as a command-line to be run in a shell wrapper.
171 *
172 * Command-lines are parsed by the shell of your OS (/bin/sh on Unix-like, cmd.exe on Windows.)
173 * This allows using e.g. pipes or conditional execution. In this mode, signals are sent to the
174 * shell wrapper and not to your commands.
175 *
176 * In order to inject dynamic values into command-lines, we strongly recommend using placeholders.
177 * This will save escaping values, which is not portable nor secure anyway:
178 *
179 * $process = Process::fromShellCommandline('my_command "${:MY_VAR}"');
180 * $process->run(null, ['MY_VAR' => $theValue]);
181 *
182 * @param string $command The command line to pass to the shell of the OS
183 * @param string|null $cwd The working directory or null to use the working dir of the current PHP process
184 * @param array|null $env The environment variables or null to use the same environment as the current PHP process
185 * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input
186 * @param int|float|null $timeout The timeout in seconds or null to disable
187 *
188 * @throws LogicException When proc_open is not installed
189 */
190 public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, mixed $input = null, ?float $timeout = 60): static
191 {
192 $process = new static([], $cwd, $env, $input, $timeout);
193 $process->commandline = $command;
194
195 return $process;
196 }
197
198 public function __sleep(): array
199 {
200 throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
201 }
202
203 public function __wakeup()
204 {
205 throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
206 }
207
208 public function __destruct()
209 {
210 if ($this->options['create_new_console'] ?? false) {
211 $this->processPipes->close();
212 } else {
213 $this->stop(0);
214 }
215 }
216
217 public function __clone()
218 {
219 $this->resetProcessData();
220 }
221
222 /**
223 * Runs the process.
224 *
225 * The callback receives the type of output (out or err) and
226 * some bytes from the output in real-time. It allows to have feedback
227 * from the independent process during execution.
228 *
229 * The STDOUT and STDERR are also available after the process is finished
230 * via the getOutput() and getErrorOutput() methods.
231 *
232 * @param callable|null $callback A PHP callback to run whenever there is some
233 * output available on STDOUT or STDERR
234 *
235 * @return int The exit status code
236 *
237 * @throws RuntimeException When process can't be launched
238 * @throws RuntimeException When process is already running
239 * @throws ProcessTimedOutException When process timed out
240 * @throws ProcessSignaledException When process stopped after receiving signal
241 * @throws LogicException In case a callback is provided and output has been disabled
242 *
243 * @final
244 */
245 public function run(callable $callback = null, array $env = []): int
246 {
247 $this->start($callback, $env);
248
249 return $this->wait();
250 }
251
252 /**
253 * Runs the process.
254 *
255 * This is identical to run() except that an exception is thrown if the process
256 * exits with a non-zero exit code.
257 *
258 * @return $this
259 *
260 * @throws ProcessFailedException if the process didn't terminate successfully
261 *
262 * @final
263 */
264 public function mustRun(callable $callback = null, array $env = []): static
265 {
266 if (0 !== $this->run($callback, $env)) {
267 throw new ProcessFailedException($this);
268 }
269
270 return $this;
271 }
272
273 /**
274 * Starts the process and returns after writing the input to STDIN.
275 *
276 * This method blocks until all STDIN data is sent to the process then it
277 * returns while the process runs in the background.
278 *
279 * The termination of the process can be awaited with wait().
280 *
281 * The callback receives the type of output (out or err) and some bytes from
282 * the output in real-time while writing the standard input to the process.
283 * It allows to have feedback from the independent process during execution.
284 *
285 * @param callable|null $callback A PHP callback to run whenever there is some
286 * output available on STDOUT or STDERR
287 *
288 * @throws RuntimeException When process can't be launched
289 * @throws RuntimeException When process is already running
290 * @throws LogicException In case a callback is provided and output has been disabled
291 */
292 public function start(callable $callback = null, array $env = [])
293 {
294 if ($this->isRunning()) {
295 throw new RuntimeException('Process is already running.');
296 }
297
298 $this->resetProcessData();
299 $this->starttime = $this->lastOutputTime = microtime(true);
300 $this->callback = $this->buildCallback($callback);
301 $this->hasCallback = null !== $callback;
302 $descriptors = $this->getDescriptors();
303
304 if ($this->env) {
305 $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->env, $env, 'strcasecmp') : $this->env;
306 }
307
308 $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->getDefaultEnv(), $env, 'strcasecmp') : $this->getDefaultEnv();
309
310 if (\is_array($commandline = $this->commandline)) {
311 $commandline = implode(' ', array_map($this->escapeArgument(...), $commandline));
312
313 if ('\\' !== \DIRECTORY_SEPARATOR) {
314 // exec is mandatory to deal with sending a signal to the process
315 $commandline = 'exec '.$commandline;
316 }
317 } else {
318 $commandline = $this->replacePlaceholders($commandline, $env);
319 }
320
321 if ('\\' === \DIRECTORY_SEPARATOR) {
322 $commandline = $this->prepareWindowsCommandLine($commandline, $env);
323 } elseif (!$this->useFileHandles && $this->isSigchildEnabled()) {
324 // last exit code is output on the fourth pipe and caught to work around --enable-sigchild
325 $descriptors[3] = ['pipe', 'w'];
326
327 // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input
328 $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;';
329 $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code';
330
331 // Workaround for the bug, when PTS functionality is enabled.
332 // @see : https://bugs.php.net/69442
333 $ptsWorkaround = fopen(__FILE__, 'r');
334 }
335
336 $envPairs = [];
337 foreach ($env as $k => $v) {
338 if (false !== $v && false === \in_array($k, ['argc', 'argv', 'ARGC', 'ARGV'], true)) {
339 $envPairs[] = $k.'='.$v;
340 }
341 }
342
343 if (!is_dir($this->cwd)) {
344 throw new RuntimeException(sprintf('The provided cwd "%s" does not exist.', $this->cwd));
345 }
346
347 $this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options);
348
349 if (!\is_resource($this->process)) {
350 throw new RuntimeException('Unable to launch a new process.');
351 }
352 $this->status = self::STATUS_STARTED;
353
354 if (isset($descriptors[3])) {
355 $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]);
356 }
357
358 if ($this->tty) {
359 return;
360 }
361
362 $this->updateStatus(false);
363 $this->checkTimeout();
364 }
365
366 /**
367 * Restarts the process.
368 *
369 * Be warned that the process is cloned before being started.
370 *
371 * @param callable|null $callback A PHP callback to run whenever there is some
372 * output available on STDOUT or STDERR
373 *
374 * @throws RuntimeException When process can't be launched
375 * @throws RuntimeException When process is already running
376 *
377 * @see start()
378 *
379 * @final
380 */
381 public function restart(callable $callback = null, array $env = []): static
382 {
383 if ($this->isRunning()) {
384 throw new RuntimeException('Process is already running.');
385 }
386
387 $process = clone $this;
388 $process->start($callback, $env);
389
390 return $process;
391 }
392
393 /**
394 * Waits for the process to terminate.
395 *
396 * The callback receives the type of output (out or err) and some bytes
397 * from the output in real-time while writing the standard input to the process.
398 * It allows to have feedback from the independent process during execution.
399 *
400 * @param callable|null $callback A valid PHP callback
401 *
402 * @return int The exitcode of the process
403 *
404 * @throws ProcessTimedOutException When process timed out
405 * @throws ProcessSignaledException When process stopped after receiving signal
406 * @throws LogicException When process is not yet started
407 */
408 public function wait(callable $callback = null): int
409 {
410 $this->requireProcessIsStarted(__FUNCTION__);
411
412 $this->updateStatus(false);
413
414 if (null !== $callback) {
415 if (!$this->processPipes->haveReadSupport()) {
416 $this->stop(0);
417 throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait".');
418 }
419 $this->callback = $this->buildCallback($callback);
420 }
421
422 do {
423 $this->checkTimeout();
424 $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
425 $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
426 } while ($running);
427
428 while ($this->isRunning()) {
429 $this->checkTimeout();
430 usleep(1000);
431 }
432
433 if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) {
434 throw new ProcessSignaledException($this);
435 }
436
437 return $this->exitcode;
438 }
439
440 /**
441 * Waits until the callback returns true.
442 *
443 * The callback receives the type of output (out or err) and some bytes
444 * from the output in real-time while writing the standard input to the process.
445 * It allows to have feedback from the independent process during execution.
446 *
447 * @throws RuntimeException When process timed out
448 * @throws LogicException When process is not yet started
449 * @throws ProcessTimedOutException In case the timeout was reached
450 */
451 public function waitUntil(callable $callback): bool
452 {
453 $this->requireProcessIsStarted(__FUNCTION__);
454 $this->updateStatus(false);
455
456 if (!$this->processPipes->haveReadSupport()) {
457 $this->stop(0);
458 throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::waitUntil".');
459 }
460 $callback = $this->buildCallback($callback);
461
462 $ready = false;
463 while (true) {
464 $this->checkTimeout();
465 $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen();
466 $output = $this->processPipes->readAndWrite($running, '\\' !== \DIRECTORY_SEPARATOR || !$running);
467
468 foreach ($output as $type => $data) {
469 if (3 !== $type) {
470 $ready = $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data) || $ready;
471 } elseif (!isset($this->fallbackStatus['signaled'])) {
472 $this->fallbackStatus['exitcode'] = (int) $data;
473 }
474 }
475 if ($ready) {
476 return true;
477 }
478 if (!$running) {
479 return false;
480 }
481
482 usleep(1000);
483 }
484 }
485
486 /**
487 * Returns the Pid (process identifier), if applicable.
488 *
489 * @return int|null The process id if running, null otherwise
490 */
491 public function getPid(): ?int
492 {
493 return $this->isRunning() ? $this->processInformation['pid'] : null;
494 }
495
496 /**
497 * Sends a POSIX signal to the process.
498 *
499 * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants)
500 *
501 * @return $this
502 *
503 * @throws LogicException In case the process is not running
504 * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
505 * @throws RuntimeException In case of failure
506 */
507 public function signal(int $signal): static
508 {
509 $this->doSignal($signal, true);
510
511 return $this;
512 }
513
514 /**
515 * Disables fetching output and error output from the underlying process.
516 *
517 * @return $this
518 *
519 * @throws RuntimeException In case the process is already running
520 * @throws LogicException if an idle timeout is set
521 */
522 public function disableOutput(): static
523 {
524 if ($this->isRunning()) {
525 throw new RuntimeException('Disabling output while the process is running is not possible.');
526 }
527 if (null !== $this->idleTimeout) {
528 throw new LogicException('Output cannot be disabled while an idle timeout is set.');
529 }
530
531 $this->outputDisabled = true;
532
533 return $this;
534 }
535
536 /**
537 * Enables fetching output and error output from the underlying process.
538 *
539 * @return $this
540 *
541 * @throws RuntimeException In case the process is already running
542 */
543 public function enableOutput(): static
544 {
545 if ($this->isRunning()) {
546 throw new RuntimeException('Enabling output while the process is running is not possible.');
547 }
548
549 $this->outputDisabled = false;
550
551 return $this;
552 }
553
554 /**
555 * Returns true in case the output is disabled, false otherwise.
556 */
557 public function isOutputDisabled(): bool
558 {
559 return $this->outputDisabled;
560 }
561
562 /**
563 * Returns the current output of the process (STDOUT).
564 *
565 * @throws LogicException in case the output has been disabled
566 * @throws LogicException In case the process is not started
567 */
568 public function getOutput(): string
569 {
570 $this->readPipesForOutput(__FUNCTION__);
571
572 if (false === $ret = stream_get_contents($this->stdout, -1, 0)) {
573 return '';
574 }
575
576 return $ret;
577 }
578
579 /**
580 * Returns the output incrementally.
581 *
582 * In comparison with the getOutput method which always return the whole
583 * output, this one returns the new output since the last call.
584 *
585 * @throws LogicException in case the output has been disabled
586 * @throws LogicException In case the process is not started
587 */
588 public function getIncrementalOutput(): string
589 {
590 $this->readPipesForOutput(__FUNCTION__);
591
592 $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset);
593 $this->incrementalOutputOffset = ftell($this->stdout);
594
595 if (false === $latest) {
596 return '';
597 }
598
599 return $latest;
600 }
601
602 /**
603 * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR).
604 *
605 * @param int $flags A bit field of Process::ITER_* flags
606 *
607 * @return \Generator<string, string>
608 *
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
877 * @param int|null $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9)
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 fn ($type, $data): bool => null !== $callback && $callback($type, $data);
1259 }
1260
1261 $out = self::OUT;
1262
1263 return function ($type, $data) use ($callback, $out): bool {
1264 if ($out == $type) {
1265 $this->addOutput($data);
1266 } else {
1267 $this->addErrorOutput($data);
1268 }
1269
1270 return null !== $callback && $callback($type, $data);
1271 };
1272 }
1273
1274 /**
1275 * Updates the status of the process, reads pipes.
1276 *
1277 * @param bool $blocking Whether to use a blocking read call
1278 */
1279 protected function updateStatus(bool $blocking)
1280 {
1281 if (self::STATUS_STARTED !== $this->status) {
1282 return;
1283 }
1284
1285 $this->processInformation = proc_get_status($this->process);
1286 $running = $this->processInformation['running'];
1287
1288 $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running);
1289
1290 if ($this->fallbackStatus && $this->isSigchildEnabled()) {
1291 $this->processInformation = $this->fallbackStatus + $this->processInformation;
1292 }
1293
1294 if (!$running) {
1295 $this->close();
1296 }
1297 }
1298
1299 /**
1300 * Returns whether PHP has been compiled with the '--enable-sigchild' option or not.
1301 */
1302 protected function isSigchildEnabled(): bool
1303 {
1304 if (null !== self::$sigchild) {
1305 return self::$sigchild;
1306 }
1307
1308 if (!\function_exists('phpinfo')) {
1309 return self::$sigchild = false;
1310 }
1311
1312 ob_start();
1313 phpinfo(\INFO_GENERAL);
1314
1315 return self::$sigchild = str_contains(ob_get_clean(), '--enable-sigchild');
1316 }
1317
1318 /**
1319 * Reads pipes for the freshest output.
1320 *
1321 * @param string $caller The name of the method that needs fresh outputs
1322 * @param bool $blocking Whether to use blocking calls or not
1323 *
1324 * @throws LogicException in case output has been disabled or process is not started
1325 */
1326 private function readPipesForOutput(string $caller, bool $blocking = false)
1327 {
1328 if ($this->outputDisabled) {
1329 throw new LogicException('Output has been disabled.');
1330 }
1331
1332 $this->requireProcessIsStarted($caller);
1333
1334 $this->updateStatus($blocking);
1335 }
1336
1337 /**
1338 * Validates and returns the filtered timeout.
1339 *
1340 * @throws InvalidArgumentException if the given timeout is a negative number
1341 */
1342 private function validateTimeout(?float $timeout): ?float
1343 {
1344 $timeout = (float) $timeout;
1345
1346 if (0.0 === $timeout) {
1347 $timeout = null;
1348 } elseif ($timeout < 0) {
1349 throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
1350 }
1351
1352 return $timeout;
1353 }
1354
1355 /**
1356 * Reads pipes, executes callback.
1357 *
1358 * @param bool $blocking Whether to use blocking calls or not
1359 * @param bool $close Whether to close file handles or not
1360 */
1361 private function readPipes(bool $blocking, bool $close)
1362 {
1363 $result = $this->processPipes->readAndWrite($blocking, $close);
1364
1365 $callback = $this->callback;
1366 foreach ($result as $type => $data) {
1367 if (3 !== $type) {
1368 $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data);
1369 } elseif (!isset($this->fallbackStatus['signaled'])) {
1370 $this->fallbackStatus['exitcode'] = (int) $data;
1371 }
1372 }
1373 }
1374
1375 /**
1376 * Closes process resource, closes file handles, sets the exitcode.
1377 *
1378 * @return int The exitcode
1379 */
1380 private function close(): int
1381 {
1382 $this->processPipes->close();
1383 if (\is_resource($this->process)) {
1384 proc_close($this->process);
1385 }
1386 $this->exitcode = $this->processInformation['exitcode'];
1387 $this->status = self::STATUS_TERMINATED;
1388
1389 if (-1 === $this->exitcode) {
1390 if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) {
1391 // if process has been signaled, no exitcode but a valid termsig, apply Unix convention
1392 $this->exitcode = 128 + $this->processInformation['termsig'];
1393 } elseif ($this->isSigchildEnabled()) {
1394 $this->processInformation['signaled'] = true;
1395 $this->processInformation['termsig'] = -1;
1396 }
1397 }
1398
1399 // Free memory from self-reference callback created by buildCallback
1400 // Doing so in other contexts like __destruct or by garbage collector is ineffective
1401 // Now pipes are closed, so the callback is no longer necessary
1402 $this->callback = null;
1403
1404 return $this->exitcode;
1405 }
1406
1407 /**
1408 * Resets data related to the latest run of the process.
1409 */
1410 private function resetProcessData()
1411 {
1412 $this->starttime = null;
1413 $this->callback = null;
1414 $this->exitcode = null;
1415 $this->fallbackStatus = [];
1416 $this->processInformation = null;
1417 $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+');
1418 $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+');
1419 $this->process = null;
1420 $this->latestSignal = null;
1421 $this->status = self::STATUS_READY;
1422 $this->incrementalOutputOffset = 0;
1423 $this->incrementalErrorOutputOffset = 0;
1424 }
1425
1426 /**
1427 * Sends a POSIX signal to the process.
1428 *
1429 * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants)
1430 * @param bool $throwException Whether to throw exception in case signal failed
1431 *
1432 * @throws LogicException In case the process is not running
1433 * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed
1434 * @throws RuntimeException In case of failure
1435 */
1436 private function doSignal(int $signal, bool $throwException): bool
1437 {
1438 if (null === $pid = $this->getPid()) {
1439 if ($throwException) {
1440 throw new LogicException('Cannot send signal on a non running process.');
1441 }
1442
1443 return false;
1444 }
1445
1446 if ('\\' === \DIRECTORY_SEPARATOR) {
1447 exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode);
1448 if ($exitCode && $this->isRunning()) {
1449 if ($throwException) {
1450 throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output)));
1451 }
1452
1453 return false;
1454 }
1455 } else {
1456 if (!$this->isSigchildEnabled()) {
1457 $ok = @proc_terminate($this->process, $signal);
1458 } elseif (\function_exists('posix_kill')) {
1459 $ok = @posix_kill($pid, $signal);
1460 } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) {
1461 $ok = false === fgets($pipes[2]);
1462 }
1463 if (!$ok) {
1464 if ($throwException) {
1465 throw new RuntimeException(sprintf('Error while sending signal "%s".', $signal));
1466 }
1467
1468 return false;
1469 }
1470 }
1471
1472 $this->latestSignal = $signal;
1473 $this->fallbackStatus['signaled'] = true;
1474 $this->fallbackStatus['exitcode'] = -1;
1475 $this->fallbackStatus['termsig'] = $this->latestSignal;
1476
1477 return true;
1478 }
1479
1480 private function prepareWindowsCommandLine(string $cmd, array &$env): string
1481 {
1482 $uid = uniqid('', true);
1483 $cmd = preg_replace_callback(
1484 '/"(?:(
1485 [^"%!^]*+
1486 (?:
1487 (?: !LF! | "(?:\^[%!^])?+" )
1488 [^"%!^]*+
1489 )++
1490 ) | [^"]*+ )"/x',
1491 function ($m) use (&$env, $uid) {
1492 static $varCount = 0;
1493 static $varCache = [];
1494 if (!isset($m[1])) {
1495 return $m[0];
1496 }
1497 if (isset($varCache[$m[0]])) {
1498 return $varCache[$m[0]];
1499 }
1500 if (str_contains($value = $m[1], "\0")) {
1501 $value = str_replace("\0", '?', $value);
1502 }
1503 if (false === strpbrk($value, "\"%!\n")) {
1504 return '"'.$value.'"';
1505 }
1506
1507 $value = str_replace(['!LF!', '"^!"', '"^%"', '"^^"', '""'], ["\n", '!', '%', '^', '"'], $value);
1508 $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"';
1509 $var = $uid.++$varCount;
1510
1511 $env[$var] = $value;
1512
1513 return $varCache[$m[0]] = '!'.$var.'!';
1514 },
1515 $cmd
1516 );
1517
1518 $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')';
1519 foreach ($this->processPipes->getFiles() as $offset => $filename) {
1520 $cmd .= ' '.$offset.'>"'.$filename.'"';
1521 }
1522
1523 return $cmd;
1524 }
1525
1526 /**
1527 * Ensures the process is running or terminated, throws a LogicException if the process has a not started.
1528 *
1529 * @throws LogicException if the process has not run
1530 */
1531 private function requireProcessIsStarted(string $functionName)
1532 {
1533 if (!$this->isStarted()) {
1534 throw new LogicException(sprintf('Process must be started before calling "%s()".', $functionName));
1535 }
1536 }
1537
1538 /**
1539 * Ensures the process is terminated, throws a LogicException if the process has a status different than "terminated".
1540 *
1541 * @throws LogicException if the process is not yet terminated
1542 */
1543 private function requireProcessIsTerminated(string $functionName)
1544 {
1545 if (!$this->isTerminated()) {
1546 throw new LogicException(sprintf('Process must be terminated before calling "%s()".', $functionName));
1547 }
1548 }
1549
1550 /**
1551 * Escapes a string to be used as a shell argument.
1552 */
1553 private function escapeArgument(?string $argument): string
1554 {
1555 if ('' === $argument || null === $argument) {
1556 return '""';
1557 }
1558 if ('\\' !== \DIRECTORY_SEPARATOR) {
1559 return "'".str_replace("'", "'\\''", $argument)."'";
1560 }
1561 if (str_contains($argument, "\0")) {
1562 $argument = str_replace("\0", '?', $argument);
1563 }
1564 if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) {
1565 return $argument;
1566 }
1567 $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument);
1568
1569 return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"';
1570 }
1571
1572 private function replacePlaceholders(string $commandline, array $env): string
1573 {
1574 return preg_replace_callback('/"\$\{:([_a-zA-Z]++[_a-zA-Z0-9]*+)\}"/', function ($matches) use ($commandline, $env) {
1575 if (!isset($env[$matches[1]]) || false === $env[$matches[1]]) {
1576 throw new InvalidArgumentException(sprintf('Command line is missing a value for parameter "%s": ', $matches[1]).$commandline);
1577 }
1578
1579 return $this->escapeArgument($env[$matches[1]]);
1580 }, $commandline);
1581 }
1582
1583 private function getDefaultEnv(): array
1584 {
1585 $env = getenv();
1586 $env = ('\\' === \DIRECTORY_SEPARATOR ? array_intersect_ukey($env, $_SERVER, 'strcasecmp') : array_intersect_key($env, $_SERVER)) ?: $env;
1587
1588 return $_ENV + ('\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($env, $_ENV, 'strcasecmp') : $env);
1589 }
1590 }
1591