mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2024-11-13 14:09:21 +00:00
0e5395c59b
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
169 lines
5.0 KiB
PHP
169 lines
5.0 KiB
PHP
<?php
|
|
|
|
/*
|
|
*
|
|
* ____ _ _ __ __ _ __ __ ____
|
|
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
|
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
|
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
|
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* @author PocketMine Team
|
|
* @link http://www.pocketmine.net/
|
|
*
|
|
*
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace pocketmine\server_phar_stub;
|
|
|
|
use function clearstatcache;
|
|
use function copy;
|
|
use function fclose;
|
|
use function fflush;
|
|
use function flock;
|
|
use function fopen;
|
|
use function fwrite;
|
|
use function getmypid;
|
|
use function hrtime;
|
|
use function is_dir;
|
|
use function is_file;
|
|
use function mkdir;
|
|
use function number_format;
|
|
use function str_replace;
|
|
use function stream_get_contents;
|
|
use function sys_get_temp_dir;
|
|
use function tempnam;
|
|
use function unlink;
|
|
use const DIRECTORY_SEPARATOR;
|
|
use const LOCK_EX;
|
|
use const LOCK_NB;
|
|
use const LOCK_UN;
|
|
|
|
/**
|
|
* Finds the appropriate tmp directory to store the decompressed phar cache, accounting for potential file name
|
|
* collisions.
|
|
*/
|
|
function preparePharCacheDirectory() : string{
|
|
clearstatcache();
|
|
|
|
$i = 0;
|
|
do{
|
|
$tmpPath = sys_get_temp_dir() . '/PocketMine-MP-phar-cache.' . $i;
|
|
$i++;
|
|
}while(is_file($tmpPath));
|
|
if(!@mkdir($tmpPath) && !is_dir($tmpPath)){
|
|
throw new \RuntimeException("Failed to create temporary directory $tmpPath. Please ensure the disk has enough space and that the current user has permission to write to this location.");
|
|
}
|
|
|
|
return $tmpPath;
|
|
}
|
|
|
|
/**
|
|
* Deletes caches left behind by previous server instances.
|
|
* This ensures that the tmp directory doesn't get flooded by servers crashing in restart loops.
|
|
*/
|
|
function cleanupPharCache(string $tmpPath) : void{
|
|
clearstatcache();
|
|
|
|
/** @var string[] $matches */
|
|
foreach(new \RegexIterator(
|
|
new \FilesystemIterator(
|
|
$tmpPath,
|
|
\FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS
|
|
),
|
|
'/(.+)\.lock$/',
|
|
\RegexIterator::GET_MATCH
|
|
) as $matches){
|
|
$lockFilePath = $matches[0];
|
|
$baseTmpPath = $matches[1];
|
|
|
|
$file = @fopen($lockFilePath, "rb");
|
|
if($file === false){
|
|
//another process probably deleted the lock file already
|
|
continue;
|
|
}
|
|
|
|
if(flock($file, LOCK_EX | LOCK_NB)){
|
|
//this tmpfile is no longer in use
|
|
flock($file, LOCK_UN);
|
|
fclose($file);
|
|
|
|
unlink($lockFilePath);
|
|
unlink($baseTmpPath . ".tar");
|
|
unlink($baseTmpPath);
|
|
echo "Deleted stale phar cache at $baseTmpPath\n";
|
|
}else{
|
|
$pid = stream_get_contents($file);
|
|
fclose($file);
|
|
|
|
echo "Phar cache at $baseTmpPath is still in use by PID $pid\n";
|
|
}
|
|
}
|
|
}
|
|
|
|
function convertPharToTar(string $tmpName, string $pharPath) : string{
|
|
$tmpPharPath = $tmpName . ".phar";
|
|
copy($pharPath, $tmpPharPath);
|
|
|
|
$phar = new \Phar($tmpPharPath);
|
|
//phar requires phar.readonly=0, and zip doesn't support disabling compression - tar is the only viable option
|
|
//we don't need phar anyway since we don't need to directly execute the file, only require files from inside it
|
|
$phar->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';
|