73 protected array $plugins = [];
76 protected array $enabledPlugins = [];
79 private array $pluginDependents = [];
81 private bool $loadPluginsGuard =
false;
87 protected array $fileAssociations = [];
89 public function __construct(
91 private ?
string $pluginDataDirectory,
94 if($this->pluginDataDirectory !==
null){
95 if(!file_exists($this->pluginDataDirectory)){
96 @mkdir($this->pluginDataDirectory, 0777,
true);
97 }elseif(!is_dir($this->pluginDataDirectory)){
98 throw new \RuntimeException(
"Plugin data path $this->pluginDataDirectory exists and is not a directory");
103 public function getPlugin(
string $name) : ?
Plugin{
104 if(isset($this->plugins[$name])){
105 return $this->plugins[$name];
111 public function registerInterface(
PluginLoader $loader) :
void{
112 $this->fileAssociations[get_class($loader)] = $loader;
119 return $this->plugins;
122 private function getDataDirectory(
string $pluginPath,
string $pluginName) : string{
123 if($this->pluginDataDirectory !== null){
124 return Path::join($this->pluginDataDirectory, $pluginName);
126 return Path::join(dirname($pluginPath), $pluginName);
129 private function internalLoadPlugin(
string $path, PluginLoader $loader, PluginDescription $description) : ?Plugin{
130 $language = $this->server->getLanguage();
131 $this->server->getLogger()->info($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_load($description->getFullName())));
133 $dataFolder = $this->getDataDirectory($path, $description->getName());
134 if(file_exists($dataFolder) && !is_dir($dataFolder)){
135 $this->server->getLogger()->critical($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
136 $description->getName(),
137 KnownTranslationFactory::pocketmine_plugin_badDataFolder($dataFolder)
141 if(!file_exists($dataFolder)){
142 mkdir($dataFolder, 0777,
true);
145 $prefixed = $loader->getAccessProtocol() . $path;
146 $loader->loadPlugin($prefixed);
148 $mainClass = $description->getMain();
149 if(!class_exists($mainClass,
true)){
150 $this->
server->getLogger()->critical($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
151 $description->getName(),
152 KnownTranslationFactory::pocketmine_plugin_mainClassNotFound()
156 if(!is_a($mainClass, Plugin::class,
true)){
157 $this->
server->getLogger()->critical($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
158 $description->getName(),
159 KnownTranslationFactory::pocketmine_plugin_mainClassWrongType(Plugin::class)
163 $reflect = new \ReflectionClass($mainClass);
164 if(!$reflect->isInstantiable()){
165 $this->
server->getLogger()->critical($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
166 $description->getName(),
167 KnownTranslationFactory::pocketmine_plugin_mainClassAbstract()
172 $permManager = PermissionManager::getInstance();
173 foreach($description->getPermissions() as $permsGroup){
174 foreach($permsGroup as $perm){
175 if($permManager->getPermission($perm->getName()) !==
null){
176 $this->
server->getLogger()->critical($language->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
177 $description->getName(),
178 KnownTranslationFactory::pocketmine_plugin_duplicatePermissionError($perm->getName())
184 $opRoot = $permManager->getPermission(DefaultPermissions::ROOT_OPERATOR);
185 $everyoneRoot = $permManager->getPermission(DefaultPermissions::ROOT_USER);
186 foreach(Utils::stringifyKeys($description->getPermissions()) as $default => $perms){
187 foreach($perms as $perm){
188 $permManager->addPermission($perm);
190 case PermissionParser::DEFAULT_TRUE:
191 $everyoneRoot->addChild($perm->getName(),
true);
193 case PermissionParser::DEFAULT_OP:
194 $opRoot->addChild($perm->getName(),
true);
196 case PermissionParser::DEFAULT_NOT_OP:
203 $everyoneRoot->addChild($perm->getName(),
true);
204 $opRoot->addChild($perm->getName(),
false);
216 $plugin =
new $mainClass($loader, $this->server, $description, $dataFolder, $prefixed,
new DiskResourceProvider($prefixed .
"/resources/"));
217 $this->plugins[$plugin->getDescription()->getName()] = $plugin;
226 private function triagePlugins(
string $path, PluginLoadTriage $triage,
int &$loadErrorCount, ?array $newLoaders =
null) : void{
227 if(is_array($newLoaders)){
229 foreach($newLoaders as $key){
230 if(isset($this->fileAssociations[$key])){
231 $loaders[$key] = $this->fileAssociations[$key];
235 $loaders = $this->fileAssociations;
239 $files = iterator_to_array(
new \FilesystemIterator($path, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS));
241 }elseif(is_file($path)){
242 $realPath = Utils::assumeNotFalse(realpath($path),
"realpath() should not return false on an accessible, existing file");
243 $files = [$realPath];
248 $loadabilityChecker =
new PluginLoadabilityChecker($this->
server->getApiVersion());
249 foreach($loaders as $loader){
250 foreach($files as $file){
251 if(!is_string($file))
throw new AssumptionFailedError(
"FilesystemIterator current should be string when using CURRENT_AS_PATHNAME");
252 if(!$loader->canLoadPlugin($file)){
256 $description = $loader->getPluginDescription($file);
257 }
catch(PluginDescriptionParseException $e){
258 $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
260 KnownTranslationFactory::pocketmine_plugin_invalidManifest($e->getMessage())
264 }
catch(\RuntimeException $e){
265 $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError($file, $e->getMessage())));
266 $this->
server->getLogger()->logException($e);
270 if($description ===
null){
274 $name = $description->getName();
276 if($this->graylist !==
null && !$this->graylist->isAllowed($name)){
277 $this->
server->getLogger()->notice($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
279 $this->graylist->isWhitelist() ? KnownTranslationFactory::pocketmine_plugin_disallowedByWhitelist() : KnownTranslationFactory::pocketmine_plugin_disallowedByBlacklist()
287 if(($loadabilityError = $loadabilityChecker->check($description)) !==
null){
288 $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError($name, $loadabilityError)));
293 if(isset($triage->plugins[$name]) || $this->getPlugin($name) instanceof Plugin){
294 $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_duplicateError($name)));
299 if(str_contains($name,
" ")){
300 $this->
server->getLogger()->warning($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_spacesDiscouraged($name)));
303 $triage->plugins[$name] =
new PluginLoadTriageEntry($file, $loader, $description);
305 $triage->softDependencies[$name] = array_merge($triage->softDependencies[$name] ?? [], $description->getSoftDepend());
306 $triage->dependencies[$name] = $description->getDepend();
308 foreach($description->getLoadBefore() as $before){
309 if(isset($triage->softDependencies[$before])){
310 $triage->softDependencies[$before][] = $name;
312 $triage->softDependencies[$before] = [$name];
326 private function checkDepsForTriage(
string $pluginName,
string $dependencyType, array &$dependencyLists, array $loadedPlugins, PluginLoadTriage $triage) : void{
327 if(isset($dependencyLists[$pluginName])){
328 foreach($dependencyLists[$pluginName] as $key => $dependency){
329 if(isset($loadedPlugins[$dependency]) || $this->getPlugin($dependency) instanceof Plugin){
330 $this->
server->getLogger()->debug(
"Successfully resolved $dependencyType dependency \"$dependency\" for plugin \"$pluginName\"");
331 unset($dependencyLists[$pluginName][$key]);
332 }elseif(array_key_exists($dependency, $triage->plugins)){
333 $this->
server->getLogger()->debug(
"Deferring resolution of $dependencyType dependency \"$dependency\" for plugin \"$pluginName\" (found but not loaded yet)");
337 if(count($dependencyLists[$pluginName]) === 0){
338 unset($dependencyLists[$pluginName]);
346 public function loadPlugins(
string $path,
int &$loadErrorCount = 0) : array{
347 if($this->loadPluginsGuard){
348 throw new \LogicException(__METHOD__ .
"() cannot be called from within itself");
350 $this->loadPluginsGuard =
true;
353 $this->triagePlugins($path, $triage, $loadErrorCount);
357 while(count($triage->plugins) > 0){
359 foreach(Utils::stringifyKeys($triage->plugins) as $name => $entry){
360 $this->checkDepsForTriage($name,
"hard", $triage->dependencies, $loadedPlugins, $triage);
361 $this->checkDepsForTriage($name,
"soft", $triage->softDependencies, $loadedPlugins, $triage);
363 if(!isset($triage->dependencies[$name]) && !isset($triage->softDependencies[$name])){
364 unset($triage->plugins[$name]);
367 $oldRegisteredLoaders = $this->fileAssociations;
368 if(($plugin = $this->internalLoadPlugin($entry->getFile(), $entry->getLoader(), $entry->getDescription())) instanceof
Plugin){
369 $loadedPlugins[$name] = $plugin;
371 foreach($this->fileAssociations as $k => $loader){
372 if(!array_key_exists($k, $oldRegisteredLoaders)){
376 if(count($diffLoaders) !== 0){
377 $this->server->getLogger()->debug(
"Plugin $name registered a new plugin loader during load, scanning for new plugins");
378 $plugins = $triage->plugins;
379 $this->triagePlugins($path, $triage, $loadErrorCount, $diffLoaders);
380 $diffPlugins = array_diff_key($triage->plugins, $plugins);
381 $this->server->getLogger()->debug(
"Re-triage found plugins: " . implode(
", ", array_keys($diffPlugins)));
389 if($loadedThisLoop === 0){
393 foreach(Utils::stringifyKeys($triage->plugins) as $name => $file){
394 if(isset($triage->softDependencies[$name]) && !isset($triage->dependencies[$name])){
395 foreach($triage->softDependencies[$name] as $k => $dependency){
396 if($this->getPlugin($dependency) ===
null && !array_key_exists($dependency, $triage->plugins)){
397 $this->
server->getLogger()->debug(
"Skipping resolution of missing soft dependency \"$dependency\" for plugin \"$name\"");
398 unset($triage->softDependencies[$name][$k]);
401 if(count($triage->softDependencies[$name]) === 0){
402 unset($triage->softDependencies[$name]);
408 foreach(Utils::stringifyKeys($triage->plugins) as $name => $file){
409 if(isset($triage->dependencies[$name])){
410 $unknownDependencies = [];
412 foreach($triage->dependencies[$name] as $k => $dependency){
413 if($this->getPlugin($dependency) ===
null && !array_key_exists($dependency, $triage->plugins)){
417 $unknownDependencies[$dependency] = $dependency;
421 if(count($unknownDependencies) > 0){
422 $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError(
424 KnownTranslationFactory::pocketmine_plugin_unknownDependency(implode(
", ", $unknownDependencies))
426 unset($triage->plugins[$name]);
432 foreach(Utils::stringifyKeys($triage->plugins) as $name => $file){
433 $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_loadError($name, KnownTranslationFactory::pocketmine_plugin_circularDependency())));
440 $this->loadPluginsGuard =
false;
441 return $loadedPlugins;
444 public function isPluginEnabled(Plugin $plugin) : bool{
445 return isset($this->plugins[$plugin->getDescription()->getName()]) && $plugin->isEnabled();
448 public function enablePlugin(Plugin $plugin) : bool{
449 if(!$plugin->isEnabled()){
450 $this->
server->getLogger()->info($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_enable($plugin->getDescription()->getFullName())));
452 $plugin->getScheduler()->setEnabled(
true);
454 $plugin->onEnableStateChange(true);
455 }
catch(DisablePluginException){
456 $this->disablePlugin($plugin);
459 if($plugin->isEnabled()){
460 $this->enabledPlugins[$plugin->getDescription()->getName()] = $plugin;
462 foreach($plugin->getDescription()->getDepend() as $dependency){
463 $this->pluginDependents[$dependency][$plugin->getDescription()->getName()] = true;
465 foreach($plugin->getDescription()->getSoftDepend() as $dependency){
466 if(isset($this->plugins[$dependency])){
467 $this->pluginDependents[$dependency][$plugin->getDescription()->getName()] = true;
471 (
new PluginEnableEvent($plugin))->call();
475 $this->
server->getLogger()->critical($this->
server->getLanguage()->translate(
476 KnownTranslationFactory::pocketmine_plugin_enableError(
478 KnownTranslationFactory::pocketmine_plugin_suicide()
489 public function disablePlugins() : void{
490 while(count($this->enabledPlugins) > 0){
491 foreach($this->enabledPlugins as $plugin){
492 if(!$plugin->isEnabled()){
495 $name = $plugin->getDescription()->getName();
496 if(isset($this->pluginDependents[$name]) && count($this->pluginDependents[$name]) > 0){
497 $this->
server->getLogger()->debug(
"Deferring disable of plugin $name due to dependent plugins still enabled: " . implode(
", ", array_keys($this->pluginDependents[$name])));
501 $this->disablePlugin($plugin);
506 public function disablePlugin(Plugin $plugin) : void{
507 if($plugin->isEnabled()){
508 $this->
server->getLogger()->info($this->
server->getLanguage()->translate(KnownTranslationFactory::pocketmine_plugin_disable($plugin->getDescription()->getFullName())));
509 (
new PluginDisableEvent($plugin))->call();
511 unset($this->enabledPlugins[$plugin->getDescription()->getName()]);
512 foreach(Utils::stringifyKeys($this->pluginDependents) as $dependency => $dependentList){
513 if(isset($this->pluginDependents[$dependency][$plugin->getDescription()->getName()])){
514 if(count($this->pluginDependents[$dependency]) === 1){
515 unset($this->pluginDependents[$dependency]);
517 unset($this->pluginDependents[$dependency][$plugin->getDescription()->getName()]);
522 $plugin->onEnableStateChange(
false);
523 $plugin->getScheduler()->shutdown();
524 HandlerListManager::global()->unregisterAll($plugin);
528 public function tickSchedulers(
int $currentTick) : void{
529 foreach($this->enabledPlugins as $pluginName => $p){
530 if(isset($this->enabledPlugins[$pluginName])){
533 $p->getScheduler()->mainThreadHeartbeat($currentTick);
538 public function clearPlugins() : void{
539 $this->disablePlugins();
541 $this->enabledPlugins = [];
542 $this->fileAssociations = [];
555 private function getEventsHandledBy(\ReflectionMethod $method) : ?string{
556 if($method->isStatic() || !$method->getDeclaringClass()->implementsInterface(Listener::class)){
559 $tags = Utils::parseDocComment((
string) $method->getDocComment());
560 if(isset($tags[ListenerMethodTags::NOT_HANDLER])){
564 $parameters = $method->getParameters();
565 if(count($parameters) !== 1){
569 $paramType = $parameters[0]->getType();
571 if(!$paramType instanceof \ReflectionNamedType || $paramType->isBuiltin()){
576 $paramClass = $paramType->getName();
577 $eventClass = new \ReflectionClass($paramClass);
578 if(!$eventClass->isSubclassOf(Event::class)){
583 return $eventClass->getName();
592 if(!$plugin->isEnabled()){
593 throw new PluginException(
"Plugin attempted to register " . get_class($listener) .
" while not enabled");
596 $reflection = new \ReflectionClass(get_class($listener));
597 foreach($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method){
598 $tags = Utils::parseDocComment((
string) $method->getDocComment());
599 if(isset($tags[ListenerMethodTags::NOT_HANDLER]) || ($eventClass = $this->getEventsHandledBy($method)) ===
null){
602 $handlerClosure = $method->getClosure($listener);
606 $priority = isset($tags[ListenerMethodTags::PRIORITY]) ? EventPriority::fromString($tags[ListenerMethodTags::PRIORITY]) : EventPriority::NORMAL;
607 }
catch(\InvalidArgumentException $e){
608 throw new PluginException(
"Event handler " . Utils::getNiceClosureName($handlerClosure) .
"() declares invalid/unknown priority \"" . $tags[ListenerMethodTags::PRIORITY] .
"\"");
611 $handleCancelled =
false;
612 if(isset($tags[ListenerMethodTags::HANDLE_CANCELLED])){
613 if(!is_a($eventClass, Cancellable::class,
true)){
614 throw new PluginException(sprintf(
615 "Event handler %s() declares @%s for non-cancellable event of type %s",
616 Utils::getNiceClosureName($handlerClosure),
617 ListenerMethodTags::HANDLE_CANCELLED,
621 switch(strtolower($tags[ListenerMethodTags::HANDLE_CANCELLED])){
624 $handleCancelled =
true;
629 throw new PluginException(
"Event handler " . Utils::getNiceClosureName($handlerClosure) .
"() declares invalid @" . ListenerMethodTags::HANDLE_CANCELLED .
" value \"" . $tags[ListenerMethodTags::HANDLE_CANCELLED] .
"\"");
633 $this->registerEvent($eventClass, $handlerClosure, $priority, $plugin, $handleCancelled);
647 if(!is_subclass_of($event,
Event::class)){
651 $handlerName = Utils::getNiceClosureName($handler);
653 $reflect = new \ReflectionFunction($handler);
654 if($reflect->isGenerator()){
655 throw new PluginException(
"Generator function $handlerName cannot be used as an event handler");
658 if(!$plugin->isEnabled()){
659 throw new PluginException(
"Plugin attempted to register event handler " . $handlerName .
"() to event " . $event .
" while not enabled");
662 $timings = Timings::getEventHandlerTimings($event, $handlerName, $plugin->getDescription()->getFullName());
664 $registeredListener =
new RegisteredListener($handler, $priority, $plugin, $handleCancelled, $timings);
665 HandlerListManager::global()->getListFor($event)->register($registeredListener);
666 return $registeredListener;