76 protected array $plugins = [];
82 protected array $enabledPlugins = [];
85 private array $pluginDependents = [];
87 private bool $loadPluginsGuard =
false;
93 protected array $fileAssociations = [];
95 public function __construct(
97 private ?
string $pluginDataDirectory,
100 if($this->pluginDataDirectory !==
null){
101 if(!file_exists($this->pluginDataDirectory)){
102 @mkdir($this->pluginDataDirectory, 0777,
true);
103 }elseif(!is_dir($this->pluginDataDirectory)){
104 throw new \RuntimeException(
"Plugin data path $this->pluginDataDirectory exists and is not a directory");
109 public function getPlugin(
string $name) : ?
Plugin{
110 if(isset($this->plugins[$name])){
111 return $this->plugins[$name];
117 public function registerInterface(
PluginLoader $loader) :
void{
118 $this->fileAssociations[get_class($loader)] = $loader;
126 return $this->plugins;
129 private function getDataDirectory(
string $pluginPath,
string $pluginName) : string{
130 if($this->pluginDataDirectory !== null){
131 return Path::join($this->pluginDataDirectory, $pluginName);
133 return Path::join(dirname($pluginPath), $pluginName);
136 private function internalLoadPlugin(
string $path, PluginLoader $loader, PluginDescription $description) : ?Plugin{
137 $language = $this->server->getLanguage();
138 $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_load($description->getFullName())));
140 $dataFolder = $this->getDataDirectory($path, $description->getName());
141 if(file_exists($dataFolder) && !is_dir($dataFolder)){
142 $this->server->getLogger()->critical($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
143 $description->getName(),
144 KnownTranslationFactory::pocketmine_plugin_badDataFolder($dataFolder)
148 if(!file_exists($dataFolder)){
149 mkdir($dataFolder, 0777,
true);
152 $prefixed = $loader->getAccessProtocol() . $path;
153 $loader->loadPlugin($prefixed);
155 $mainClass = $description->getMain();
156 if(!class_exists($mainClass,
true)){
157 $this->
server->getLogger()->critical($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
158 $description->getName(),
159 KnownTranslationFactory::pocketmine_plugin_mainClassNotFound()
163 if(!is_a($mainClass, Plugin::class,
true)){
164 $this->
server->getLogger()->critical($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
165 $description->getName(),
166 KnownTranslationFactory::pocketmine_plugin_mainClassWrongType(Plugin::class)
170 $reflect = new \ReflectionClass($mainClass);
171 if(!$reflect->isInstantiable()){
172 $this->
server->getLogger()->critical($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
173 $description->getName(),
174 KnownTranslationFactory::pocketmine_plugin_mainClassAbstract()
179 $permManager = PermissionManager::getInstance();
180 foreach($description->getPermissions() as $permsGroup){
181 foreach($permsGroup as $perm){
182 if($permManager->getPermission($perm->getName()) !==
null){
183 $this->
server->getLogger()->critical($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
184 $description->getName(),
185 KnownTranslationFactory::pocketmine_plugin_duplicatePermissionError($perm->getName())
191 $opRoot = $permManager->getPermission(DefaultPermissions::ROOT_OPERATOR);
192 $everyoneRoot = $permManager->getPermission(DefaultPermissions::ROOT_USER);
193 foreach(Utils::stringifyKeys($description->getPermissions()) as $default => $perms){
194 foreach($perms as $perm){
195 $permManager->addPermission($perm);
197 case PermissionParser::DEFAULT_TRUE:
198 $everyoneRoot->addChild($perm->getName(),
true);
200 case PermissionParser::DEFAULT_OP:
201 $opRoot->addChild($perm->getName(),
true);
203 case PermissionParser::DEFAULT_NOT_OP:
210 $everyoneRoot->addChild($perm->getName(),
true);
211 $opRoot->addChild($perm->getName(),
false);
223 $plugin =
new $mainClass($loader, $this->server, $description, $dataFolder, $prefixed, $prefixed .
"/resources/");
224 $this->plugins[$plugin->getDescription()->getName()] = $plugin;
233 private function triagePlugins(
string $path, PluginLoadTriage $triage,
int &$loadErrorCount, ?array $newLoaders =
null) : void{
234 if(is_array($newLoaders)){
236 foreach($newLoaders as $key){
237 if(isset($this->fileAssociations[$key])){
238 $loaders[$key] = $this->fileAssociations[$key];
242 $loaders = $this->fileAssociations;
246 $files = iterator_to_array(
new \FilesystemIterator($path, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS));
248 }elseif(is_file($path)){
249 $realPath = Utils::assumeNotFalse(realpath($path),
"realpath() should not return false on an accessible, existing file");
250 $files = [$realPath];
255 $loadabilityChecker =
new PluginLoadabilityChecker($this->
server->getApiVersion());
256 foreach($loaders as $loader){
257 foreach($files as $file){
258 if(!is_string($file))
throw new AssumptionFailedError(
"FilesystemIterator current should be string when using CURRENT_AS_PATHNAME");
259 if(!$loader->canLoadPlugin($file)){
263 $description = $loader->getPluginDescription($file);
264 }
catch(PluginDescriptionParseException $e){
265 $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
267 KnownTranslationFactory::pocketmine_plugin_invalidManifest($e->getMessage())
271 }
catch(\RuntimeException $e){
272 $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError($file, $e->getMessage())));
273 $this->
server->getLogger()->logException($e);
277 if($description ===
null){
281 $name = $description->getName();
283 if($this->graylist !==
null && !$this->graylist->isAllowed($name)){
284 $this->
server->getLogger()->notice($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
286 $this->graylist->isWhitelist() ? KnownTranslationFactory::pocketmine_plugin_disallowedByWhitelist() : KnownTranslationFactory::pocketmine_plugin_disallowedByBlacklist()
294 if(($loadabilityError = $loadabilityChecker->check($description)) !==
null){
295 $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError($name, $loadabilityError)));
300 if(isset($triage->plugins[$name]) || $this->getPlugin($name) instanceof Plugin){
301 $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_duplicateError($name)));
306 if(str_contains($name,
" ")){
307 $this->
server->getLogger()->warning($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_spacesDiscouraged($name)));
310 $triage->plugins[$name] =
new PluginLoadTriageEntry($file, $loader, $description);
312 $triage->softDependencies[$name] = array_merge($triage->softDependencies[$name] ?? [], $description->getSoftDepend());
313 $triage->dependencies[$name] = $description->getDepend();
315 foreach($description->getLoadBefore() as $before){
316 if(isset($triage->softDependencies[$before])){
317 $triage->softDependencies[$before][] = $name;
319 $triage->softDependencies[$before] = [$name];
333 private function checkDepsForTriage(
string $pluginName,
string $dependencyType, array &$dependencyLists, array $loadedPlugins, PluginLoadTriage $triage) : void{
334 if(isset($dependencyLists[$pluginName])){
335 foreach(Utils::promoteKeys($dependencyLists[$pluginName]) as $key => $dependency){
336 if(isset($loadedPlugins[$dependency]) || $this->getPlugin($dependency) instanceof Plugin){
337 $this->
server->getLogger()->debug(
"Successfully resolved $dependencyType dependency \"$dependency\" for plugin \"$pluginName\"");
338 unset($dependencyLists[$pluginName][$key]);
339 }elseif(array_key_exists($dependency, $triage->plugins)){
340 $this->
server->getLogger()->debug(
"Deferring resolution of $dependencyType dependency \"$dependency\" for plugin \"$pluginName\" (found but not loaded yet)");
344 if(count($dependencyLists[$pluginName]) === 0){
345 unset($dependencyLists[$pluginName]);
353 public function loadPlugins(
string $path,
int &$loadErrorCount = 0) : array{
354 if($this->loadPluginsGuard){
355 throw new \LogicException(__METHOD__ .
"() cannot be called from within itself");
357 $this->loadPluginsGuard =
true;
360 $this->triagePlugins($path, $triage, $loadErrorCount);
364 while(count($triage->plugins) > 0){
366 foreach(Utils::stringifyKeys($triage->plugins) as $name => $entry){
367 $this->checkDepsForTriage($name,
"hard", $triage->dependencies, $loadedPlugins, $triage);
368 $this->checkDepsForTriage($name,
"soft", $triage->softDependencies, $loadedPlugins, $triage);
370 if(!isset($triage->dependencies[$name]) && !isset($triage->softDependencies[$name])){
371 unset($triage->plugins[$name]);
374 $oldRegisteredLoaders = $this->fileAssociations;
375 if(($plugin = $this->internalLoadPlugin($entry->getFile(), $entry->getLoader(), $entry->getDescription())) instanceof
Plugin){
376 $loadedPlugins[$name] = $plugin;
378 foreach($this->fileAssociations as $k => $loader){
379 if(!array_key_exists($k, $oldRegisteredLoaders)){
383 if(count($diffLoaders) !== 0){
384 $this->server->getLogger()->debug(
"Plugin $name registered a new plugin loader during load, scanning for new plugins");
385 $plugins = $triage->plugins;
386 $this->triagePlugins($path, $triage, $loadErrorCount, $diffLoaders);
387 $diffPlugins = array_diff_key($triage->plugins, $plugins);
388 $this->server->getLogger()->debug(
"Re-triage found plugins: " . implode(
", ", array_keys($diffPlugins)));
396 if($loadedThisLoop === 0){
400 foreach(Utils::stringifyKeys($triage->plugins) as $name => $file){
401 if(isset($triage->softDependencies[$name]) && !isset($triage->dependencies[$name])){
402 foreach(Utils::promoteKeys($triage->softDependencies[$name]) as $k => $dependency){
403 if($this->getPlugin($dependency) ===
null && !array_key_exists($dependency, $triage->plugins)){
404 $this->
server->getLogger()->debug(
"Skipping resolution of missing soft dependency \"$dependency\" for plugin \"$name\"");
405 unset($triage->softDependencies[$name][$k]);
408 if(count($triage->softDependencies[$name]) === 0){
409 unset($triage->softDependencies[$name]);
415 foreach(Utils::stringifyKeys($triage->plugins) as $name => $file){
416 if(isset($triage->dependencies[$name])){
417 $unknownDependencies = [];
419 foreach($triage->dependencies[$name] as $dependency){
420 if($this->getPlugin($dependency) ===
null && !array_key_exists($dependency, $triage->plugins)){
424 $unknownDependencies[$dependency] = $dependency;
428 if(count($unknownDependencies) > 0){
429 $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
431 KnownTranslationFactory::pocketmine_plugin_unknownDependency(implode(
", ", $unknownDependencies))
433 unset($triage->plugins[$name]);
439 foreach(Utils::stringifyKeys($triage->plugins) as $name => $file){
440 $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError($name, KnownTranslationFactory::pocketmine_plugin_circularDependency())));
447 $this->loadPluginsGuard =
false;
448 return $loadedPlugins;
451 public function isPluginEnabled(Plugin $plugin) : bool{
452 return isset($this->plugins[$plugin->getDescription()->getName()]) && $plugin->isEnabled();
455 public function enablePlugin(Plugin $plugin) : bool{
456 if(!$plugin->isEnabled()){
457 $this->
server->getLogger()->info($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_enable($plugin->getDescription()->getFullName())));
459 $plugin->getScheduler()->setEnabled(
true);
461 $plugin->onEnableStateChange(true);
462 }
catch(DisablePluginException){
463 $this->disablePlugin($plugin);
466 if($plugin->isEnabled()){
467 $this->enabledPlugins[$plugin->getDescription()->getName()] = $plugin;
469 foreach($plugin->getDescription()->getDepend() as $dependency){
470 $this->pluginDependents[$dependency][$plugin->getDescription()->getName()] = true;
472 foreach($plugin->getDescription()->getSoftDepend() as $dependency){
473 if(isset($this->plugins[$dependency])){
474 $this->pluginDependents[$dependency][$plugin->getDescription()->getName()] = true;
478 (
new PluginEnableEvent($plugin))->call();
482 $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(
483 KnownTranslationFactory::pocketmine_plugin_enableError(
485 KnownTranslationFactory::pocketmine_plugin_suicide()
497 public function disablePlugins() : void{
498 while(count($this->enabledPlugins) > 0){
499 foreach($this->enabledPlugins as $plugin){
500 $name = $plugin->getDescription()->getName();
501 if(isset($this->pluginDependents[$name]) && count($this->pluginDependents[$name]) > 0){
502 $this->
server->getLogger()->debug(
"Deferring disable of plugin $name due to dependent plugins still enabled: " . implode(
", ", array_keys($this->pluginDependents[$name])));
506 $this->disablePlugin($plugin);
511 private function disablePlugin(Plugin $plugin) : void{
512 if($plugin->isEnabled()){
513 $this->
server->getLogger()->info($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_disable($plugin->getDescription()->getFullName())));
514 (
new PluginDisableEvent($plugin))->call();
516 unset($this->enabledPlugins[$plugin->getDescription()->getName()]);
517 foreach(Utils::stringifyKeys($this->pluginDependents) as $dependency => $dependentList){
518 if(isset($this->pluginDependents[$dependency][$plugin->getDescription()->getName()])){
519 if(count($this->pluginDependents[$dependency]) === 1){
520 unset($this->pluginDependents[$dependency]);
522 unset($this->pluginDependents[$dependency][$plugin->getDescription()->getName()]);
527 $plugin->onEnableStateChange(
false);
528 $plugin->getScheduler()->shutdown();
529 HandlerListManager::global()->unregisterAll($plugin);
533 public function tickSchedulers(
int $currentTick) : void{
534 foreach($this->enabledPlugins as $p){
535 $p->getScheduler()->mainThreadHeartbeat($currentTick);
549 private function getEventsHandledBy(\ReflectionMethod $method) : ?string{
550 if($method->isStatic() || !$method->getDeclaringClass()->implementsInterface(Listener::class)){
553 $tags = Utils::parseDocComment((
string) $method->getDocComment());
554 if(isset($tags[ListenerMethodTags::NOT_HANDLER])){
558 $parameters = $method->getParameters();
559 if(count($parameters) !== 1){
563 $paramType = $parameters[0]->getType();
565 if(!$paramType instanceof \ReflectionNamedType || $paramType->isBuiltin()){
570 $paramClass = $paramType->getName();
571 $eventClass = new \ReflectionClass($paramClass);
572 if(!$eventClass->isSubclassOf(Event::class)){
577 return $eventClass->getName();
586 if(!$plugin->isEnabled()){
587 throw new PluginException(
"Plugin attempted to register " . get_class($listener) .
" while not enabled");
590 $reflection = new \ReflectionClass(get_class($listener));
591 foreach($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method){
592 $tags = Utils::parseDocComment((
string) $method->getDocComment());
593 if(isset($tags[ListenerMethodTags::NOT_HANDLER]) || ($eventClass = $this->getEventsHandledBy($method)) ===
null){
596 $handlerClosure = $method->getClosure($listener);
600 $priority = isset($tags[ListenerMethodTags::PRIORITY]) ? EventPriority::fromString($tags[ListenerMethodTags::PRIORITY]) : EventPriority::NORMAL;
601 }
catch(\InvalidArgumentException $e){
602 throw new PluginException(
"Event handler " . Utils::getNiceClosureName($handlerClosure) .
"() declares invalid/unknown priority \"" . $tags[ListenerMethodTags::PRIORITY] .
"\"");
605 $handleCancelled =
false;
606 if(isset($tags[ListenerMethodTags::HANDLE_CANCELLED])){
607 if(!is_a($eventClass, Cancellable::class,
true)){
608 throw new PluginException(sprintf(
609 "Event handler %s() declares @%s for non-cancellable event of type %s",
610 Utils::getNiceClosureName($handlerClosure),
611 ListenerMethodTags::HANDLE_CANCELLED,
615 switch(strtolower($tags[ListenerMethodTags::HANDLE_CANCELLED])){
618 $handleCancelled =
true;
623 throw new PluginException(
"Event handler " . Utils::getNiceClosureName($handlerClosure) .
"() declares invalid @" . ListenerMethodTags::HANDLE_CANCELLED .
" value \"" . $tags[ListenerMethodTags::HANDLE_CANCELLED] .
"\"");
627 $this->registerEvent($eventClass, $handlerClosure, $priority, $plugin, $handleCancelled);
641 if(!is_subclass_of($event,
Event::class)){
645 $handlerName = Utils::getNiceClosureName($handler);
647 $reflect = new \ReflectionFunction($handler);
648 if($reflect->isGenerator()){
649 throw new PluginException(
"Generator function $handlerName cannot be used as an event handler");
652 if(!$plugin->isEnabled()){
653 throw new PluginException(
"Plugin attempted to register event handler " . $handlerName .
"() to event " . $event .
" while not enabled");
656 $timings = Timings::getEventHandlerTimings($event, $handlerName, $plugin->getDescription()->getFullName());
658 $registeredListener =
new RegisteredListener($handler, $priority, $plugin, $handleCancelled, $timings);
659 HandlerListManager::global()->getListFor($event)->register($registeredListener);
660 return $registeredListener;