diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 9f9f8394e6bf8..c99a3f7ef6a89 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/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/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index 28931358dd68b..20ac09a1e93ca 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/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/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 5421305693149..15319c3d43dd2 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/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/src/Symfony/Component/Console/Tests/Fixtures/application_signalable.php b/src/Symfony/Component/Console/Tests/Fixtures/application_signalable.php new file mode 100644 index 0000000000000..0194703b2fe01 --- /dev/null +++ b/src/Symfony/Component/Console/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() + +;
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: