diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 722045091ff4..79b156ecaaf7 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -10,6 +10,7 @@ CHANGELOG * Add `BackedEnum` support with `#[Argument]` and `#[Option]` inputs in invokable commands * Allow Usages to be specified via `#[AsCommand]` attribute. * Allow passing invokable commands to `Symfony\Component\Console\Tester\CommandTester` + * Add optional timeout for interaction in `QuestionHelper` 7.3 --- diff --git a/src/Symfony/Component/Console/Helper/QuestionHelper.php b/src/Symfony/Component/Console/Helper/QuestionHelper.php index 9b65c321368f..ccd11e84082e 100644 --- a/src/Symfony/Component/Console/Helper/QuestionHelper.php +++ b/src/Symfony/Component/Console/Helper/QuestionHelper.php @@ -502,6 +502,19 @@ private function isInteractiveInput($inputStream): bool */ private function readInput($inputStream, Question $question): string|false { + if (null !== $question->getTimeoutSeconds() && $this->isInteractiveInput($inputStream)) { + $read = [$inputStream]; + $write = null; + $except = null; + $timeoutSeconds = $question->getTimeoutSeconds(); + $changedStreams = stream_select($read, $write, $except, $timeoutSeconds); + + if (0 === $changedStreams) { + $plural = 1 === $timeoutSeconds ? '' : 's'; + throw new MissingInputException("Timed out after waiting for input for $timeoutSeconds second$plural."); + } + } + if (!$question->isMultiline()) { $cp = $this->setIOCodepage(); $ret = fgets($inputStream, 4096); diff --git a/src/Symfony/Component/Console/Question/Question.php b/src/Symfony/Component/Console/Question/Question.php index cb65bd6746ee..5f8b1494648e 100644 --- a/src/Symfony/Component/Console/Question/Question.php +++ b/src/Symfony/Component/Console/Question/Question.php @@ -38,6 +38,7 @@ class Question private ?\Closure $normalizer = null; private bool $trimmable = true; private bool $multiline = false; + private ?int $timeoutSeconds = null; /** * @param string $question The question to ask to the user @@ -85,6 +86,27 @@ public function setMultiline(bool $multiline): static return $this; } + /** + * Returns the timeout in seconds. + */ + public function getTimeoutSeconds(): ?int + { + return $this->timeoutSeconds; + } + + /** + * The timeout is the maximum time the user has to answer the question. + * If the user does not answer within this time, an exception will be thrown. + * + * @return $this + */ + public function setTimeout(?int $seconds): static + { + $this->timeoutSeconds = $seconds; + + return $this; + } + /** * Returns whether the user response must be hidden. */ diff --git a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php index 0e91dd85b199..38977f42fff7 100644 --- a/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php +++ b/src/Symfony/Component/Console/Tests/Helper/QuestionHelperTest.php @@ -186,6 +186,45 @@ public function testAskNonTrimmed() $this->assertEquals('What time is it?', stream_get_contents($output->getStream())); } + public function testAskTimeout() + { + $dialog = new QuestionHelper(); + + $question = new Question('What is your name?'); + $question->setTimeout(1); + + $this->expectException(MissingInputException::class); + $this->expectExceptionMessage('Timed out after waiting for input for 1 second.'); + + try { + $startTime = microtime(true); + $dialog->ask($this->createStreamableInputInterfaceMock(\STDIN), $this->createOutputInterface(), $question); + } finally { + $elapsedTime = microtime(true) - $startTime; + self::assertGreaterThanOrEqual(1, $elapsedTime, 'The question should timeout after 1 second'); + } + } + + public function testAskTimeoutWithIncompatibleStream() + { + $dialog = new QuestionHelper(); + $inputStream = $this->getInputStream(''); + + $question = new Question('What is your name?'); + $question->setTimeout(1); + + $this->expectException(MissingInputException::class); + $this->expectExceptionMessage('Aborted.'); + + try { + $startTime = microtime(true); + $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question); + } finally { + $elapsedTime = microtime(true) - $startTime; + self::assertLessThan(1, $elapsedTime, 'Question should not wait for input on a non-interactive stream'); + } + } + public function testAskWithAutocomplete() { if (!Terminal::hasSttyAvailable()) {
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: