From 500caa3f099596fdc4ca824b87e91f86bea299dd Mon Sep 17 00:00:00 2001 From: Lucas Bustamante Date: Wed, 7 Feb 2024 00:44:15 -0300 Subject: [PATCH] [Process] Fix Inconsistent Exit Status in proc_get_status for PHP Versions Below 8.3 --- src/Symfony/Component/Process/Process.php | 14 +++++ .../Component/Process/Tests/ProcessTest.php | 55 ++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php index 2b6ed9efa9e27..a4b0a784cf26e 100644 --- a/src/Symfony/Component/Process/Process.php +++ b/src/Symfony/Component/Process/Process.php @@ -80,6 +80,7 @@ class Process implements \IteratorAggregate private $processPipes; private $latestSignal; + private $cachedExitCode; private static $sigchild; @@ -1345,6 +1346,19 @@ protected function updateStatus(bool $blocking) $this->processInformation = proc_get_status($this->process); $running = $this->processInformation['running']; + // In PHP < 8.3, "proc_get_status" only returns the correct exit status on the first call. + // Subsequent calls return -1 as the process is discarded. This workaround caches the first + // retrieved exit status for consistent results in later calls, mimicking PHP 8.3 behavior. + if (\PHP_VERSION_ID < 80300) { + if (!isset($this->cachedExitCode) && !$running && -1 !== $this->processInformation['exitcode']) { + $this->cachedExitCode = $this->processInformation['exitcode']; + } + + if (isset($this->cachedExitCode) && !$running && -1 === $this->processInformation['exitcode']) { + $this->processInformation['exitcode'] = $this->cachedExitCode; + } + } + $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running); if ($this->fallbackStatus && $this->isSigchildEnabled()) { diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php index daf842e1f9889..059d59a446ce1 100644 --- a/src/Symfony/Component/Process/Tests/ProcessTest.php +++ b/src/Symfony/Component/Process/Tests/ProcessTest.php @@ -1541,6 +1541,60 @@ public function testEnvCaseInsensitiveOnWindows() } } + public function testMultipleCallsToProcGetStatus() + { + $process = $this->getProcess('echo foo'); + $process->start(static function () use ($process) { + return $process->isRunning(); + }); + while ($process->isRunning()) { + usleep(1000); + } + $this->assertSame(0, $process->getExitCode()); + } + + public function testFailingProcessWithMultipleCallsToProcGetStatus() + { + $process = $this->getProcess('exit 123'); + $process->start(static function () use ($process) { + return $process->isRunning(); + }); + while ($process->isRunning()) { + usleep(1000); + } + $this->assertSame(123, $process->getExitCode()); + } + + /** + * @group slow + */ + public function testLongRunningProcessWithMultipleCallsToProcGetStatus() + { + $process = $this->getProcess('php -r "sleep(1); echo \'done\';"'); + $process->start(static function () use ($process) { + return $process->isRunning(); + }); + while ($process->isRunning()) { + usleep(1000); + } + $this->assertSame(0, $process->getExitCode()); + } + + /** + * @group slow + */ + public function testLongRunningProcessWithMultipleCallsToProcGetStatusError() + { + $process = $this->getProcess('php -r "sleep(1); echo \'failure\'; exit(123);"'); + $process->start(static function () use ($process) { + return $process->isRunning(); + }); + while ($process->isRunning()) { + usleep(1000); + } + $this->assertSame(123, $process->getExitCode()); + } + /** * @group transient-on-windows */ @@ -1556,7 +1610,6 @@ public function testNotTerminableInputPipe() /** * @param string|array $commandline - * @param mixed $input */ private function getProcess($commandline, ?string $cwd = null, ?array $env = null, $input = null, ?int $timeout = 60): Process {