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,
new DiskResourceProvider($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($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($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 $k => $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()
496 public function disablePlugins() : void{
497 while(count($this->enabledPlugins) > 0){
498 foreach($this->enabledPlugins as $plugin){
499 if(!$plugin->isEnabled()){
502 $name = $plugin->getDescription()->getName();
503 if(isset($this->pluginDependents[$name]) && count($this->pluginDependents[$name]) > 0){
504 $this->
server->getLogger()->debug(
"Deferring disable of plugin $name due to dependent plugins still enabled: " . implode(
", ", array_keys($this->pluginDependents[$name])));
508 $this->disablePlugin($plugin);
513 public function disablePlugin(Plugin $plugin) : void{
514 if($plugin->isEnabled()){
515 $this->
server->getLogger()->info($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_disable($plugin->getDescription()->getFullName())));
516 (
new PluginDisableEvent($plugin))->call();
518 unset($this->enabledPlugins[$plugin->getDescription()->getName()]);
519 foreach(Utils::stringifyKeys($this->pluginDependents) as $dependency => $dependentList){
520 if(isset($this->pluginDependents[$dependency][$plugin->getDescription()->getName()])){
521 if(count($this->pluginDependents[$dependency]) === 1){
522 unset($this->pluginDependents[$dependency]);
524 unset($this->pluginDependents[$dependency][$plugin->getDescription()->getName()]);
529 $plugin->onEnableStateChange(
false);
530 $plugin->getScheduler()->shutdown();
531 HandlerListManager::global()->unregisterAll($plugin);
535 public function tickSchedulers(
int $currentTick) : void{
536 foreach(Utils::promoteKeys($this->enabledPlugins) as $pluginName => $p){
537 if(isset($this->enabledPlugins[$pluginName])){
540 $p->getScheduler()->mainThreadHeartbeat($currentTick);
545 public function clearPlugins() : void{
546 $this->disablePlugins();
548 $this->enabledPlugins = [];
549 $this->fileAssociations = [];
562 private function getEventsHandledBy(\ReflectionMethod $method) : ?string{
563 if($method->isStatic() || !$method->getDeclaringClass()->implementsInterface(Listener::class)){
566 $tags = Utils::parseDocComment((
string) $method->getDocComment());
567 if(isset($tags[ListenerMethodTags::NOT_HANDLER])){
571 $parameters = $method->getParameters();
572 if(count($parameters) !== 1){
576 $paramType = $parameters[0]->getType();
578 if(!$paramType instanceof \ReflectionNamedType || $paramType->isBuiltin()){
583 $paramClass = $paramType->getName();
584 $eventClass = new \ReflectionClass($paramClass);
585 if(!$eventClass->isSubclassOf(Event::class)){
590 return $eventClass->getName();
599 if(!$plugin->isEnabled()){
600 throw new PluginException(
"Plugin attempted to register " . get_class($listener) .
" while not enabled");
603 $reflection = new \ReflectionClass(get_class($listener));
604 foreach($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method){
605 $tags = Utils::parseDocComment((
string) $method->getDocComment());
606 if(isset($tags[ListenerMethodTags::NOT_HANDLER]) || ($eventClass = $this->getEventsHandledBy($method)) ===
null){
609 $handlerClosure = $method->getClosure($listener);
613 $priority = isset($tags[ListenerMethodTags::PRIORITY]) ? EventPriority::fromString($tags[ListenerMethodTags::PRIORITY]) : EventPriority::NORMAL;
614 }
catch(\InvalidArgumentException $e){
615 throw new PluginException(
"Event handler " . Utils::getNiceClosureName($handlerClosure) .
"() declares invalid/unknown priority \"" . $tags[ListenerMethodTags::PRIORITY] .
"\"");
618 $handleCancelled =
false;
619 if(isset($tags[ListenerMethodTags::HANDLE_CANCELLED])){
620 if(!is_a($eventClass, Cancellable::class,
true)){
621 throw new PluginException(sprintf(
622 "Event handler %s() declares @%s for non-cancellable event of type %s",
623 Utils::getNiceClosureName($handlerClosure),
624 ListenerMethodTags::HANDLE_CANCELLED,
628 switch(strtolower($tags[ListenerMethodTags::HANDLE_CANCELLED])){
631 $handleCancelled =
true;
636 throw new PluginException(
"Event handler " . Utils::getNiceClosureName($handlerClosure) .
"() declares invalid @" . ListenerMethodTags::HANDLE_CANCELLED .
" value \"" . $tags[ListenerMethodTags::HANDLE_CANCELLED] .
"\"");
640 $this->registerEvent($eventClass, $handlerClosure, $priority, $plugin, $handleCancelled);
654 if(!is_subclass_of($event,
Event::class)){
658 $handlerName = Utils::getNiceClosureName($handler);
660 $reflect = new \ReflectionFunction($handler);
661 if($reflect->isGenerator()){
662 throw new PluginException(
"Generator function $handlerName cannot be used as an event handler");
665 if(!$plugin->isEnabled()){
666 throw new PluginException(
"Plugin attempted to register event handler " . $handlerName .
"() to event " . $event .
" while not enabled");
669 $timings = Timings::getEventHandlerTimings($event, $handlerName, $plugin->getDescription()->getFullName());
671 $registeredListener =
new RegisteredListener($handler, $priority, $plugin, $handleCancelled, $timings);
672 HandlerListManager::global()->getListFor($event)->register($registeredListener);
673 return $registeredListener;