8000 [Console] fix restoring stty mode on CTRL+C · symfony/symfony@7d40d0b · GitHub
[go: up one dir, main page]

Skip to content

Commit 7d40d0b

Browse files
[Console] fix restoring stty mode on CTRL+C
1 parent e39cb50 commit 7d40d0b

File tree

4 files changed

+91
-4
lines changed

4 files changed

+91
-4
lines changed

src/Symfony/Component/Console/Application.php

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

955+
if (Terminal::hasSttyAvailable()) {
956+
$sttyMode = shell_exec('stty -g');
957+
958+
foreach ([\SIGINT, \SIGTERM] as $signal) {
959+
$this->signalRegistry->register($signal, static function () use ($sttyMode) {
960+
shell_exec('stty '.$sttyMode);
961+
});
962+
}
963+
}
964+
955965
if ($this->dispatcher) {
956966
foreach ($this->signalsToDispatchEvent as $signal) {
957967
$event = new ConsoleSignalEvent($command, $input, $output, $signal);

src/Symfony/Component/Console/Helper/QuestionHelper.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,9 @@ private function autocomplete(OutputInterface $output, Question $question, $inpu
248248
$numMatches = \count($matches);
249249

250250
$sttyMode = shell_exec('stty -g');
251+
$isStdin = 'php://stdin' === (stream_get_meta_data($inputStream)['uri'] ?? null);
252+
$r = [$inputStream];
253+
$w = [];
251254

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

258261
// Read a keypress
259262
while (!feof($inputStream)) {
263+
while ($isStdin && 0 === @stream_select($r, $w, $w, 0, 100)) {
264+
// Give signal handlers a chance to run
265+
$r = [$inputStream];
266+
}
260267
$c = fread($inputStream, 1);
261268

262269
// as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false.
263270
if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) {
264-
shell_exec(sprintf('stty %s', $sttyMode));
271+
shell_exec('stty '.$sttyMode);
265272
throw new MissingInputException('Aborted.');
266273
} elseif ("\177" === $c) { // Backspace Character
267274
if (0 === $numMatches && 0 !== $i) {
@@ -365,8 +372,7 @@ function ($match) use ($ret) {
365372
}
366373
}
367374

368-
// Reset stty so it behaves normally again
369-
shell_exec(sprintf('stty %s', $sttyMode));
375+
shell_exec('stty '.$sttyMode);
370376

371377
return $fullChoice;
372378
}
@@ -427,7 +433,7 @@ private function getHiddenResponse(OutputInterface $output, $inputStream, bool $
427433
$value = fgets($inputStream, 4096);
428434

429435
if (self::$stty && Terminal::hasSttyAvailable()) {
430-
shell_exec(sprintf('stty %s', $sttyMode));
436+
shell_exec('stty '.$sttyMode);
431437
}
432438

433439
if (false === $value) {

src/Symfony/Component/Console/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+
;

0 commit comments

Comments
 (0)
0