Skip to content

Commit

Permalink
Merge branch '5.4' into 6.0
Browse files Browse the repository at this point in the history
* 5.4: (21 commits)
  [Finder] Fix finding VCS re-included files in excluded directory
  [Yaml] Improve the deprecation warnings for octal numbers to suggest migrating
  Fix Choice constraint with associative choices array
  [Form] UrlType should not add protocol to emails
  [Dotenv] Fix bootEnv() override with .env.local.php when the env key already exists
  Silence isatty warnings during tty detection
  [Serializer] Fix AbstractObjectNormalizer not considering pseudo type false
  [Notifier] Fix encoding of messages with FreeMobileTransport
  [Cache] workaround PHP crash
  [Console] Fix PHP 8.1 deprecation in ChoiceQuestion
  [HttpKernel] Fix compatibility with php bridge and already started php sessions
  [Notifier] smsapi-notifier - correct encoding
  Replaced full CoC text with link to documentation
  Making the parser stateless
  [Console] fix restoring stty mode on CTRL+C
  fix merge (bis)
  fix merge
  [Process] Avoid calling fclose on an already closed resource
  [GHA] test tty group
  [DI] Fix tests on PHP 7.1
  ...
  • Loading branch information
nicolas-grekas committed Jan 26, 2022
2 parents aa05995 + a2a86ec commit 22e8efd
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 10 deletions.
10 changes: 10 additions & 0 deletions Application.php
Expand Up @@ -953,6 +953,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);
Expand Down
17 changes: 12 additions & 5 deletions Helper/QuestionHelper.php
Expand Up @@ -246,6 +246,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');
Expand All @@ -255,11 +258,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) {
Expand Down Expand Up @@ -364,7 +371,7 @@ function ($match) use ($ret) {
}

// Reset stty so it behaves normally again
shell_exec(sprintf('stty %s', $sttyMode));
shell_exec('stty '.$sttyMode);

return $fullChoice;
}
Expand Down Expand Up @@ -425,7 +432,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) {
Expand Down Expand Up @@ -478,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')) {
Expand Down
6 changes: 3 additions & 3 deletions Question/ChoiceQuestion.php
Expand Up @@ -119,18 +119,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);
}
}

Expand Down
35 changes: 35 additions & 0 deletions Tests/ApplicationTest.php
Expand Up @@ -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
{
Expand Down Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions Tests/Fixtures/application_signalable.php
@@ -0,0 +1,36 @@
<?php

use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\SignalableCommandInterface;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\SingleCommandApplication;

$vendor = __DIR__;
while (!file_exists($vendor.'/vendor')) {
$vendor = \dirname($vendor);
}
require $vendor.'/vendor/autoload.php';

(new class() extends SingleCommandApplication implements SignalableCommandInterface {
public function getSubscribedSignals(): array
{
return [SIGINT];
}

public function handleSignal(int $signal): void
{
exit;
}
})
->setCode(function(InputInterface $input, OutputInterface $output) {
$this->getHelper('question')
->ask($input, $output, new ChoiceQuestion('😊', ['y']));

return 0;
})
->run()

;
18 changes: 16 additions & 2 deletions Tests/Question/ChoiceQuestionTest.php
Expand Up @@ -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);

Expand Down Expand Up @@ -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',
],
[
false,
[0],
Expand Down

0 comments on commit 22e8efd

Please sign in to comment.