PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 3.0.1
Defender Security – Malware Scanner, Login Security & Firewall v3.0.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 3.0.1, at vendor/symfony/process/Process.php

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