Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 7d40d0b

Browse filesBrowse files
[Console] fix restoring stty mode on CTRL+C
1 parent e39cb50 commit 7d40d0b
Copy full SHA for 7d40d0b

File tree

4 files changed

+91
-4
lines changed
Filter options

4 files changed

+91
-4
lines changed

‎src/Symfony/Component/Console/Application.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Application.php
+10Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Helper/QuestionHelper.php
+10-4Lines 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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Console/Tests/ApplicationTest.php
+35Lines 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
+36Lines 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)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.