PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 3.12.3
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v3.12.3
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
wpfunnels / vendor / symfony / process / Process.php

Process.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 3.12.3, at vendor/symfony/process/Process.php

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