From 9eedd60225506d56e42210a70c21bb80ca8456ce Mon Sep 17 00:00:00 2001 From: Simon Asika Date: Mon, 4 Apr 2022 16:56:32 +0800 Subject: [PATCH 01/38] [Process] Fix Process::getEnv() when setEnv() hasn't been called before --- Process.php | 2 +- Tests/ProcessTest.php | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Process.php b/Process.php index d92eeb25..09cd9602 100644 --- a/Process.php +++ b/Process.php @@ -53,7 +53,7 @@ class Process implements \IteratorAggregate private $hasCallback = false; private $commandline; private $cwd; - private $env; + private $env = []; private $input; private $starttime; private $lastOutputTime; diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 6cd41ebc..d4ab4dbc 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1505,8 +1505,11 @@ public function testPreparedCommandWithNoValues() public function testEnvArgument() { - $env = ['FOO' => 'Foo', 'BAR' => 'Bar']; $cmd = '\\' === \DIRECTORY_SEPARATOR ? 'echo !FOO! !BAR! !BAZ!' : 'echo $FOO $BAR $BAZ'; + $p = Process::fromShellCommandline($cmd); + $this->assertSame([], $p->getEnv()); + + $env = ['FOO' => 'Foo', 'BAR' => 'Bar']; $p = Process::fromShellCommandline($cmd, null, $env); $p->run(null, ['BAR' => 'baR', 'BAZ' => 'baZ']); From 5cee9cdc4f7805e2699d9fd66991a0e6df8252a2 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 27 Jun 2022 15:16:42 +0200 Subject: [PATCH 02/38] CS fixes --- ExecutableFinder.php | 4 ++-- Pipes/AbstractPipes.php | 2 +- ProcessUtils.php | 2 +- Tests/ExecutableFinderTest.php | 14 +++++++------- Tests/ProcessTest.php | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index ff68ed33..e2dd064d 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -50,8 +50,8 @@ public function addSuffix($suffix) */ public function find($name, $default = null, array $extraDirs = []) { - if (ini_get('open_basedir')) { - $searchPath = array_merge(explode(\PATH_SEPARATOR, ini_get('open_basedir')), $extraDirs); + if (\ini_get('open_basedir')) { + $searchPath = array_merge(explode(\PATH_SEPARATOR, \ini_get('open_basedir')), $extraDirs); $dirs = []; foreach ($searchPath as $path) { // Silencing against https://bugs.php.net/69240 diff --git a/Pipes/AbstractPipes.php b/Pipes/AbstractPipes.php index ab65866c..9532e3ef 100644 --- a/Pipes/AbstractPipes.php +++ b/Pipes/AbstractPipes.php @@ -104,7 +104,7 @@ protected function write(): ?array stream_set_blocking($input, 0); } elseif (!isset($this->inputBuffer[0])) { if (!\is_string($input)) { - if (!is_scalar($input)) { + if (!\is_scalar($input)) { throw new InvalidArgumentException(sprintf('"%s" yielded a value of type "%s", but only scalars and stream resources are supported.', \get_class($this->input), \gettype($input))); } $input = (string) $input; diff --git a/ProcessUtils.php b/ProcessUtils.php index eb39a4a9..121693ba 100644 --- a/ProcessUtils.php +++ b/ProcessUtils.php @@ -48,7 +48,7 @@ public static function validateInput($caller, $input) if (\is_string($input)) { return $input; } - if (is_scalar($input)) { + if (\is_scalar($input)) { return (string) $input; } if ($input instanceof Process) { diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index d056841f..5c63cf0f 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -37,7 +37,7 @@ private function setPath($path) public function testFind() { - if (ini_get('open_basedir')) { + if (\ini_get('open_basedir')) { $this->markTestSkipped('Cannot test when open_basedir is set'); } @@ -51,7 +51,7 @@ public function testFind() public function testFindWithDefault() { - if (ini_get('open_basedir')) { + if (\ini_get('open_basedir')) { $this->markTestSkipped('Cannot test when open_basedir is set'); } @@ -67,7 +67,7 @@ public function testFindWithDefault() public function testFindWithNullAsDefault() { - if (ini_get('open_basedir')) { + if (\ini_get('open_basedir')) { $this->markTestSkipped('Cannot test when open_basedir is set'); } @@ -82,7 +82,7 @@ public function testFindWithNullAsDefault() public function testFindWithExtraDirs() { - if (ini_get('open_basedir')) { + if (\ini_get('open_basedir')) { $this->markTestSkipped('Cannot test when open_basedir is set'); } @@ -105,7 +105,7 @@ public function testFindWithOpenBaseDir() $this->markTestSkipped('Cannot run test on windows'); } - if (ini_get('open_basedir')) { + if (\ini_get('open_basedir')) { $this->markTestSkipped('Cannot test when open_basedir is set'); } @@ -122,7 +122,7 @@ public function testFindWithOpenBaseDir() */ public function testFindProcessInOpenBasedir() { - if (ini_get('open_basedir')) { + if (\ini_get('open_basedir')) { $this->markTestSkipped('Cannot test when open_basedir is set'); } if ('\\' === \DIRECTORY_SEPARATOR) { @@ -140,7 +140,7 @@ public function testFindProcessInOpenBasedir() public function testFindBatchExecutableOnWindows() { - if (ini_get('open_basedir')) { + if (\ini_get('open_basedir')) { $this->markTestSkipped('Cannot test when open_basedir is set'); } if ('\\' !== \DIRECTORY_SEPARATOR) { diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index d4ab4dbc..74c662fb 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1133,7 +1133,7 @@ public function testTermSignalTerminatesProcessCleanly() public function responsesCodeProvider() { return [ - //expected output / getter / code to execute + // expected output / getter / code to execute // [1,'getExitCode','exit(1);'], // [true,'isSuccessful','exit();'], ['output', 'getOutput', 'echo \'output\';'], From c5ba874c9b636dbccf761e22ce750e88ec3f55e1 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 1 Jan 2023 09:32:19 +0100 Subject: [PATCH 03/38] Bump license year to 2023 --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 88bf75bb..00837045 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2022 Fabien Potencier +Copyright (c) 2004-2023 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From f7f1cf1595bc176da3ed5dda597e2168d44ff786 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Tue, 24 Jan 2023 15:02:24 +0100 Subject: [PATCH 04/38] Update license years (last time) --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 00837045..0138f8f0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2023 Fabien Potencier +Copyright (c) 2004-present Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 4019350bf7a212e95aea39395bf0de278db01253 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Wed, 14 Dec 2022 15:42:16 +0100 Subject: [PATCH 05/38] Migrate to `static` data providers using `rector/rector` --- Tests/ProcessTest.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 1646bcb9..790167fc 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -301,7 +301,7 @@ public function testInvalidInput($value) $process->setInput($value); } - public function provideInvalidInputValues() + public static function provideInvalidInputValues() { return [ [[]], @@ -319,7 +319,7 @@ public function testValidInput($expected, $value) $this->assertSame($expected, $process->getInput()); } - public function provideInputValues() + public static function provideInputValues() { return [ [null, null], @@ -328,7 +328,7 @@ public function provideInputValues() ]; } - public function chainedCommandsOutputProvider() + public static function chainedCommandsOutputProvider() { if ('\\' === \DIRECTORY_SEPARATOR) { return [ @@ -422,7 +422,7 @@ public function testIncrementalOutput($getOutput, $getIncrementalOutput, $uri) fclose($h); } - public function provideIncrementalOutput() + public static function provideIncrementalOutput() { return [ ['getOutput', 'getIncrementalOutput', 'php://stdout'], @@ -957,7 +957,7 @@ public function testMethodsThatNeedARunningProcess($method) $process->{$method}(); } - public function provideMethodsThatNeedARunningProcess() + public static function provideMethodsThatNeedARunningProcess() { return [ ['getOutput'], @@ -988,7 +988,7 @@ public function testMethodsThatNeedATerminatedProcess($method) throw $e; } - public function provideMethodsThatNeedATerminatedProcess() + public static function provideMethodsThatNeedATerminatedProcess() { return [ ['hasBeenSignaled'], @@ -1093,7 +1093,7 @@ public function testGetOutputWhileDisabled($fetchMethod) $p->{$fetchMethod}(); } - public function provideOutputFetchingMethods() + public static function provideOutputFetchingMethods() { return [ ['getOutput'], @@ -1130,7 +1130,7 @@ public function testTermSignalTerminatesProcessCleanly() $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException'); } - public function responsesCodeProvider() + public static function responsesCodeProvider() { return [ // expected output / getter / code to execute @@ -1140,7 +1140,7 @@ public function responsesCodeProvider() ]; } - public function pipesCodeProvider() + public static function pipesCodeProvider() { $variations = [ 'fwrite(STDOUT, $in = file_get_contents(\'php://stdin\')); fwrite(STDERR, $in);', @@ -1183,7 +1183,7 @@ public function testIncrementalOutputDoesNotRequireAnotherCall($stream, $method) $process->stop(); } - public function provideVariousIncrementals() + public static function provideVariousIncrementals() { return [ ['php://stdout', 'getIncrementalOutput'], @@ -1449,7 +1449,7 @@ public function testRawCommandLine() $this->assertSame($expected, str_replace('Standard input code', '-', $p->getOutput())); } - public function provideEscapeArgument() + public static function provideEscapeArgument() { yield ['a"b%c%']; yield ['a"b^c^']; From 3f3ab88c5592914a2b80bb3d9f594919367144a1 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 16 Feb 2023 10:33:00 +0100 Subject: [PATCH 06/38] CS fix --- Process.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Process.php b/Process.php index 14e17774..871522de 100644 --- a/Process.php +++ b/Process.php @@ -617,10 +617,10 @@ public function getIncrementalOutput() * * @param int $flags A bit field of Process::ITER_* flags * + * @return \Generator + * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started - * - * @return \Generator */ #[\ReturnTypeWillChange] public function getIterator(int $flags = 0) From d4ce417ebcb0b7d090b4c178ed6d3accc518e8bd Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 21 Feb 2023 17:34:40 +0100 Subject: [PATCH 07/38] Fix phpdocs in components --- Process.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Process.php b/Process.php index 871522de..b47ecca1 100644 --- a/Process.php +++ b/Process.php @@ -910,7 +910,7 @@ public function getStatus() * Stops the process. * * @param int|float $timeout The timeout in seconds - * @param int $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9) + * @param int|null $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9) * * @return int|null The exit-code of the process or null if it's not running */ From 4b850da0cc3a2a9181c1ed407adbca4733dc839b Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 6 Mar 2023 21:48:01 +0100 Subject: [PATCH 08/38] [Tests] Replace `setMethods()` by `onlyMethods()` and `addMethods()` --- Tests/ProcessFailedExceptionTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ProcessFailedExceptionTest.php b/Tests/ProcessFailedExceptionTest.php index d6d7bfb0..259ffd63 100644 --- a/Tests/ProcessFailedExceptionTest.php +++ b/Tests/ProcessFailedExceptionTest.php @@ -25,7 +25,7 @@ class ProcessFailedExceptionTest extends TestCase */ public function testProcessFailedExceptionThrowsException() { - $process = $this->getMockBuilder(Process::class)->setMethods(['isSuccessful'])->setConstructorArgs([['php']])->getMock(); + $process = $this->getMockBuilder(Process::class)->onlyMethods(['isSuccessful'])->setConstructorArgs([['php']])->getMock(); $process->expects($this->once()) ->method('isSuccessful') ->willReturn(true); @@ -49,7 +49,7 @@ public function testProcessFailedExceptionPopulatesInformationFromProcessOutput( $errorOutput = 'FATAL: Unexpected error'; $workingDirectory = getcwd(); - $process = $this->getMockBuilder(Process::class)->setMethods(['isSuccessful', 'getOutput', 'getErrorOutput', 'getExitCode', 'getExitCodeText', 'isOutputDisabled', 'getWorkingDirectory'])->setConstructorArgs([[$cmd]])->getMock(); + $process = $this->getMockBuilder(Process::class)->onlyMethods(['isSuccessful', 'getOutput', 'getErrorOutput', 'getExitCode', 'getExitCodeText', 'isOutputDisabled', 'getWorkingDirectory'])->setConstructorArgs([[$cmd]])->getMock(); $process->expects($this->once()) ->method('isSuccessful') ->willReturn(false); @@ -97,7 +97,7 @@ public function testDisabledOutputInFailedExceptionDoesNotPopulateOutput() $exitText = 'General error'; $workingDirectory = getcwd(); - $process = $this->getMockBuilder(Process::class)->setMethods(['isSuccessful', 'isOutputDisabled', 'getExitCode', 'getExitCodeText', 'getOutput', 'getErrorOutput', 'getWorkingDirectory'])->setConstructorArgs([[$cmd]])->getMock(); + $process = $this->getMockBuilder(Process::class)->onlyMethods(['isSuccessful', 'isOutputDisabled', 'getExitCode', 'getExitCodeText', 'getOutput', 'getErrorOutput', 'getWorkingDirectory'])->setConstructorArgs([[$cmd]])->getMock(); $process->expects($this->once()) ->method('isSuccessful') ->willReturn(false); From 4b842fc4b61609e0a155a114082bd94e31e98287 Mon Sep 17 00:00:00 2001 From: Denis Date: Mon, 13 Feb 2023 17:14:55 +0400 Subject: [PATCH 09/38] for #49320 --- PhpExecutableFinder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PhpExecutableFinder.php b/PhpExecutableFinder.php index 998808b6..bed6c3dc 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -56,7 +56,7 @@ public function find(bool $includeArgs = true) $args = $includeArgs && $args ? ' '.implode(' ', $args) : ''; // PHP_BINARY return the current sapi executable - if (\PHP_BINARY && \in_array(\PHP_SAPI, ['cgi-fcgi', 'cli', 'cli-server', 'phpdbg'], true)) { + if (\PHP_BINARY && \in_array(\PHP_SAPI, ['cli', 'cli-server', 'phpdbg'], true)) { return \PHP_BINARY.$args; } From e3c46cc5689c8782944274bb30702106ecbe3b64 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Wed, 17 May 2023 13:26:05 +0200 Subject: [PATCH 10/38] [Process] Stop the process correctly even if underlying input stream is not closed: While checking a process to end, on posix system, process component only checks if pipes are still open, this fix ensure that if the process is terminated it correctly return, even if the underlying pipe is not closed. It can be useful when using \STDIN as a input stream as it will always be open --- Process.php | 2 +- Tests/ProcessTest.php | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Process.php b/Process.php index b47ecca1..9b19475a 100644 --- a/Process.php +++ b/Process.php @@ -428,7 +428,7 @@ public function wait(callable $callback = null) do { $this->checkTimeout(); - $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); + $running = $this->isRunning() && ('\\' === \DIRECTORY_SEPARATOR || $this->processPipes->areOpen()); $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running); } while ($running); diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 790167fc..36acf02a 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1538,6 +1538,16 @@ public function testEnvCaseInsensitiveOnWindows() } } + public function testNotTerminableInputPipe() + { + $process = $this->getProcess('echo foo'); + $process->setInput(\STDIN); + $process->start(); + $process->setTimeout(2); + $process->wait(); + $this->assertFalse($process->isRunning()); + } + /** * @param string|array $commandline * @param mixed $input From 86ca4c74afe24bc1889bc45a20adbd47c3131c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Pr=C3=A9vot?= Date: Sat, 1 Jul 2023 20:52:48 +0200 Subject: [PATCH 11/38] Fix executable bit --- Tests/ErrorProcessInitiator.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 Tests/ErrorProcessInitiator.php diff --git a/Tests/ErrorProcessInitiator.php b/Tests/ErrorProcessInitiator.php old mode 100755 new mode 100644 From 1a44dc377ec86a50fab40d066cd061e28a6b482f Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 12 Jul 2023 13:45:09 +0200 Subject: [PATCH 12/38] [PhpUnitBridge] Kill the last concurrent process when it stales for more than 60s --- Tests/ProcessTest.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 36acf02a..6e6ee8a4 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -66,11 +66,11 @@ public function testInvalidCwd() $cmd->run(); } + /** + * @group transient-on-windows + */ public function testThatProcessDoesNotThrowWarningDuringRun() { - if ('\\' === \DIRECTORY_SEPARATOR) { - $this->markTestSkipped('This test is transient on Windows'); - } @trigger_error('Test Error', \E_USER_NOTICE); $process = $this->getProcessForCode('sleep(3)'); $process->run(); @@ -130,12 +130,11 @@ public function testStopWithTimeoutIsActuallyWorking() $this->assertLessThan(15, microtime(true) - $start); } + /** + * @group transient-on-windows + */ public function testWaitUntilSpecificOutput() { - if ('\\' === \DIRECTORY_SEPARATOR) { - $this->markTestIncomplete('This test is too transient on Windows, help wanted to improve it'); - } - $p = $this->getProcess([self::$phpBin, __DIR__.'/KillableProcessWithOutput.php']); $p->start(); @@ -1538,6 +1537,9 @@ public function testEnvCaseInsensitiveOnWindows() } } + /** + * @group transient-on-windows + */ public function testNotTerminableInputPipe() { $process = $this->getProcess('echo foo'); From ce9850f4eafd1181427f005a8b238d943878e53c Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 1 Aug 2023 15:39:00 +0200 Subject: [PATCH 13/38] [Process] Fix test case --- Tests/ErrorProcessInitiator.php | 4 ++-- Tests/ProcessTest.php | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Tests/ErrorProcessInitiator.php b/Tests/ErrorProcessInitiator.php index 4c8556ac..54168022 100644 --- a/Tests/ErrorProcessInitiator.php +++ b/Tests/ErrorProcessInitiator.php @@ -14,12 +14,12 @@ use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Process; -require \dirname(__DIR__).'/vendor/autoload.php'; +require is_file(\dirname(__DIR__).'/vendor/autoload.php') ? \dirname(__DIR__).'/vendor/autoload.php' : \dirname(__DIR__, 5).'/vendor/autoload.php'; ['e' => $php] = getopt('e:') + ['e' => 'php']; try { - $process = new Process("exec $php -r \"echo 'ready'; trigger_error('error', E_USER_ERROR);\""); + $process = new Process([$php, '-r', "echo 'ready'; trigger_error('error', E_USER_ERROR);"]); $process->start(); $process->setTimeout(0.5); while (!str_contains($process->getOutput(), 'ready')) { diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 6e6ee8a4..827c7239 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1523,6 +1523,10 @@ public function testWaitStoppedDeadProcess() $process->setTimeout(2); $process->wait(); $this->assertFalse($process->isRunning()); + + if ('\\' !== \DIRECTORY_SEPARATOR) { + $this->assertSame(0, $process->getExitCode()); + } } public function testEnvCaseInsensitiveOnWindows() From 45261e1fccad1b5447a8d7a8e67aa7b4a9798b7b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 7 Aug 2023 11:52:08 +0200 Subject: [PATCH 14/38] [Process] Fix silencing `wait` when using a sigchild-enabled binary --- Process.php | 2 +- Tests/ProcessTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Process.php b/Process.php index 9b19475a..30ebeb6b 100644 --- a/Process.php +++ b/Process.php @@ -331,7 +331,7 @@ public function start(callable $callback = null, array $env = []) // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;'; - $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code'; + $commandline .= 'pid=$!; echo $pid >&3; wait $pid 2>/dev/null; code=$?; echo $code >&3; exit $code'; // Workaround for the bug, when PTS functionality is enabled. // @see : https://bugs.php.net/69442 diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 827c7239..80493799 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1524,7 +1524,7 @@ public function testWaitStoppedDeadProcess() $process->wait(); $this->assertFalse($process->isRunning()); - if ('\\' !== \DIRECTORY_SEPARATOR) { + if ('\\' !== \DIRECTORY_SEPARATOR && !\Closure::bind(function () { return $this->isSigchildEnabled(); }, $process, $process)()) { $this->assertSame(0, $process->getExitCode()); } } From 8fa22178dfc368911dbd513b431cd9b06f9afe7a Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Sat, 2 Dec 2023 09:38:30 +0100 Subject: [PATCH 15/38] always pass microseconds to usleep as integers --- Pipes/WindowsPipes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pipes/WindowsPipes.php b/Pipes/WindowsPipes.php index bca84f57..968dd026 100644 --- a/Pipes/WindowsPipes.php +++ b/Pipes/WindowsPipes.php @@ -149,7 +149,7 @@ public function readAndWrite(bool $blocking, bool $close = false): array if ($w) { @stream_select($r, $w, $e, 0, Process::TIMEOUT_PRECISION * 1E6); } elseif ($this->fileHandles) { - usleep(Process::TIMEOUT_PRECISION * 1E6); + usleep((int) (Process::TIMEOUT_PRECISION * 1E6)); } } foreach ($this->fileHandles as $type => $fileHandle) { From 9a2172b463c6b56dd2b44a4f664169950fe29048 Mon Sep 17 00:00:00 2001 From: Kay Wei Date: Wed, 10 Jan 2024 15:19:56 +0800 Subject: [PATCH 16/38] [Process] Fix executable finder when the command starts with a dash --- PhpExecutableFinder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PhpExecutableFinder.php b/PhpExecutableFinder.php index bed6c3dc..45dbcca4 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -35,7 +35,7 @@ public function find(bool $includeArgs = true) { if ($php = getenv('PHP_BINARY')) { if (!is_executable($php)) { - $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v'; + $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; if ($php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) { if (!is_executable($php)) { return false; From 175925109fa5e76a0e6c283e3a3c9ffc0dd7968a Mon Sep 17 00:00:00 2001 From: Cornel Cruceru Date: Sat, 13 Jan 2024 01:03:15 +0200 Subject: [PATCH 17/38] [Process] Fixed inconsistent test Sometimes the process no longer appears to be running when the signal is sent which causes a LogicException to be thrown. This doesn't appear to be consistent and I can reproduce it randomly on my local machine. To avoid having tests fail at random I decided that it's better to send the signal only if the process is still marked as running. --- Tests/ErrorProcessInitiator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ErrorProcessInitiator.php b/Tests/ErrorProcessInitiator.php index 54168022..0b75add6 100644 --- a/Tests/ErrorProcessInitiator.php +++ b/Tests/ErrorProcessInitiator.php @@ -25,7 +25,7 @@ while (!str_contains($process->getOutput(), 'ready')) { usleep(1000); } - $process->signal(\SIGSTOP); + $process->isRunning() && $process->signal(\SIGSTOP); $process->wait(); return $process->getExitCode(); From cbc28e34015ad50166fc2f9c8962d28d0fe861eb Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 23 Jan 2024 14:51:25 +0100 Subject: [PATCH 18/38] Apply php-cs-fixer fix --rules nullable_type_declaration_for_default_null_value --- ExecutableFinder.php | 2 +- InputStream.php | 2 +- PhpProcess.php | 6 +++--- Process.php | 18 +++++++++--------- Tests/ProcessTest.php | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index eb8f0629..f392c962 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -46,7 +46,7 @@ public function addSuffix(string $suffix) * * @return string|null */ - public function find(string $name, string $default = null, array $extraDirs = []) + public function find(string $name, ?string $default = null, array $extraDirs = []) { if (\ini_get('open_basedir')) { $searchPath = array_merge(explode(\PATH_SEPARATOR, \ini_get('open_basedir')), $extraDirs); diff --git a/InputStream.php b/InputStream.php index 240665f3..0c45b524 100644 --- a/InputStream.php +++ b/InputStream.php @@ -30,7 +30,7 @@ class InputStream implements \IteratorAggregate /** * Sets a callback that is called when the write buffer becomes empty. */ - public function onEmpty(callable $onEmpty = null) + public function onEmpty(?callable $onEmpty = null) { $this->onEmpty = $onEmpty; } diff --git a/PhpProcess.php b/PhpProcess.php index 2bc338e5..3a1d147c 100644 --- a/PhpProcess.php +++ b/PhpProcess.php @@ -32,7 +32,7 @@ class PhpProcess extends Process * @param int $timeout The timeout in seconds * @param array|null $php Path to the PHP binary to use with any additional arguments */ - public function __construct(string $script, string $cwd = null, array $env = null, int $timeout = 60, array $php = null) + public function __construct(string $script, ?string $cwd = null, ?array $env = null, int $timeout = 60, ?array $php = null) { if (null === $php) { $executableFinder = new PhpExecutableFinder(); @@ -53,7 +53,7 @@ public function __construct(string $script, string $cwd = null, array $env = nul /** * {@inheritdoc} */ - public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60) + public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, $input = null, ?float $timeout = 60) { throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); } @@ -61,7 +61,7 @@ public static function fromShellCommandline(string $command, string $cwd = null, /** * {@inheritdoc} */ - public function start(callable $callback = null, array $env = []) + public function start(?callable $callback = null, array $env = []) { if (null === $this->getCommandLine()) { throw new RuntimeException('Unable to find the PHP executable.'); diff --git a/Process.php b/Process.php index 30ebeb6b..2b6ed9ef 100644 --- a/Process.php +++ b/Process.php @@ -140,7 +140,7 @@ class Process implements \IteratorAggregate * * @throws LogicException When proc_open is not installed */ - public function __construct(array $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60) + public function __construct(array $command, ?string $cwd = null, ?array $env = null, $input = null, ?float $timeout = 60) { if (!\function_exists('proc_open')) { throw new LogicException('The Process class relies on proc_open, which is not available on your PHP installation.'); @@ -189,7 +189,7 @@ public function __construct(array $command, string $cwd = null, array $env = nul * * @throws LogicException When proc_open is not installed */ - public static function fromShellCommandline(string $command, string $cwd = null, array $env = null, $input = null, ?float $timeout = 60) + public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, $input = null, ?float $timeout = 60) { $process = new static([], $cwd, $env, $input, $timeout); $process->commandline = $command; @@ -247,7 +247,7 @@ public function __clone() * * @final */ - public function run(callable $callback = null, array $env = []): int + public function run(?callable $callback = null, array $env = []): int { $this->start($callback, $env); @@ -266,7 +266,7 @@ public function run(callable $callback = null, array $env = []): int * * @final */ - public function mustRun(callable $callback = null, array $env = []): self + public function mustRun(?callable $callback = null, array $env = []): self { if (0 !== $this->run($callback, $env)) { throw new ProcessFailedException($this); @@ -294,7 +294,7 @@ public function mustRun(callable $callback = null, array $env = []): self * @throws RuntimeException When process is already running * @throws LogicException In case a callback is provided and output has been disabled */ - public function start(callable $callback = null, array $env = []) + public function start(?callable $callback = null, array $env = []) { if ($this->isRunning()) { throw new RuntimeException('Process is already running.'); @@ -385,7 +385,7 @@ public function start(callable $callback = null, array $env = []) * * @final */ - public function restart(callable $callback = null, array $env = []): self + public function restart(?callable $callback = null, array $env = []): self { if ($this->isRunning()) { throw new RuntimeException('Process is already running.'); @@ -412,7 +412,7 @@ public function restart(callable $callback = null, array $env = []): self * @throws ProcessSignaledException When process stopped after receiving signal * @throws LogicException When process is not yet started */ - public function wait(callable $callback = null) + public function wait(?callable $callback = null) { $this->requireProcessIsStarted(__FUNCTION__); @@ -914,7 +914,7 @@ public function getStatus() * * @return int|null The exit-code of the process or null if it's not running */ - public function stop(float $timeout = 10, int $signal = null) + public function stop(float $timeout = 10, ?int $signal = null) { $timeoutMicro = microtime(true) + $timeout; if ($this->isRunning()) { @@ -1310,7 +1310,7 @@ private function getDescriptors(): array * * @return \Closure */ - protected function buildCallback(callable $callback = null) + protected function buildCallback(?callable $callback = null) { if ($this->outputDisabled) { return function ($type, $data) use ($callback): bool { diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 80493799..daf842e1 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1558,7 +1558,7 @@ 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 + private function getProcess($commandline, ?string $cwd = null, ?array $env = null, $input = null, ?int $timeout = 60): Process { if (\is_string($commandline)) { $process = Process::fromShellCommandline($commandline, $cwd, $env, $input, $timeout); @@ -1573,7 +1573,7 @@ private function getProcess($commandline, string $cwd = null, array $env = null, return self::$process = $process; } - private function getProcessForCode(string $code, string $cwd = null, array $env = null, $input = null, ?int $timeout = 60): Process + private function getProcessForCode(string $code, ?string $cwd = null, ?array $env = null, $input = null, ?int $timeout = 60): Process { return $this->getProcess([self::$phpBin, '-r', $code], $cwd, $env, $input, $timeout); } From 7e2c857ee885cada866ae5c9a43613f002ec11c4 Mon Sep 17 00:00:00 2001 From: Lucas Bustamante Date: Wed, 7 Feb 2024 00:44:15 -0300 Subject: [PATCH 19/38] [Process] Fix Inconsistent Exit Status in proc_get_status for PHP Versions Below 8.3 --- Process.php | 14 +++++++++++ Tests/ProcessTest.php | 55 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/Process.php b/Process.php index 2b6ed9ef..a4b0a784 100644 --- a/Process.php +++ b/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/Tests/ProcessTest.php b/Tests/ProcessTest.php index daf842e1..059d59a4 100644 --- a/Tests/ProcessTest.php +++ b/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 { From 4fdf34004f149cc20b2f51d7d119aa500caad975 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com> Date: Mon, 12 Feb 2024 15:55:24 +0100 Subject: [PATCH 20/38] [Process] Fix failing tests causing segfaults --- Tests/ProcessTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index 059d59a4..a2e370de 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1570,7 +1570,7 @@ public function testFailingProcessWithMultipleCallsToProcGetStatus() */ public function testLongRunningProcessWithMultipleCallsToProcGetStatus() { - $process = $this->getProcess('php -r "sleep(1); echo \'done\';"'); + $process = $this->getProcess('sleep 1 && echo "done" && php -r "exit(0);"'); $process->start(static function () use ($process) { return $process->isRunning(); }); @@ -1585,7 +1585,7 @@ public function testLongRunningProcessWithMultipleCallsToProcGetStatus() */ public function testLongRunningProcessWithMultipleCallsToProcGetStatusError() { - $process = $this->getProcess('php -r "sleep(1); echo \'failure\'; exit(123);"'); + $process = $this->getProcess('sleep 1 && echo "failure" && php -r "exit(123);"'); $process->start(static function () use ($process) { return $process->isRunning(); }); From 85a554acd7c28522241faf2e97b9541247a0d3d5 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 18 Apr 2024 09:55:03 +0200 Subject: [PATCH 21/38] Auto-close PRs on subtree-splits --- .gitattributes | 3 +- .github/PULL_REQUEST_TEMPLATE.md | 8 +++++ .github/workflows/check-subtree-split.yml | 37 +++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/check-subtree-split.yml diff --git a/.gitattributes b/.gitattributes index 84c7add0..14c3c359 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,3 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore +/.git* export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..4689c4da --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Please do not submit any Pull Requests here. They will be closed. +--- + +Please submit your PR here instead: +https://github.com/symfony/symfony + +This repository is what we call a "subtree split": a read-only subset of that main repository. +We're looking forward to your PR there! diff --git a/.github/workflows/check-subtree-split.yml b/.github/workflows/check-subtree-split.yml new file mode 100644 index 00000000..16be48ba --- /dev/null +++ b/.github/workflows/check-subtree-split.yml @@ -0,0 +1,37 @@ +name: Check subtree split + +on: + pull_request_target: + +jobs: + close-pull-request: + runs-on: ubuntu-latest + + steps: + - name: Close pull request + uses: actions/github-script@v6 + with: + script: | + if (context.repo.owner === "symfony") { + github.rest.issues.createComment({ + owner: "symfony", + repo: context.repo.repo, + issue_number: context.issue.number, + body: ` + Thanks for your Pull Request! We love contributions. + + However, you should instead open your PR on the main repository: + https://github.com/symfony/symfony + + This repository is what we call a "subtree split": a read-only subset of that main repository. + We're looking forward to your PR there! + ` + }); + + github.rest.pulls.update({ + owner: "symfony", + repo: context.repo.repo, + pull_number: context.issue.number, + state: "closed" + }); + } From b3da76c30c3f33c21356ceeb631f8958b2b932cd Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 29 Apr 2024 16:31:15 +0200 Subject: [PATCH 22/38] Remove calls to `TestCase::iniSet()` and calls to deprecated methods of `MockBuilder` --- Tests/ExecutableFinderTest.php | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index 5c63cf0f..6d089def 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -109,12 +109,16 @@ public function testFindWithOpenBaseDir() $this->markTestSkipped('Cannot test when open_basedir is set'); } - $this->iniSet('open_basedir', \dirname(\PHP_BINARY).\PATH_SEPARATOR.'/'); + $initialOpenBaseDir = ini_set('open_basedir', \dirname(\PHP_BINARY).\PATH_SEPARATOR.'/'); - $finder = new ExecutableFinder(); - $result = $finder->find($this->getPhpBinaryName()); + try { + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName()); - $this->assertSamePath(\PHP_BINARY, $result); + $this->assertSamePath(\PHP_BINARY, $result); + } finally { + ini_set('open_basedir', $initialOpenBaseDir); + } } /** @@ -130,12 +134,17 @@ public function testFindProcessInOpenBasedir() } $this->setPath(''); - $this->iniSet('open_basedir', \PHP_BINARY.\PATH_SEPARATOR.'/'); - $finder = new ExecutableFinder(); - $result = $finder->find($this->getPhpBinaryName(), false); + $initialOpenBaseDir = ini_set('open_basedir', \PHP_BINARY.\PATH_SEPARATOR.'/'); - $this->assertSamePath(\PHP_BINARY, $result); + try { + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName(), false); + + $this->assertSamePath(\PHP_BINARY, $result); + } finally { + ini_set('open_basedir', $initialOpenBaseDir); + } } public function testFindBatchExecutableOnWindows() From deedcb3bb4669cae2148bc920eafd2b16dc7c046 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Fri, 31 May 2024 16:33:22 +0200 Subject: [PATCH 23/38] Revert "minor #54653 Auto-close PRs on subtree-splits (nicolas-grekas)" This reverts commit 2c9352dd91ebaf37b8a3e3c26fd8e1306df2fb73, reversing changes made to 18c3e87f1512be2cc50e90235b144b13bc347258. --- .gitattributes | 3 +- .github/PULL_REQUEST_TEMPLATE.md | 8 ----- .github/workflows/check-subtree-split.yml | 37 ----------------------- 3 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 .github/workflows/check-subtree-split.yml diff --git a/.gitattributes b/.gitattributes index 14c3c359..84c7add0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.git* export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 4689c4da..00000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,8 +0,0 @@ -Please do not submit any Pull Requests here. They will be closed. ---- - -Please submit your PR here instead: -https://github.com/symfony/symfony - -This repository is what we call a "subtree split": a read-only subset of that main repository. -We're looking forward to your PR there! diff --git a/.github/workflows/check-subtree-split.yml b/.github/workflows/check-subtree-split.yml deleted file mode 100644 index 16be48ba..00000000 --- a/.github/workflows/check-subtree-split.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Check subtree split - -on: - pull_request_target: - -jobs: - close-pull-request: - runs-on: ubuntu-latest - - steps: - - name: Close pull request - uses: actions/github-script@v6 - with: - script: | - if (context.repo.owner === "symfony") { - github.rest.issues.createComment({ - owner: "symfony", - repo: context.repo.repo, - issue_number: context.issue.number, - body: ` - Thanks for your Pull Request! We love contributions. - - However, you should instead open your PR on the main repository: - https://github.com/symfony/symfony - - This repository is what we call a "subtree split": a read-only subset of that main repository. - We're looking forward to your PR there! - ` - }); - - github.rest.pulls.update({ - owner: "symfony", - repo: context.repo.repo, - pull_number: context.issue.number, - state: "closed" - }); - } From 32354f62488486b6efcbcd61a1dc8a619287fd29 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 5 Sep 2024 18:13:22 +0200 Subject: [PATCH 24/38] Don't use is_resource() on non-streams --- Process.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Process.php b/Process.php index a4b0a784..62addf1e 100644 --- a/Process.php +++ b/Process.php @@ -352,7 +352,7 @@ public function start(?callable $callback = null, array $env = []) $this->process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); - if (!\is_resource($this->process)) { + if (!$this->process) { throw new RuntimeException('Unable to launch a new process.'); } $this->status = self::STATUS_STARTED; @@ -1456,8 +1456,9 @@ private function readPipes(bool $blocking, bool $close) private function close(): int { $this->processPipes->close(); - if (\is_resource($this->process)) { + if ($this->process) { proc_close($this->process); + $this->process = null; } $this->exitcode = $this->processInformation['exitcode']; $this->status = self::STATUS_TERMINATED; From 6f16c626e9fcf3fc7ce9c79ef34432adcf792282 Mon Sep 17 00:00:00 2001 From: Jan Walther Date: Tue, 1 Aug 2023 16:37:55 +0200 Subject: [PATCH 25/38] [Process] Fix finding executables independently of open_basedir --- ExecutableFinder.php | 32 +++++++++------------- PhpExecutableFinder.php | 2 +- Tests/ExecutableFinderTest.php | 50 +++++----------------------------- 3 files changed, 21 insertions(+), 63 deletions(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index f392c962..a2f184d5 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -48,25 +48,10 @@ public function addSuffix(string $suffix) */ public function find(string $name, ?string $default = null, array $extraDirs = []) { - if (\ini_get('open_basedir')) { - $searchPath = array_merge(explode(\PATH_SEPARATOR, \ini_get('open_basedir')), $extraDirs); - $dirs = []; - foreach ($searchPath as $path) { - // Silencing against https://bugs.php.net/69240 - if (@is_dir($path)) { - $dirs[] = $path; - } else { - if (basename($path) == $name && @is_executable($path)) { - return $path; - } - } - } - } else { - $dirs = array_merge( - explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), - $extraDirs - ); - } + $dirs = array_merge( + explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), + $extraDirs + ); $suffixes = ['']; if ('\\' === \DIRECTORY_SEPARATOR) { @@ -78,9 +63,18 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) { return $file; } + + if (!@is_dir($dir) && basename($dir) === $name.$suffix && @is_executable($dir)) { + return $dir; + } } } + $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; + if (\function_exists('exec') && ($executablePath = strtok(@exec($command.' '.escapeshellarg($name)), \PHP_EOL)) && is_executable($executablePath)) { + return $executablePath; + } + return $default; } } diff --git a/PhpExecutableFinder.php b/PhpExecutableFinder.php index 45dbcca4..54fe7443 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -36,7 +36,7 @@ public function find(bool $includeArgs = true) if ($php = getenv('PHP_BINARY')) { if (!is_executable($php)) { $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; - if ($php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) { + if (\function_exists('exec') && $php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) { if (!is_executable($php)) { return false; } diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index 6d089def..a1b8d6d5 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -19,20 +19,9 @@ */ class ExecutableFinderTest extends TestCase { - private $path; - protected function tearDown(): void { - if ($this->path) { - // Restore path if it was changed. - putenv('PATH='.$this->path); - } - } - - private function setPath($path) - { - $this->path = getenv('PATH'); - putenv('PATH='.$path); + putenv('PATH='.($_SERVER['PATH'] ?? $_SERVER['Path'])); } public function testFind() @@ -41,7 +30,7 @@ public function testFind() $this->markTestSkipped('Cannot test when open_basedir is set'); } - $this->setPath(\dirname(\PHP_BINARY)); + putenv('PATH='.\dirname(\PHP_BINARY)); $finder = new ExecutableFinder(); $result = $finder->find($this->getPhpBinaryName()); @@ -57,7 +46,7 @@ public function testFindWithDefault() $expected = 'defaultValue'; - $this->setPath(''); + putenv('PATH='); $finder = new ExecutableFinder(); $result = $finder->find('foo', $expected); @@ -71,7 +60,7 @@ public function testFindWithNullAsDefault() $this->markTestSkipped('Cannot test when open_basedir is set'); } - $this->setPath(''); + putenv('PATH='); $finder = new ExecutableFinder(); @@ -86,7 +75,7 @@ public function testFindWithExtraDirs() $this->markTestSkipped('Cannot test when open_basedir is set'); } - $this->setPath(''); + putenv('PATH='); $extraDirs = [\dirname(\PHP_BINARY)]; @@ -109,6 +98,7 @@ public function testFindWithOpenBaseDir() $this->markTestSkipped('Cannot test when open_basedir is set'); } + putenv('PATH='.\dirname(\PHP_BINARY)); $initialOpenBaseDir = ini_set('open_basedir', \dirname(\PHP_BINARY).\PATH_SEPARATOR.'/'); try { @@ -121,32 +111,6 @@ public function testFindWithOpenBaseDir() } } - /** - * @runInSeparateProcess - */ - public function testFindProcessInOpenBasedir() - { - if (\ini_get('open_basedir')) { - $this->markTestSkipped('Cannot test when open_basedir is set'); - } - if ('\\' === \DIRECTORY_SEPARATOR) { - $this->markTestSkipped('Cannot run test on windows'); - } - - $this->setPath(''); - - $initialOpenBaseDir = ini_set('open_basedir', \PHP_BINARY.\PATH_SEPARATOR.'/'); - - try { - $finder = new ExecutableFinder(); - $result = $finder->find($this->getPhpBinaryName(), false); - - $this->assertSamePath(\PHP_BINARY, $result); - } finally { - ini_set('open_basedir', $initialOpenBaseDir); - } - } - public function testFindBatchExecutableOnWindows() { if (\ini_get('open_basedir')) { @@ -163,7 +127,7 @@ public function testFindBatchExecutableOnWindows() $this->assertFalse(is_executable($target)); - $this->setPath(sys_get_temp_dir()); + putenv('PATH='.sys_get_temp_dir()); $finder = new ExecutableFinder(); $result = $finder->find(basename($target), false); From 1b9fa82b5c62cd49da8c9e3952dd8531ada65096 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 17 Sep 2024 14:46:43 +0200 Subject: [PATCH 26/38] [Process] minor fix --- ExecutableFinder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index a2f184d5..6dc00b7c 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -71,7 +71,7 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ } $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; - if (\function_exists('exec') && ($executablePath = strtok(@exec($command.' '.escapeshellarg($name)), \PHP_EOL)) && is_executable($executablePath)) { + if (\function_exists('exec') && ($executablePath = strtok(@exec($command.' '.escapeshellarg($name)), \PHP_EOL)) && @is_executable($executablePath)) { return $executablePath; } From 95f3f19d0f8f06e4253c66a0828ddb69f8b8ede4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Mon, 23 Sep 2024 11:24:18 +0200 Subject: [PATCH 27/38] Add PR template and auto-close PR on subtree split repositories --- .gitattributes | 3 +-- .github/PULL_REQUEST_TEMPLATE.md | 8 ++++++++ .github/workflows/close-pull-request.yml | 20 ++++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/close-pull-request.yml diff --git a/.gitattributes b/.gitattributes index 84c7add0..14c3c359 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,3 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore +/.git* export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..4689c4da --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Please do not submit any Pull Requests here. They will be closed. +--- + +Please submit your PR here instead: +https://github.com/symfony/symfony + +This repository is what we call a "subtree split": a read-only subset of that main repository. +We're looking forward to your PR there! diff --git a/.github/workflows/close-pull-request.yml b/.github/workflows/close-pull-request.yml new file mode 100644 index 00000000..e55b4781 --- /dev/null +++ b/.github/workflows/close-pull-request.yml @@ -0,0 +1,20 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: | + Thanks for your Pull Request! We love contributions. + + However, you should instead open your PR on the main repository: + https://github.com/symfony/symfony + + This repository is what we call a "subtree split": a read-only subset of that main repository. + We're looking forward to your PR there! From e2d11b6ca03e3041ca2f53a4da3f16d2f8e45c5a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 29 Oct 2024 21:56:12 +0100 Subject: [PATCH 28/38] [Process] Fix handling empty path found in the PATH env var with ExecutableFinder --- ExecutableFinder.php | 3 +++ Tests/ExecutableFinderTest.php | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index 6dc00b7c..45d91e4a 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -60,6 +60,9 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ } foreach ($suffixes as $suffix) { foreach ($dirs as $dir) { + if ('' === $dir) { + $dir = '.'; + } if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) { return $file; } diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index a1b8d6d5..c4876e47 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -111,6 +111,9 @@ public function testFindWithOpenBaseDir() } } + /** + * @runInSeparateProcess + */ public function testFindBatchExecutableOnWindows() { if (\ini_get('open_basedir')) { @@ -138,6 +141,24 @@ public function testFindBatchExecutableOnWindows() $this->assertSamePath($target.'.BAT', $result); } + /** + * @runInSeparateProcess + */ + public function testEmptyDirInPath() + { + putenv(sprintf('PATH=%s:', \dirname(\PHP_BINARY))); + + touch('executable'); + chmod('executable', 0700); + + $finder = new ExecutableFinder(); + $result = $finder->find('executable'); + + $this->assertSame('./executable', $result); + + unlink('executable'); + } + private function assertSamePath($expected, $tested) { if ('\\' === \DIRECTORY_SEPARATOR) { From 651830b1a3cbae1b58bc63c8ba75c5a735abe522 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 30 Oct 2024 22:56:41 +0100 Subject: [PATCH 29/38] [Process] Properly deal with not-found executables on Windows --- ExecutableFinder.php | 10 ++++++++-- PhpExecutableFinder.php | 16 ++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index 6dc00b7c..d446bb65 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -70,8 +70,14 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ } } - $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; - if (\function_exists('exec') && ($executablePath = strtok(@exec($command.' '.escapeshellarg($name)), \PHP_EOL)) && @is_executable($executablePath)) { + if (!\function_exists('exec') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) { + return $default; + } + + $command = '\\' === \DIRECTORY_SEPARATOR ? 'where %s 2> NUL' : 'command -v -- %s'; + $execResult = exec(\sprintf($command, escapeshellarg($name))); + + if (($executablePath = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) && @is_executable($executablePath)) { return $executablePath; } diff --git a/PhpExecutableFinder.php b/PhpExecutableFinder.php index 54fe7443..b9aff690 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -35,12 +35,16 @@ public function find(bool $includeArgs = true) { if ($php = getenv('PHP_BINARY')) { if (!is_executable($php)) { - $command = '\\' === \DIRECTORY_SEPARATOR ? 'where' : 'command -v --'; - if (\function_exists('exec') && $php = strtok(exec($command.' '.escapeshellarg($php)), \PHP_EOL)) { - if (!is_executable($php)) { - return false; - } - } else { + if (!\function_exists('exec') || \strlen($php) !== strcspn($php, '/'.\DIRECTORY_SEPARATOR)) { + return false; + } + + $command = '\\' === \DIRECTORY_SEPARATOR ? 'where %s 2> NUL' : 'command -v -- %s'; + $execResult = exec(\sprintf($command, escapeshellarg($php))); + if (!$php = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) { + return false; + } + if (!is_executable($php)) { return false; } } From 46c203f382b73a2575d043e49a17073d3c808fad Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 2 Nov 2024 14:14:29 +0100 Subject: [PATCH 30/38] [Process] Return built-in cmd.exe commands directly in ExecutableFinder --- ExecutableFinder.php | 12 ++++++++++++ Tests/ExecutableFinderTest.php | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index 2293595c..1604b6f0 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -20,6 +20,13 @@ class ExecutableFinder { private $suffixes = ['.exe', '.bat', '.cmd', '.com']; + private const CMD_BUILTINS = [ + 'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date', + 'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto', + 'help', 'if', 'label', 'md', 'mkdir', 'mklink', 'move', 'path', 'pause', + 'popd', 'prompt', 'pushd', 'rd', 'rem', 'ren', 'rename', 'rmdir', 'set', + 'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol', + ]; /** * Replaces default suffixes of executable. @@ -48,6 +55,11 @@ public function addSuffix(string $suffix) */ public function find(string $name, ?string $default = null, array $extraDirs = []) { + // windows built-in commands that are present in cmd.exe should not be resolved using PATH as they do not exist as exes + if ('\\' === \DIRECTORY_SEPARATOR && \in_array(strtolower($name), self::CMD_BUILTINS, true)) { + return $name; + } + $dirs = array_merge( explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), $extraDirs diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index c4876e47..adb5556d 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -159,6 +159,18 @@ public function testEmptyDirInPath() unlink('executable'); } + public function testFindBuiltInCommandOnWindows() + { + if ('\\' !== \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Can be only tested on windows'); + } + + $finder = new ExecutableFinder(); + $this->assertSame('rmdir', $finder->find('RMDIR')); + $this->assertSame('cd', $finder->find('cd')); + $this->assertSame('move', $finder->find('MoVe')); + } + private function assertSamePath($expected, $tested) { if ('\\' === \DIRECTORY_SEPARATOR) { From b61fb1c70392905d5f5f99824324983124a1dd08 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Mon, 4 Nov 2024 09:44:46 +0100 Subject: [PATCH 31/38] [Process] Improve test cleanup by unlinking in a `finally` block --- Tests/ExecutableFinderTest.php | 36 +++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index c4876e47..3995e73a 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -125,18 +125,20 @@ public function testFindBatchExecutableOnWindows() $target = tempnam(sys_get_temp_dir(), 'example-windows-executable'); - touch($target); - touch($target.'.BAT'); - - $this->assertFalse(is_executable($target)); + try { + touch($target); + touch($target.'.BAT'); - putenv('PATH='.sys_get_temp_dir()); + $this->assertFalse(is_executable($target)); - $finder = new ExecutableFinder(); - $result = $finder->find(basename($target), false); + putenv('PATH='.sys_get_temp_dir()); - unlink($target); - unlink($target.'.BAT'); + $finder = new ExecutableFinder(); + $result = $finder->find(basename($target), false); + } finally { + unlink($target); + unlink($target.'.BAT'); + } $this->assertSamePath($target.'.BAT', $result); } @@ -148,15 +150,17 @@ public function testEmptyDirInPath() { putenv(sprintf('PATH=%s:', \dirname(\PHP_BINARY))); - touch('executable'); - chmod('executable', 0700); - - $finder = new ExecutableFinder(); - $result = $finder->find('executable'); + try { + touch('executable'); + chmod('executable', 0700); - $this->assertSame('./executable', $result); + $finder = new ExecutableFinder(); + $result = $finder->find('executable'); - unlink('executable'); + $this->assertSame('./executable', $result); + } finally { + unlink('executable'); + } } private function assertSamePath($expected, $tested) From a56fe7b6066efd82037aedfbd1c657e3bcce1810 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 4 Nov 2024 10:27:52 +0100 Subject: [PATCH 32/38] ignore case of built-in cmd.exe commands --- Tests/ExecutableFinderTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index adb5556d..e335e47c 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -166,9 +166,9 @@ public function testFindBuiltInCommandOnWindows() } $finder = new ExecutableFinder(); - $this->assertSame('rmdir', $finder->find('RMDIR')); - $this->assertSame('cd', $finder->find('cd')); - $this->assertSame('move', $finder->find('MoVe')); + $this->assertSame('rmdir', strtolower($finder->find('RMDIR'))); + $this->assertSame('cd', strtolower($finder->find('cd'))); + $this->assertSame('move', strtolower($finder->find('MoVe'))); } private function assertSamePath($expected, $tested) From 7be8366a553b0ea5ec03d01f68c2214b1ce82e89 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 4 Nov 2024 10:25:02 +0100 Subject: [PATCH 33/38] fix the directory separator being used --- Tests/ExecutableFinderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index c4876e47..f85d8c9a 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -146,7 +146,7 @@ public function testFindBatchExecutableOnWindows() */ public function testEmptyDirInPath() { - putenv(sprintf('PATH=%s:', \dirname(\PHP_BINARY))); + putenv(sprintf('PATH=%s%s', \dirname(\PHP_BINARY), \PATH_SEPARATOR)); touch('executable'); chmod('executable', 0700); From 81e1a0cdac68330b5acec27c427cf59be49c73f7 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 4 Nov 2024 11:01:19 +0100 Subject: [PATCH 34/38] fix the path separator being used --- Tests/ExecutableFinderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index 4a6c2c4b..fbeb7f07 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -157,7 +157,7 @@ public function testEmptyDirInPath() $finder = new ExecutableFinder(); $result = $finder->find('executable'); - $this->assertSame('./executable', $result); + $this->assertSame(sprintf('.%sexecutable', \PATH_SEPARATOR), $result); } finally { unlink('executable'); } From 72baf6b0591f07b051450bdf2608f93fb5c0a6e5 Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Mon, 4 Nov 2024 11:14:40 +0100 Subject: [PATCH 35/38] fix the constant being used --- Tests/ExecutableFinderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index fbeb7f07..4aadd9b2 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -157,7 +157,7 @@ public function testEmptyDirInPath() $finder = new ExecutableFinder(); $result = $finder->find('executable'); - $this->assertSame(sprintf('.%sexecutable', \PATH_SEPARATOR), $result); + $this->assertSame(sprintf('.%sexecutable', \DIRECTORY_SEPARATOR), $result); } finally { unlink('executable'); } From d94dda5a49f8e43523d6966ab705a754001d42fe Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 4 Nov 2024 11:43:26 +0100 Subject: [PATCH 36/38] [Process] Fix escaping /X arguments on Windows --- Process.php | 2 +- Tests/ProcessTest.php | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Process.php b/Process.php index 62addf1e..b8012dda 100644 --- a/Process.php +++ b/Process.php @@ -1638,7 +1638,7 @@ private function escapeArgument(?string $argument): string if (str_contains($argument, "\0")) { $argument = str_replace("\0", '?', $argument); } - if (!preg_match('/[\/()%!^"<>&|\s]/', $argument)) { + if (!preg_match('/[()%!^"<>&|\s]/', $argument)) { return $argument; } $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument); diff --git a/Tests/ProcessTest.php b/Tests/ProcessTest.php index a2e370de..e4d92874 100644 --- a/Tests/ProcessTest.php +++ b/Tests/ProcessTest.php @@ -1424,7 +1424,12 @@ public function testGetCommandLine() { $p = new Process(['/usr/bin/php']); - $expected = '\\' === \DIRECTORY_SEPARATOR ? '"/usr/bin/php"' : "'/usr/bin/php'"; + $expected = '\\' === \DIRECTORY_SEPARATOR ? '/usr/bin/php' : "'/usr/bin/php'"; + $this->assertSame($expected, $p->getCommandLine()); + + $p = new Process(['cd', '/d']); + + $expected = '\\' === \DIRECTORY_SEPARATOR ? 'cd /d' : "'cd' '/d'"; $this->assertSame($expected, $p->getCommandLine()); } From 05c2ccc705cb0336becfdc10f6dd67896d9ba91a Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 28 Oct 2024 12:35:32 +0100 Subject: [PATCH 37/38] [Process] Use %PATH% before %CD% to load the shell on Windows --- ExecutableFinder.php | 14 ++++++++------ PhpExecutableFinder.php | 15 ++------------- Process.php | 9 ++++++++- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/ExecutableFinder.php b/ExecutableFinder.php index 1604b6f0..89edd22f 100644 --- a/ExecutableFinder.php +++ b/ExecutableFinder.php @@ -19,7 +19,6 @@ */ class ExecutableFinder { - private $suffixes = ['.exe', '.bat', '.cmd', '.com']; private const CMD_BUILTINS = [ 'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date', 'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto', @@ -28,6 +27,8 @@ class ExecutableFinder 'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol', ]; + private $suffixes = []; + /** * Replaces default suffixes of executable. */ @@ -65,11 +66,13 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ $extraDirs ); - $suffixes = ['']; + $suffixes = []; if ('\\' === \DIRECTORY_SEPARATOR) { $pathExt = getenv('PATHEXT'); - $suffixes = array_merge($pathExt ? explode(\PATH_SEPARATOR, $pathExt) : $this->suffixes, $suffixes); + $suffixes = $this->suffixes; + $suffixes = array_merge($suffixes, $pathExt ? explode(\PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com']); } + $suffixes = '' !== pathinfo($name, PATHINFO_EXTENSION) ? array_merge([''], $suffixes) : array_merge($suffixes, ['']); foreach ($suffixes as $suffix) { foreach ($dirs as $dir) { if ('' === $dir) { @@ -85,12 +88,11 @@ public function find(string $name, ?string $default = null, array $extraDirs = [ } } - if (!\function_exists('exec') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) { + if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('exec') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) { return $default; } - $command = '\\' === \DIRECTORY_SEPARATOR ? 'where %s 2> NUL' : 'command -v -- %s'; - $execResult = exec(\sprintf($command, escapeshellarg($name))); + $execResult = exec('command -v -- '.escapeshellarg($name)); if (($executablePath = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) && @is_executable($executablePath)) { return $executablePath; diff --git a/PhpExecutableFinder.php b/PhpExecutableFinder.php index b9aff690..c3a9680d 100644 --- a/PhpExecutableFinder.php +++ b/PhpExecutableFinder.php @@ -34,19 +34,8 @@ public function __construct() public function find(bool $includeArgs = true) { if ($php = getenv('PHP_BINARY')) { - if (!is_executable($php)) { - if (!\function_exists('exec') || \strlen($php) !== strcspn($php, '/'.\DIRECTORY_SEPARATOR)) { - return false; - } - - $command = '\\' === \DIRECTORY_SEPARATOR ? 'where %s 2> NUL' : 'command -v -- %s'; - $execResult = exec(\sprintf($command, escapeshellarg($php))); - if (!$php = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) { - return false; - } - if (!is_executable($php)) { - return false; - } + if (!is_executable($php) && !$php = $this->executableFinder->find($php)) { + return false; } if (@is_dir($php)) { diff --git a/Process.php b/Process.php index 62addf1e..0f3457f3 100644 --- a/Process.php +++ b/Process.php @@ -1592,7 +1592,14 @@ function ($m) use (&$env, &$varCache, &$varCount, $uid) { $cmd ); - $cmd = 'cmd /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; + static $comSpec; + + if (!$comSpec && $comSpec = (new ExecutableFinder())->find('cmd.exe')) { + // Escape according to CommandLineToArgvW rules + $comSpec = '"'.preg_replace('{(\\\\*+)"}', '$1$1\"', $comSpec) .'"'; + } + + $cmd = ($comSpec ?? 'cmd').' /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; foreach ($this->processPipes->getFiles() as $offset => $filename) { $cmd .= ' '.$offset.'>"'.$filename.'"'; } From 01906871cb9b5e3cf872863b91aba4ec9767daf4 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 6 Nov 2024 10:18:28 +0100 Subject: [PATCH 38/38] [Process] Fix test --- Tests/ExecutableFinderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ExecutableFinderTest.php b/Tests/ExecutableFinderTest.php index 4aadd9b2..84e5b3c3 100644 --- a/Tests/ExecutableFinderTest.php +++ b/Tests/ExecutableFinderTest.php @@ -123,7 +123,7 @@ public function testFindBatchExecutableOnWindows() $this->markTestSkipped('Can be only tested on windows'); } - $target = tempnam(sys_get_temp_dir(), 'example-windows-executable'); + $target = str_replace('.tmp', '_tmp', tempnam(sys_get_temp_dir(), 'example-windows-executable')); try { touch($target);