| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144 |
- <?php
- /*
- * This file is part of the Symfony package.
- *
- * (c) Fabien Potencier <fabien@symfony.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- namespace Symfony\Component\Console\Helper;
- /**
- * TerminalInputHelper stops Ctrl-C and similar signals from leaving the terminal in
- * an unusable state if its settings have been modified when reading user input.
- * This can be an issue on non-Windows platforms.
- *
- * Usage:
- *
- * $inputHelper = new TerminalInputHelper($inputStream);
- *
- * ...change terminal settings
- *
- * // Wait for input before all input reads
- * $inputHelper->waitForInput();
- *
- * ...read input
- *
- * // Call finish to restore terminal settings and signal handlers
- * $inputHelper->finish()
- *
- * @internal
- */
- final class TerminalInputHelper
- {
- /** @var resource */
- private $inputStream;
- private bool $isStdin;
- private string $initialState;
- private int $signalToKill = 0;
- private array $signalHandlers = [];
- private array $targetSignals = [];
- /**
- * @param resource $inputStream
- *
- * @throws \RuntimeException If unable to read terminal settings
- */
- public function __construct($inputStream)
- {
- if (!\is_string($state = shell_exec('stty -g'))) {
- throw new \RuntimeException('Unable to read the terminal settings.');
- }
- $this->inputStream = $inputStream;
- $this->initialState = $state;
- $this->isStdin = 'php://stdin' === stream_get_meta_data($inputStream)['uri'];
- $this->createSignalHandlers();
- }
- /**
- * Waits for input and terminates if sent a default signal.
- */
- public function waitForInput(): void
- {
- if ($this->isStdin) {
- $r = [$this->inputStream];
- $w = [];
- // Allow signal handlers to run, either before Enter is pressed
- // when icanon is enabled, or a single character is entered when
- // icanon is disabled
- while (0 === @stream_select($r, $w, $w, 0, 100)) {
- $r = [$this->inputStream];
- }
- }
- $this->checkForKillSignal();
- }
- /**
- * Restores terminal state and signal handlers.
- */
- public function finish(): void
- {
- // Safeguard in case an unhandled kill signal exists
- $this->checkForKillSignal();
- shell_exec('stty '.$this->initialState);
- $this->signalToKill = 0;
- foreach ($this->signalHandlers as $signal => $originalHandler) {
- pcntl_signal($signal, $originalHandler);
- }
- $this->signalHandlers = [];
- $this->targetSignals = [];
- }
- private function createSignalHandlers(): void
- {
- if (!\function_exists('pcntl_async_signals') || !\function_exists('pcntl_signal')) {
- return;
- }
- pcntl_async_signals(true);
- $this->targetSignals = [\SIGINT, \SIGQUIT, \SIGTERM];
- foreach ($this->targetSignals as $signal) {
- $this->signalHandlers[$signal] = pcntl_signal_get_handler($signal);
- pcntl_signal($signal, function ($signal) {
- // Save current state, then restore to initial state
- $currentState = shell_exec('stty -g');
- shell_exec('stty '.$this->initialState);
- $originalHandler = $this->signalHandlers[$signal];
- if (\is_callable($originalHandler)) {
- $originalHandler($signal);
- // Handler did not exit, so restore to current state
- shell_exec('stty '.$currentState);
- return;
- }
- // Not a callable, so SIG_DFL or SIG_IGN
- if (\SIG_DFL === $originalHandler) {
- $this->signalToKill = $signal;
- }
- });
- }
- }
- private function checkForKillSignal(): void
- {
- if (\in_array($this->signalToKill, $this->targetSignals, true)) {
- // Try posix_kill
- if (\function_exists('posix_kill')) {
- pcntl_signal($this->signalToKill, \SIG_DFL);
- posix_kill(getmypid(), $this->signalToKill);
- }
- // Best attempt fallback
- exit(128 + $this->signalToKill);
- }
- }
- }
|