diff --git a/src/Command/FixerApplication.php b/src/Command/FixerApplication.php index d0deb1607a..800d79f490 100644 --- a/src/Command/FixerApplication.php +++ b/src/Command/FixerApplication.php @@ -25,6 +25,7 @@ use PHPStan\Internal\DirectoryCreatorException; use PHPStan\Internal\HttpClientFactory; use PHPStan\Parallel\ForkParallelChecker; +use PHPStan\Parallel\PharAutoloaderLock; use PHPStan\PhpDoc\StubFilesProvider; use PHPStan\Process\ForkedProcessPromise; use PHPStan\Process\ProcessCanceledException; @@ -100,6 +101,7 @@ public function __construct( private HttpClientFactory $httpClientFactory, private ForkParallelChecker $forkParallelChecker, private FixerWorkerRunner $fixerWorkerRunner, + private PharAutoloaderLock $pharAutoloaderLock, ) { } @@ -457,8 +459,13 @@ private function analyse( }); }); + $useFork = $this->forkParallelChecker->isSupported(); + if ($useFork) { + $this->pharAutoloaderLock->install(); + } + $process = $this->createProcessPromise( - $this->forkParallelChecker->isSupported(), + $useFork, $loop, $server, $mainScript, diff --git a/src/Parallel/ParallelAnalyser.php b/src/Parallel/ParallelAnalyser.php index c5be7a4e54..5ba28f70d1 100644 --- a/src/Parallel/ParallelAnalyser.php +++ b/src/Parallel/ParallelAnalyser.php @@ -54,6 +54,7 @@ public function __construct( private int $decoderBufferSize, private ForkParallelChecker $forkParallelChecker, private WorkerRunner $workerRunner, + private PharAutoloaderLock $pharAutoloaderLock, ) { $this->processTimeout = max($processTimeout, self::DEFAULT_TIMEOUT); @@ -175,6 +176,9 @@ public function analyse( }; $useFork = $this->forkParallelChecker->isSupported(); + if ($useFork) { + $this->pharAutoloaderLock->install(); + } for ($i = 0; $i < $numberOfProcesses; $i++) { if (count($jobs) === 0) { diff --git a/src/Parallel/PharAutoloaderLock.php b/src/Parallel/PharAutoloaderLock.php new file mode 100644 index 0000000000..2ddaf5d1d8 --- /dev/null +++ b/src/Parallel/PharAutoloaderLock.php @@ -0,0 +1,129 @@ +installed) { + return; + } + $this->installed = true; + + if (Phar::running(false) === '') { + return; + } + + $lockPath = sys_get_temp_dir() . '/phpstan-fork-phar-lock-' . getmypid() . '-' . uniqid(); + touch($lockPath); + + // Wrap every registered autoloader callable. Using spl_autoload_* + // rather than Composer\Autoload\ClassLoader::getRegisteredLoaders() + // keeps this file free of any reference to Composer's namespace — + // php-scoper would otherwise rewrite that reference to the prefixed + // form, which does not exist at runtime inside the built phar. + foreach (spl_autoload_functions() as $callback) { + spl_autoload_unregister($callback); + spl_autoload_register(static function (string $class) use ($callback, $lockPath): void { + if (self::$depth > 0) { + // Already inside this process's locked autoload — nested + // reads from libphar are sequential within a single + // process, so they don't need (and would deadlock on) a + // second flock. + $callback($class); + return; + } + $fh = fopen($lockPath, 'r'); + if ($fh === false) { + $callback($class); + return; + } + flock($fh, LOCK_EX); + self::$depth++; + try { + $callback($class); + } finally { + self::$depth--; + flock($fh, LOCK_UN); + fclose($fh); + } + }); + } + + $parentPid = getmypid(); + register_shutdown_function(static function () use ($parentPid, $lockPath): void { + if (getmypid() !== $parentPid) { + return; + } + @unlink($lockPath); + }); + } + +}