From 4aba9d9725f63b4708a8f04ac3c8c2e222377f74 Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Mon, 22 May 2023 22:52:43 +0100 Subject: [PATCH] Absorb pocketmine/classloader into the core code the only use for this class is to facilitate random runtime plugin loading, and it's not complete even for that purpose. Since nothing but PM uses pocketmine/classloader anyway, it doesn't make sense to have it outside the core. As with LogPthreads, it's just adding more maintenance work. --- composer.json | 1 - composer.lock | 49 +----- src/PocketMine.php | 3 +- src/Server.php | 5 +- src/plugin/PharPluginLoader.php | 3 +- src/scheduler/AsyncPool.php | 3 +- src/thread/CommonThreadPartsTrait.php | 10 +- src/thread/ThreadSafeClassLoader.php | 182 ++++++++++++++++++++++ tests/phpunit/scheduler/AsyncPoolTest.php | 3 +- 9 files changed, 199 insertions(+), 60 deletions(-) create mode 100644 src/thread/ThreadSafeClassLoader.php diff --git a/composer.json b/composer.json index c8fb559d74..fd47dca93e 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,6 @@ "pocketmine/bedrock-protocol": "~21.0.0+bedrock-1.19.80", "pocketmine/binaryutils": "^0.2.1", "pocketmine/callback-validator": "^1.0.2", - "pocketmine/classloader": "dev-stable", "pocketmine/color": "^0.3.0", "pocketmine/errorhandler": "^0.6.0", "pocketmine/locale-data": "~2.19.0", diff --git a/composer.lock b/composer.lock index 05d87b634d..a9d1e6d810 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0dc836612512d87a694945ffb87a4096", + "content-hash": "812a98ecf031488987b3216e785fee19", "packages": [ { "name": "adhocore/json-comment", @@ -464,52 +464,6 @@ }, "time": "2020-12-11T01:45:37+00:00" }, - { - "name": "pocketmine/classloader", - "version": "dev-stable", - "source": { - "type": "git", - "url": "https://github.com/pmmp/ClassLoader.git", - "reference": "e15c9b4d310581d2d2c9bf2794869cb940e011e1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/pmmp/ClassLoader/zipball/e15c9b4d310581d2d2c9bf2794869cb940e011e1", - "reference": "e15c9b4d310581d2d2c9bf2794869cb940e011e1", - "shasum": "" - }, - "require": { - "ext-pmmpthread": "^6.0", - "ext-reflection": "*", - "php": "^8.1" - }, - "conflict": { - "pocketmine/spl": "<0.4" - }, - "require-dev": { - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "1.10.15", - "phpstan/phpstan-strict-rules": "^1.0", - "phpunit/phpunit": "^9.5" - }, - "default-branch": true, - "type": "library", - "autoload": { - "classmap": [ - "./src" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0" - ], - "description": "Ad-hoc autoloading components used by PocketMine-MP", - "support": { - "issues": "https://github.com/pmmp/ClassLoader/issues", - "source": "https://github.com/pmmp/ClassLoader/tree/stable" - }, - "time": "2023-05-19T23:39:02+00:00" - }, { "name": "pocketmine/color", "version": "0.3.1", @@ -3252,7 +3206,6 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { - "pocketmine/classloader": 20, "pocketmine/snooze": 20 }, "prefer-stable": false, diff --git a/src/PocketMine.php b/src/PocketMine.php index 018d8e0bf1..7003cca85d 100644 --- a/src/PocketMine.php +++ b/src/PocketMine.php @@ -26,6 +26,7 @@ namespace pocketmine { use Composer\InstalledVersions; use pocketmine\errorhandler\ErrorToExceptionHandler; use pocketmine\thread\ThreadManager; + use pocketmine\thread\ThreadSafeClassLoader; use pocketmine\utils\Filesystem; use pocketmine\utils\MainLogger; use pocketmine\utils\Process; @@ -327,7 +328,7 @@ JIT_WARNING /* * We now use the Composer autoloader, but this autoloader is still for loading plugins. */ - $autoloader = new \BaseClassLoader(); + $autoloader = new ThreadSafeClassLoader(); $autoloader->register(false); new Server($autoloader, $logger, $dataPath, $pluginPath); diff --git a/src/Server.php b/src/Server.php index df419c918e..f604c93d7e 100644 --- a/src/Server.php +++ b/src/Server.php @@ -93,6 +93,7 @@ use pocketmine\scheduler\AsyncPool; use pocketmine\snooze\SleeperHandler; use pocketmine\stats\SendUsageTask; use pocketmine\thread\log\AttachableThreadSafeLogger; +use pocketmine\thread\ThreadSafeClassLoader; use pocketmine\timings\Timings; use pocketmine\timings\TimingsHandler; use pocketmine\updater\UpdateChecker; @@ -414,7 +415,7 @@ class Server{ return $this->configGroup->getConfigString("motd", self::DEFAULT_SERVER_NAME); } - public function getLoader() : \DynamicClassLoader{ + public function getLoader() : ThreadSafeClassLoader{ return $this->autoloader; } @@ -760,7 +761,7 @@ class Server{ } public function __construct( - private \DynamicClassLoader $autoloader, + private ThreadSafeClassLoader $autoloader, private AttachableThreadSafeLogger $logger, string $dataPath, string $pluginPath diff --git a/src/plugin/PharPluginLoader.php b/src/plugin/PharPluginLoader.php index b812f20c8a..a8dc04804a 100644 --- a/src/plugin/PharPluginLoader.php +++ b/src/plugin/PharPluginLoader.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\plugin; +use pocketmine\thread\ThreadSafeClassLoader; use function is_file; use function str_ends_with; @@ -31,7 +32,7 @@ use function str_ends_with; */ class PharPluginLoader implements PluginLoader{ public function __construct( - private \DynamicClassLoader $loader + private ThreadSafeClassLoader $loader ){} public function canLoadPlugin(string $path) : bool{ diff --git a/src/scheduler/AsyncPool.php b/src/scheduler/AsyncPool.php index 72d8b29e5a..1ee726765e 100644 --- a/src/scheduler/AsyncPool.php +++ b/src/scheduler/AsyncPool.php @@ -28,6 +28,7 @@ use pmmp\thread\ThreadSafeArray; use pocketmine\snooze\SleeperHandler; use pocketmine\snooze\SleeperNotifier; use pocketmine\thread\log\ThreadSafeLogger; +use pocketmine\thread\ThreadSafeClassLoader; use pocketmine\utils\Utils; use function array_keys; use function array_map; @@ -70,7 +71,7 @@ class AsyncPool{ public function __construct( protected int $size, private int $workerMemoryLimit, - private \ClassLoader $classLoader, + private ThreadSafeClassLoader $classLoader, private ThreadSafeLogger $logger, private SleeperHandler $eventLoop ){} diff --git a/src/thread/CommonThreadPartsTrait.php b/src/thread/CommonThreadPartsTrait.php index a2429b056b..c35dd77914 100644 --- a/src/thread/CommonThreadPartsTrait.php +++ b/src/thread/CommonThreadPartsTrait.php @@ -30,8 +30,8 @@ use function error_reporting; trait CommonThreadPartsTrait{ /** - * @var ThreadSafeArray|\ClassLoader[]|null - * @phpstan-var ThreadSafeArray|null + * @var ThreadSafeArray|ThreadSafeClassLoader[]|null + * @phpstan-var ThreadSafeArray|null */ private ?ThreadSafeArray $classLoaders = null; protected ?string $composerAutoloaderPath = null; @@ -39,14 +39,14 @@ trait CommonThreadPartsTrait{ protected bool $isKilled = false; /** - * @return \ClassLoader[] + * @return ThreadSafeClassLoader[] */ public function getClassLoaders() : ?array{ return $this->classLoaders !== null ? (array) $this->classLoaders : null; } /** - * @param \ClassLoader[] $autoloaders + * @param ThreadSafeClassLoader[] $autoloaders */ public function setClassLoaders(?array $autoloaders = null) : void{ $this->composerAutoloaderPath = \pocketmine\COMPOSER_AUTOLOADER_PATH; @@ -82,7 +82,7 @@ trait CommonThreadPartsTrait{ $autoloaders = $this->classLoaders; if($autoloaders !== null){ foreach($autoloaders as $autoloader){ - /** @var \ClassLoader $autoloader */ + /** @var ThreadSafeClassLoader $autoloader */ $autoloader->register(false); } } diff --git a/src/thread/ThreadSafeClassLoader.php b/src/thread/ThreadSafeClassLoader.php new file mode 100644 index 0000000000..95b983dc1b --- /dev/null +++ b/src/thread/ThreadSafeClassLoader.php @@ -0,0 +1,182 @@ + + */ + private $fallbackLookup; + /** + * @var ThreadSafeArray|string[][] + * @phpstan-var ThreadSafeArray> + */ + private $psr4Lookup; + + public function __construct(){ + $this->fallbackLookup = new ThreadSafeArray(); + $this->psr4Lookup = new ThreadSafeArray(); + } + + protected function normalizePath(string $path) : string{ + $parts = explode("://", $path, 2); + if(count($parts) === 2){ + return $parts[0] . "://" . str_replace('/', DIRECTORY_SEPARATOR, $parts[1]); + } + return str_replace('/', DIRECTORY_SEPARATOR, $parts[0]); + } + + public function addPath(string $namespacePrefix, string $path, bool $prepend = false) : void{ + $path = $this->normalizePath($path); + if($namespacePrefix === '' || $namespacePrefix === '\\'){ + $this->fallbackLookup->synchronized(function() use ($path, $prepend) : void{ + $this->appendOrPrependLookupEntry($this->fallbackLookup, $path, $prepend); + }); + }else{ + $namespacePrefix = trim($namespacePrefix, '\\') . '\\'; + $this->psr4Lookup->synchronized(function() use ($namespacePrefix, $path, $prepend) : void{ + $list = $this->psr4Lookup[$namespacePrefix] ?? null; + if($list === null){ + $list = $this->psr4Lookup[$namespacePrefix] = new ThreadSafeArray(); + } + $this->appendOrPrependLookupEntry($list, $path, $prepend); + }); + } + } + + /** + * @phpstan-param ThreadSafeArray $list + */ + protected function appendOrPrependLookupEntry(ThreadSafeArray $list, string $entry, bool $prepend) : void{ + if($prepend){ + $entries = $this->getAndRemoveLookupEntries($list); + $list[] = $entry; + foreach($entries as $removedEntry){ + $list[] = $removedEntry; + } + }else{ + $list[] = $entry; + } + } + + /** + * @return string[] + * + * @phpstan-param ThreadSafeArray $list + * @phpstan-return list + */ + protected function getAndRemoveLookupEntries(ThreadSafeArray $list) : array{ + $entries = []; + while(($entry = $list->shift()) !== null){ + $entries[] = $entry; + } + return $entries; + } + + public function register(bool $prepend = false) : bool{ + return spl_autoload_register(function(string $name) : void{ + $this->loadClass($name); + }, true, $prepend); + } + + /** + * Called when there is a class to load + */ + public function loadClass(string $name) : bool{ + $path = $this->findClass($name); + if($path !== null){ + include($path); + if(!class_exists($name, false) && !interface_exists($name, false) && !trait_exists($name, false)){ + return false; + } + + if(method_exists($name, "onClassLoaded") && (new \ReflectionClass($name))->getMethod("onClassLoaded")->isStatic()){ + $name::onClassLoaded(); + } + + return true; + } + + return false; + } + + /** + * Returns the path for the class, if any + */ + public function findClass(string $name) : ?string{ + $baseName = str_replace("\\", DIRECTORY_SEPARATOR, $name); + + foreach($this->fallbackLookup as $path){ + $filename = $path . DIRECTORY_SEPARATOR . $baseName . ".php"; + if(file_exists($filename)){ + return $filename; + } + } + + // PSR-4 lookup + $logicalPathPsr4 = $baseName . ".php"; + + return $this->psr4Lookup->synchronized(function() use ($name, $logicalPathPsr4) : ?string{ + $subPath = $name; + while(false !== $lastPos = strrpos($subPath, '\\')){ + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + $lookup = $this->psr4Lookup[$search] ?? null; + if($lookup !== null){ + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach($lookup as $dir){ + if(file_exists($file = $dir . $pathEnd)){ + return $file; + } + } + } + } + return null; + }); + } +} diff --git a/tests/phpunit/scheduler/AsyncPoolTest.php b/tests/phpunit/scheduler/AsyncPoolTest.php index 436251d9e3..fd7dc344ac 100644 --- a/tests/phpunit/scheduler/AsyncPoolTest.php +++ b/tests/phpunit/scheduler/AsyncPoolTest.php @@ -27,6 +27,7 @@ use PHPUnit\Framework\TestCase; use pmmp\thread\ThreadSafeArray; use pocketmine\promise\PromiseResolver; use pocketmine\snooze\SleeperHandler; +use pocketmine\thread\ThreadSafeClassLoader; use pocketmine\utils\MainLogger; use function define; use function dirname; @@ -45,7 +46,7 @@ class AsyncPoolTest extends TestCase{ public function setUp() : void{ @define('pocketmine\\COMPOSER_AUTOLOADER_PATH', dirname(__DIR__, 3) . '/vendor/autoload.php'); $this->mainLogger = new MainLogger(tempnam(sys_get_temp_dir(), "pmlog"), false, "Main", new \DateTimeZone('UTC')); - $this->pool = new AsyncPool(2, 1024, new \BaseClassLoader(), $this->mainLogger, new SleeperHandler()); + $this->pool = new AsyncPool(2, 1024, new ThreadSafeClassLoader(), $this->mainLogger, new SleeperHandler()); } public function tearDown() : void{