From a89045900ae02d0f1b67ce9e627c3454fc25f0ff Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 21 Jan 2022 20:04:48 +0100 Subject: [PATCH 1/3] [Console] fix restoring stty mode on CTRL+C --- Application.php | 10 +++++++ Helper/QuestionHelper.php | 13 ++++++-- Tests/ApplicationTest.php | 35 ++++++++++++++++++++++ Tests/Fixtures/application_signalable.php | 36 +++++++++++++++++++++++ 4 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 Tests/Fixtures/application_signalable.php diff --git a/Application.php b/Application.php index 9f9f8394e..c99a3f7ef 100644 --- a/Application.php +++ b/Application.php @@ -952,6 +952,16 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI throw new RuntimeException('Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); } + if (Terminal::hasSttyAvailable()) { + $sttyMode = shell_exec('stty -g'); + + foreach ([\SIGINT, \SIGTERM] as $signal) { + $this->signalRegistry->register($signal, static function () use ($sttyMode) { + shell_exec('stty '.$sttyMode); + }); + } + } + if ($this->dispatcher) { foreach ($this->signalsToDispatchEvent as $signal) { $event = new ConsoleSignalEvent($command, $input, $output, $signal); diff --git a/Helper/QuestionHelper.php b/Helper/QuestionHelper.php index 28931358d..20ac09a1e 100644 --- a/Helper/QuestionHelper.php +++ b/Helper/QuestionHelper.php @@ -248,6 +248,9 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu $numMatches = \count($matches); $sttyMode = shell_exec('stty -g'); + $isStdin = 'php://stdin' === (stream_get_meta_data($inputStream)['uri'] ?? null); + $r = [$inputStream]; + $w = []; // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead) shell_exec('stty -icanon -echo'); @@ -257,11 +260,15 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu // Read a keypress while (!feof($inputStream)) { + while ($isStdin && 0 === @stream_select($r, $w, $w, 0, 100)) { + // Give signal handlers a chance to run + $r = [$inputStream]; + } $c = fread($inputStream, 1); // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false. if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) { - shell_exec(sprintf('stty %s', $sttyMode)); + shell_exec('stty '.$sttyMode); throw new MissingInputException('Aborted.'); } elseif ("\177" === $c) { // Backspace Character if (0 === $numMatches && 0 !== $i) { @@ -366,7 +373,7 @@ function ($match) use ($ret) { } // Reset stty so it behaves normally again - shell_exec(sprintf('stty %s', $sttyMode)); + shell_exec('stty '.$sttyMode); return $fullChoice; } @@ -427,7 +434,7 @@ private function getHiddenResponse(OutputInterface $output, $inputStream, bool $ $value = fgets($inputStream, 4096); if (self::$stty && Terminal::hasSttyAvailable()) { - shell_exec(sprintf('stty %s', $sttyMode)); + shell_exec('stty '.$sttyMode); } if (false === $value) { diff --git a/Tests/ApplicationTest.php b/Tests/ApplicationTest.php index 542130569..15319c3d4 100644 --- a/Tests/ApplicationTest.php +++ b/Tests/ApplicationTest.php @@ -38,9 +38,11 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Console\SignalRegistry\SignalRegistry; +use Symfony\Component\Console\Terminal; use Symfony\Component\Console\Tester\ApplicationTester; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\Process\Process; class ApplicationTest extends TestCase { @@ -1882,6 +1884,39 @@ public function testSignalableCommandInterfaceWithoutSignals() $application->add($command); $this->assertSame(0, $application->run(new ArrayInput(['signal']))); } + + /** + * @group tty + */ + public function testSignalableRestoresStty() + { + if (!Terminal::hasSttyAvailable()) { + $this->markTestSkipped('stty not available'); + } + + if (!SignalRegistry::isSupported()) { + $this->markTestSkipped('pcntl signals not available'); + } + + $previousSttyMode = shell_exec('stty -g'); + + $p = new Process(['php', __DIR__.'/Fixtures/application_signalable.php']); + $p->setTty(true); + $p->start(); + + for ($i = 0; $i < 10 && shell_exec('stty -g') === $previousSttyMode; ++$i) { + usleep(100000); + } + + $this->assertNotSame($previousSttyMode, shell_exec('stty -g')); + $p->signal(\SIGINT); + $p->wait(); + + $sttyMode = shell_exec('stty -g'); + shell_exec('stty '.$previousSttyMode); + + $this->assertSame($previousSttyMode, $sttyMode); + } } class CustomApplication extends Application diff --git a/Tests/Fixtures/application_signalable.php b/Tests/Fixtures/application_signalable.php new file mode 100644 index 000000000..0194703b2 --- /dev/null +++ b/Tests/Fixtures/application_signalable.php @@ -0,0 +1,36 @@ +setCode(function(InputInterface $input, OutputInterface $output) { + $this->getHelper('question') + ->ask($input, $output, new ChoiceQuestion('😊', ['y'])); + + return 0; + }) + ->run() + +; From 32ba2ac82767a50959f826e3d64c27923070a85b Mon Sep 17 00:00:00 2001 From: BrokenSourceCode Date: Tue, 25 Jan 2022 23:56:21 +0100 Subject: [PATCH 2/3] [Console] Fix PHP 8.1 deprecation in ChoiceQuestion --- Question/ChoiceQuestion.php | 6 +++--- Tests/Question/ChoiceQuestionTest.php | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Question/ChoiceQuestion.php b/Question/ChoiceQuestion.php index 72703fb16..6247ca716 100644 --- a/Question/ChoiceQuestion.php +++ b/Question/ChoiceQuestion.php @@ -131,18 +131,18 @@ private function getDefaultValidator(): callable return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) { if ($multiselect) { // Check for a separated comma values - if (!preg_match('/^[^,]+(?:,[^,]+)*$/', $selected, $matches)) { + if (!preg_match('/^[^,]+(?:,[^,]+)*$/', (string) $selected, $matches)) { throw new InvalidArgumentException(sprintf($errorMessage, $selected)); } - $selectedChoices = explode(',', $selected); + $selectedChoices = explode(',', (string) $selected); } else { $selectedChoices = [$selected]; } if ($this->isTrimmable()) { foreach ($selectedChoices as $k => $v) { - $selectedChoices[$k] = trim($v); + $selectedChoices[$k] = trim((string) $v); } } diff --git a/Tests/Question/ChoiceQuestionTest.php b/Tests/Question/ChoiceQuestionTest.php index 9db12f852..327f69ad7 100644 --- a/Tests/Question/ChoiceQuestionTest.php +++ b/Tests/Question/ChoiceQuestionTest.php @@ -19,14 +19,15 @@ class ChoiceQuestionTest extends TestCase /** * @dataProvider selectUseCases */ - public function testSelectUseCases($multiSelect, $answers, $expected, $message) + public function testSelectUseCases($multiSelect, $answers, $expected, $message, $default = null) { $question = new ChoiceQuestion('A question', [ 'First response', 'Second response', 'Third response', 'Fourth response', - ]); + null, + ], $default); $question->setMultiselect($multiSelect); @@ -59,6 +60,19 @@ public function selectUseCases() ['First response', 'Second response'], 'When passed multiple answers on MultiSelect, the defaultValidator must return these answers as an array', ], + [ + false, + [null], + null, + 'When used null as default single answer on singleSelect, the defaultValidator must return this answer as null', + ], + [ + false, + ['First response'], + 'First response', + 'When used a string as default single answer on singleSelect, the defaultValidator must return this answer as a string', + 'First response', + ], ]; } From 0259f01dbf9d77badddbbf4c2abb681f24c9cac6 Mon Sep 17 00:00:00 2001 From: James Gilliland Date: Thu, 23 Sep 2021 10:00:15 -0500 Subject: [PATCH 3/3] Silence isatty warnings during tty detection --- Helper/QuestionHelper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Helper/QuestionHelper.php b/Helper/QuestionHelper.php index 0516545bc..a4754b824 100644 --- a/Helper/QuestionHelper.php +++ b/Helper/QuestionHelper.php @@ -485,11 +485,11 @@ private function isInteractiveInput($inputStream): bool } if (\function_exists('stream_isatty')) { - return self::$stdinIsInteractive = stream_isatty(fopen('php://stdin', 'r')); + return self::$stdinIsInteractive = @stream_isatty(fopen('php://stdin', 'r')); } if (\function_exists('posix_isatty')) { - return self::$stdinIsInteractive = posix_isatty(fopen('php://stdin', 'r')); + return self::$stdinIsInteractive = @posix_isatty(fopen('php://stdin', 'r')); } if (!\function_exists('exec')) {