From 0e5395c59bdc0d130eafa6c9b385075214988e52 Mon Sep 17 00:00:00 2001 From: Dylan T Date: Mon, 18 Mar 2024 16:48:17 +0000 Subject: [PATCH] PocketMine-MP.phar self-extraction to decompressed cache (#6217) Because ext-phar sucks, tmp gets spammed by cache files for every thread when loading files from the phar on the fly. Instead, we convert the `.phar` into a decompressed `.tar` in the tmp directory and require files from inside it. Surprisingly, this works because `ext-phar` supports `tar` and `zip` natively. No stream wrapper is required, as the `PocketMine.php` bootstrap loads files relative to its location, so the cache is automatically used for everything. To be honest I would rather get rid of phars entirely, but they are still the easiest way to have PhpStorm load PocketMine-MP API information for now, and the alternatives are more complicated and inconvenient. ### Caveats Everywhere that previously used `new Phar(Phar::running(false))` in the core code needs to be updated to use `PharData` for this to work correctly. Plugins don't need to do anything. ### Why not just use `Phar::decompressFiles()`? This requires setting `phar.readonly` to `0`, which is a security issue. Technically, we could have used a subprocess to do this, but it just didn't seem right. ### WTF? `phar://` can be used on `tar` files??? Yup. I was just as surprised to find out that `require` works in such contexts. ### Relevant issues - Closes #6214 ## Changes ### API changes None. ### Behavioural changes Server startup will be slightly slower, as the phar has to decompress and convert itself into a `.tar`. However, testing showed that this generally takes less than 200 ms, so it should be barely noticeable. ## Backwards compatibility No BC issues. ## Tests Locally tested and the CI will also verify --- build/server-phar-stub.php | 168 +++++++++++++++++++++++++++++++++++++ build/server-phar.php | 18 +--- src/VersionInfo.php | 6 +- 3 files changed, 175 insertions(+), 17 deletions(-) create mode 100644 build/server-phar-stub.php diff --git a/build/server-phar-stub.php b/build/server-phar-stub.php new file mode 100644 index 000000000..b4018e3a7 --- /dev/null +++ b/build/server-phar-stub.php @@ -0,0 +1,168 @@ +convertToData(\Phar::TAR, \Phar::NONE); + unset($phar); + \Phar::unlinkArchive($tmpPharPath); + + return $tmpName . ".tar"; +} + +/** + * Locks a phar tmp cache to prevent it from being deleted by other server instances. + * This code looks similar to Filesystem::createLockFile(), but we can't use that because it's inside the compressed + * phar. + */ +function lockPharCache(string $lockFilePath) : void{ + //this static variable will keep the file(s) locked until the process ends + static $lockFiles = []; + + $lockFile = fopen($lockFilePath, "wb"); + if($lockFile === false){ + throw new \RuntimeException("Failed to open temporary file"); + } + flock($lockFile, LOCK_EX); //this tells other server instances not to delete this cache file + fwrite($lockFile, (string) getmypid()); //maybe useful for debugging + fflush($lockFile); + $lockFiles[$lockFilePath] = $lockFile; +} + +/** + * Prepares a decompressed .tar of PocketMine-MP.phar in the system temp directory for loading code from. + * + * @return string path to the temporary decompressed phar (actually a .tar) + */ +function preparePharCache(string $tmpPath, string $pharPath) : string{ + clearstatcache(); + + $tmpName = tempnam($tmpPath, "PMMP"); + if($tmpName === false){ + throw new \RuntimeException("Failed to create temporary file"); + } + + lockPharCache($tmpName . ".lock"); + return convertPharToTar($tmpName, $pharPath); +} + +$tmpDir = preparePharCacheDirectory(); +cleanupPharCache($tmpDir); +echo "Preparing PocketMine-MP.phar decompressed cache...\n"; +$start = hrtime(true); +$cacheName = preparePharCache($tmpDir, __FILE__); +echo "Cache ready at $cacheName in " . number_format((hrtime(true) - $start) / 1e9, 2) . "s\n"; + +require 'phar://' . str_replace(DIRECTORY_SEPARATOR, '/', $cacheName) . '/src/PocketMine.php'; diff --git a/build/server-phar.php b/build/server-phar.php index 8b4d410ce..f6bb29d51 100644 --- a/build/server-phar.php +++ b/build/server-phar.php @@ -23,7 +23,9 @@ declare(strict_types=1); namespace pocketmine\build\server_phar; +use pocketmine\utils\Filesystem; use pocketmine\utils\Git; +use Symfony\Component\Filesystem\Path; use function array_map; use function count; use function dirname; @@ -169,21 +171,7 @@ function main() : void{ 'git' => $gitHash, 'build' => $build ], - <<<'STUB' -getMetadata(); if(isset($meta["git"])){ $gitHash = $meta["git"]; @@ -82,7 +83,8 @@ final class VersionInfo{ if(self::$buildNumber === null){ self::$buildNumber = 0; if(\Phar::running(true) !== ""){ - $phar = new \Phar(\Phar::running(false)); + $pharPath = \Phar::running(false); + $phar = \Phar::isValidPharFilename($pharPath) ? new \Phar($pharPath) : new \PharData($pharPath); $meta = $phar->getMetadata(); if(is_array($meta) && isset($meta["build"]) && is_int($meta["build"])){ self::$buildNumber = $meta["build"];