Skip to content

Commit a2a86ec

Browse files
Merge branch '5.3' into 5.4
* 5.3: Fix Choice constraint with associative choices array [Form] UrlType should not add protocol to emails 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 [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
2 parents 209db6b + 79e0887 commit a2a86ec

File tree

6 files changed

+112
-10
lines changed

6 files changed

+112
-10
lines changed

Application.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,16 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
979979
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.');
980980
}
981981

982+
if (Terminal::hasSttyAvailable()) {
983+
$sttyMode = shell_exec('stty -g');
984+
985+
foreach ([\SIGINT, \SIGTERM] as $signal) {
986+
$this->signalRegistry->register($signal, static function () use ($sttyMode) {
987+
shell_exec('stty '.$sttyMode);
988+
});
989+
}
990+
}
991+
982992
if ($this->dispatcher) {
983993
foreach ($this->signalsToDispatchEvent as $signal) {
984994
$event = new ConsoleSignalEvent($command, $input, $output, $signal);

Helper/QuestionHelper.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,9 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu
251251
$numMatches = \count($matches);
252252

253253
$sttyMode = shell_exec('stty -g');
254+
$isStdin = 'php://stdin' === (stream_get_meta_data($inputStream)['uri'] ?? null);
255+
$r = [$inputStream];
256+
$w = [];
254257

255258
// Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
256259
shell_exec('stty -icanon -echo');
@@ -260,11 +263,15 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu
260263

261264
// Read a keypress
262265
while (!feof($inputStream)) {
266+
while ($isStdin && 0 === @stream_select($r, $w, $w, 0, 100)) {
267+
// Give signal handlers a chance to run
268+
$r = [$inputStream];
269+
}
263270
$c = fread($inputStream, 1);
264271

265272
// as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false.
266273
if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) {
267-
shell_exec(sprintf('stty %s', $sttyMode));
274+
shell_exec('stty '.$sttyMode);
268275
throw new MissingInputException('Aborted.');
269276
} elseif ("\177" === $c) { // Backspace Character
270277
if (0 === $numMatches && 0 !== $i) {
@@ -369,7 +376,7 @@ function ($match) use ($ret) {
369376
}
370377

371378
// Reset stty so it behaves normally again
372-
shell_exec(sprintf('stty %s', $sttyMode));
379+
shell_exec('stty '.$sttyMode);
373380

374381
return $fullChoice;
375382
}
@@ -430,7 +437,7 @@ private function getHiddenResponse(OutputInterface $output, $inputStream, bool $
430437
$value = fgets($inputStream, 4096);
431438

432439
if (self::$stty && Terminal::hasSttyAvailable()) {
433-
shell_exec(sprintf('stty %s', $sttyMode));
440+
shell_exec('stty '.$sttyMode);
434441
}
435442

436443
if (false === $value) {
@@ -485,11 +492,11 @@ private function isInteractiveInput($inputStream): bool
485492
}
486493

487494
if (\function_exists('stream_isatty')) {
488-
return self::$stdinIsInteractive = stream_isatty(fopen('php://stdin', 'r'));
495+
return self::$stdinIsInteractive = @stream_isatty(fopen('php://stdin', 'r'));
489496
}
490497

491498
if (\function_exists('posix_isatty')) {
492-
return self::$stdinIsInteractive = posix_isatty(fopen('php://stdin', 'r'));
499+
return self::$stdinIsInteractive = @posix_isatty(fopen('php://stdin', 'r'));
493500
}
494501

495502
if (!\function_exists('exec')) {

Question/ChoiceQuestion.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,18 +125,18 @@ private function getDefaultValidator(): callable
125125
return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) {
126126
if ($multiselect) {
127127
// Check for a separated comma values
128-
if (!preg_match('/^[^,]+(?:,[^,]+)*$/', $selected, $matches)) {
128+
if (!preg_match('/^[^,]+(?:,[^,]+)*$/', (string) $selected, $matches)) {
129129
throw new InvalidArgumentException(sprintf($errorMessage, $selected));
130130
}
131131

132-
$selectedChoices = explode(',', $selected);
132+
$selectedChoices = explode(',', (string) $selected);
133133
} else {
134134
$selectedChoices = [$selected];
135135
}
136136

137137
if ($this->isTrimmable()) {
138138
foreach ($selectedChoices as $k => $v) {
139-
$selectedChoices[$k] = trim($v);
139+
$selectedChoices[$k] = trim((string) $v);
140140
}
141141
}
142142

Tests/ApplicationTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,11 @@
3838
use Symfony\Component\Console\Output\OutputInterface;
3939
use Symfony\Component\Console\Output\StreamOutput;
4040
use Symfony\Component\Console\SignalRegistry\SignalRegistry;
41+
use Symfony\Component\Console\Terminal;
4142
use Symfony\Component\Console\Tester\ApplicationTester;
4243
use Symfony\Component\DependencyInjection\ContainerBuilder;
4344
use Symfony\Component\EventDispatcher\EventDispatcher;
45+
use Symfony\Component\Process\Process;
4446

4547
class ApplicationTest extends TestCase
4648
{
@@ -1882,6 +1884,39 @@ public function testSignalableCommandInterfaceWithoutSignals()
18821884
$application->add($command);
18831885
$this->assertSame(0, $application->run(new ArrayInput(['signal'])));
18841886
}
1887+
1888+
/**
1889+
* @group tty
1890+
*/
1891+
public function testSignalableRestoresStty()
1892+
{
1893+
if (!Terminal::hasSttyAvailable()) {
1894+
$this->markTestSkipped('stty not available');
1895+
}
1896+
1897+
if (!SignalRegistry::isSupported()) {
1898+
$this->markTestSkipped('pcntl signals not available');
1899+
}
1900+
1901+
$previousSttyMode = shell_exec('stty -g');
1902+
1903+
$p = new Process(['php', __DIR__.'/Fixtures/application_signalable.php']);
1904+
$p->setTty(true);
1905+
$p->start();
1906+
1907+
for ($i = 0; $i < 10 && shell_exec('stty -g') === $previousSttyMode; ++$i) {
1908+
usleep(100000);
1909+
}
1910+
1911+
$this->assertNotSame($previousSttyMode, shell_exec('stty -g'));
1912+
$p->signal(\SIGINT);
1913+
$p->wait();
1914+
1915+
$sttyMode = shell_exec('stty -g');
1916+
shell_exec('stty '.$previousSttyMode);
1917+
1918+
$this->assertSame($previousSttyMode, $sttyMode);
1919+
}
18851920
}
18861921

18871922
class CustomApplication extends Application
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
use Symfony\Component\Console\Application;
4+
use Symfony\Component\Console\Command\SignalableCommandInterface;
5+
use Symfony\Component\Console\Helper\QuestionHelper;
6+
use Symfony\Component\Console\Input\InputInterface;
7+
use Symfony\Component\Console\Output\OutputInterface;
8+
use Symfony\Component\Console\Question\ChoiceQuestion;
9+
use Symfony\Component\Console\SingleCommandApplication;
10+
11+
$vendor = __DIR__;
12+
while (!file_exists($vendor.'/vendor')) {
13+
$vendor = \dirname($vendor);
14+
}
15+
require $vendor.'/vendor/autoload.php';
16+
17+
(new class() extends SingleCommandApplication implements SignalableCommandInterface {
18+
public function getSubscribedSignals(): array
19+
{
20+
return [SIGINT];
21+
}
22+
23+
public function handleSignal(int $signal): void
24+
{
25+
exit;
26+
}
27+
})
28+
->setCode(function(InputInterface $input, OutputInterface $output) {
29+
$this->getHelper('question')
30+
->ask($input, $output, new ChoiceQuestion('😊', ['y']));
31+
32+
return 0;
33+
})
34+
->run()
35+
36+
;

Tests/Question/ChoiceQuestionTest.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@ class ChoiceQuestionTest extends TestCase
1919
/**
2020
* @dataProvider selectUseCases
2121
*/
22-
public function testSelectUseCases($multiSelect, $answers, $expected, $message)
22+
public function testSelectUseCases($multiSelect, $answers, $expected, $message, $default = null)
2323
{
2424
$question = new ChoiceQuestion('A question', [
2525
'First response',
2626
'Second response',
2727
'Third response',
2828
'Fourth response',
29-
]);
29+
null,
30+
], $default);
3031

3132
$question->setMultiselect($multiSelect);
3233

@@ -59,6 +60,19 @@ public function selectUseCases()
5960
['First response', 'Second response'],
6061
'When passed multiple answers on MultiSelect, the defaultValidator must return these answers as an array',
6162
],
63+
[
64+
false,
65+
[null],
66+
null,
67+
'When used null as default single answer on singleSelect, the defaultValidator must return this answer as null',
68+
],
69+
[
70+
false,
71+
['First response'],
72+
'First response',
73+
'When used a string as default single answer on singleSelect, the defaultValidator must return this answer as a string',
74+
'First response',
75+
],
6276
[
6377
false,
6478
[0],

0 commit comments

Comments
 (0)