PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.5.1
Defender Security – Malware Scanner, Login Security & Firewall v5.5.1
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / vendor / symfony / process / Process.php

Process.php in Defender Security – Malware Scanner, Login Security & Firewall 5.5.1, at vendor/symfony/process/Process.php

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