Paper Plugins

Co-authored-by: Micah Rao <micah.s.rao@gmail.com>
This commit is contained in:
Owen1212055
2022-07-06 23:00:31 -04:00
parent 329dfdabdc
commit 216388dfdf
103 changed files with 7450 additions and 42 deletions

View File

@@ -0,0 +1,82 @@
package io.papermc.paper.plugin.manager;
import io.papermc.paper.plugin.configuration.PluginMeta;
import io.papermc.paper.plugin.provider.type.PluginFileType;
import org.bukkit.Bukkit;
import org.bukkit.event.Event;
import org.bukkit.event.Listener;
import org.bukkit.plugin.InvalidDescriptionException;
import org.bukkit.plugin.InvalidPluginException;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginLoader;
import org.bukkit.plugin.RegisteredListener;
import org.bukkit.plugin.UnknownDependencyException;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
/**
* A purely internal type that implements the now deprecated {@link PluginLoader} after the implementation
* of papers new plugin system.
*/
@ApiStatus.Internal
public class DummyBukkitPluginLoader implements PluginLoader {
private static final Pattern[] PATTERNS = new Pattern[0];
@Override
public @NotNull Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, UnknownDependencyException {
try {
return PaperPluginManagerImpl.getInstance().loadPlugin(file);
} catch (InvalidDescriptionException e) {
throw new InvalidPluginException(e);
}
}
@Override
public @NotNull PluginDescriptionFile getPluginDescription(@NotNull File file) throws InvalidDescriptionException {
try (JarFile jar = new JarFile(file)) {
PluginFileType<?, ?> type = PluginFileType.guessType(jar);
if (type == null) {
throw new InvalidDescriptionException(new FileNotFoundException("Jar does not contain plugin.yml"));
}
PluginMeta meta = type.getConfig(jar);
if (meta instanceof PluginDescriptionFile pluginDescriptionFile) {
return pluginDescriptionFile;
} else {
throw new InvalidDescriptionException("Plugin type does not use plugin.yml. Cannot read file description.");
}
} catch (Exception e) {
throw new InvalidDescriptionException(e);
}
}
@Override
public @NotNull Pattern[] getPluginFileFilters() {
return PATTERNS;
}
@Override
public @NotNull Map<Class<? extends Event>, Set<RegisteredListener>> createRegisteredListeners(@NotNull Listener listener, @NotNull Plugin plugin) {
return PaperPluginManagerImpl.getInstance().paperEventManager.createRegisteredListeners(listener, plugin);
}
@Override
public void enablePlugin(@NotNull Plugin plugin) {
Bukkit.getPluginManager().enablePlugin(plugin);
}
@Override
public void disablePlugin(@NotNull Plugin plugin) {
Bukkit.getPluginManager().disablePlugin(plugin);
}
}

View File

@@ -0,0 +1,61 @@
package io.papermc.paper.plugin.manager;
import com.mojang.logging.LogUtils;
import io.papermc.paper.plugin.entrypoint.Entrypoint;
import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
import io.papermc.paper.plugin.provider.PluginProvider;
import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
import io.papermc.paper.plugin.storage.ServerPluginProviderStorage;
import org.bukkit.plugin.java.JavaPlugin;
import org.slf4j.Logger;
import java.util.ArrayList;
import java.util.List;
public class MultiRuntimePluginProviderStorage extends ServerPluginProviderStorage {
private static final Logger LOGGER = LogUtils.getClassLogger();
private final List<JavaPlugin> provided = new ArrayList<>();
private final MetaDependencyTree dependencyTree;
MultiRuntimePluginProviderStorage(MetaDependencyTree dependencyTree) {
this.dependencyTree = dependencyTree;
}
@Override
public void register(PluginProvider<JavaPlugin> provider) {
if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
LOGGER.warn("Skipping loading of paper plugin requested from SimplePluginManager.");
return;
}
super.register(provider);
/*
Register the provider into the server entrypoint, this allows it to show in /plugins correctly. Generally it might be better in the future to make a separate storage,
as putting it into the entrypoint handlers doesn't make much sense.
*/
LaunchEntryPointHandler.INSTANCE.register(Entrypoint.PLUGIN, provider);
}
@Override
public void processProvided(PluginProvider<JavaPlugin> provider, JavaPlugin provided) {
super.processProvided(provider, provided);
this.provided.add(provided);
}
@Override
public boolean throwOnCycle() {
return false;
}
public List<JavaPlugin> getLoaded() {
return this.provided;
}
@Override
public MetaDependencyTree createDependencyTree() {
return this.dependencyTree;
}
}

View File

@@ -0,0 +1,43 @@
package io.papermc.paper.plugin.manager;
import org.bukkit.permissions.Permissible;
import org.bukkit.permissions.Permission;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
class NormalPaperPermissionManager extends PaperPermissionManager {
private final Map<String, Permission> permissions = new HashMap<>();
private final Map<Boolean, Set<Permission>> defaultPerms = new LinkedHashMap<>();
private final Map<String, Map<Permissible, Boolean>> permSubs = new HashMap<>();
private final Map<Boolean, Map<Permissible, Boolean>> defSubs = new HashMap<>();
public NormalPaperPermissionManager() {
this.defaultPerms().put(true, new LinkedHashSet<>());
this.defaultPerms().put(false, new LinkedHashSet<>());
}
@Override
public Map<String, Permission> permissions() {
return this.permissions;
}
@Override
public Map<Boolean, Set<Permission>> defaultPerms() {
return this.defaultPerms;
}
@Override
public Map<String, Map<Permissible, Boolean>> permSubs() {
return this.permSubs;
}
@Override
public Map<Boolean, Map<Permissible, Boolean>> defSubs() {
return this.defSubs;
}
}

View File

@@ -0,0 +1,194 @@
package io.papermc.paper.plugin.manager;
import co.aikar.timings.TimedEventExecutor;
import com.destroystokyo.paper.event.server.ServerExceptionEvent;
import com.destroystokyo.paper.exception.ServerEventException;
import com.google.common.collect.Sets;
import org.bukkit.Server;
import org.bukkit.Warning;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.plugin.AuthorNagException;
import org.bukkit.plugin.EventExecutor;
import org.bukkit.plugin.IllegalPluginAccessException;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.RegisteredListener;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
class PaperEventManager {
private final Server server;
public PaperEventManager(Server server) {
this.server = server;
}
// SimplePluginManager
public void callEvent(@NotNull Event event) {
if (event.isAsynchronous() && this.server.isPrimaryThread()) {
throw new IllegalStateException(event.getEventName() + " may only be triggered asynchronously.");
} else if (!event.isAsynchronous() && !this.server.isPrimaryThread() && !this.server.isStopping()) {
throw new IllegalStateException(event.getEventName() + " may only be triggered synchronously.");
}
HandlerList handlers = event.getHandlers();
RegisteredListener[] listeners = handlers.getRegisteredListeners();
for (RegisteredListener registration : listeners) {
if (!registration.getPlugin().isEnabled()) {
continue;
}
try {
registration.callEvent(event);
} catch (AuthorNagException ex) {
Plugin plugin = registration.getPlugin();
if (plugin.isNaggable()) {
plugin.setNaggable(false);
this.server.getLogger().log(Level.SEVERE, String.format(
"Nag author(s): '%s' of '%s' about the following: %s",
plugin.getPluginMeta().getAuthors(),
plugin.getPluginMeta().getDisplayName(),
ex.getMessage()
));
}
} catch (Throwable ex) {
String msg = "Could not pass event " + event.getEventName() + " to " + registration.getPlugin().getPluginMeta().getDisplayName();
this.server.getLogger().log(Level.SEVERE, msg, ex);
if (!(event instanceof ServerExceptionEvent)) { // We don't want to cause an endless event loop
this.callEvent(new ServerExceptionEvent(new ServerEventException(msg, ex, registration.getPlugin(), registration.getListener(), event)));
}
}
}
}
public void registerEvents(@NotNull Listener listener, @NotNull Plugin plugin) {
if (!plugin.isEnabled()) {
throw new IllegalPluginAccessException("Plugin attempted to register " + listener + " while not enabled");
}
for (Map.Entry<Class<? extends Event>, Set<RegisteredListener>> entry : this.createRegisteredListeners(listener, plugin).entrySet()) {
this.getEventListeners(this.getRegistrationClass(entry.getKey())).registerAll(entry.getValue());
}
}
public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin) {
this.registerEvent(event, listener, priority, executor, plugin, false);
}
public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin, boolean ignoreCancelled) {
if (!plugin.isEnabled()) {
throw new IllegalPluginAccessException("Plugin attempted to register " + event + " while not enabled");
}
executor = new TimedEventExecutor(executor, plugin, null, event);
this.getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled));
}
@NotNull
private HandlerList getEventListeners(@NotNull Class<? extends Event> type) {
try {
Method method = this.getRegistrationClass(type).getDeclaredMethod("getHandlerList");
method.setAccessible(true);
return (HandlerList) method.invoke(null);
} catch (Exception e) {
throw new IllegalPluginAccessException(e.toString());
}
}
@NotNull
private Class<? extends Event> getRegistrationClass(@NotNull Class<? extends Event> clazz) {
try {
clazz.getDeclaredMethod("getHandlerList");
return clazz;
} catch (NoSuchMethodException e) {
if (clazz.getSuperclass() != null
&& !clazz.getSuperclass().equals(Event.class)
&& Event.class.isAssignableFrom(clazz.getSuperclass())) {
return this.getRegistrationClass(clazz.getSuperclass().asSubclass(Event.class));
} else {
throw new IllegalPluginAccessException("Unable to find handler list for event " + clazz.getName() + ". Static getHandlerList method required!");
}
}
}
// JavaPluginLoader
@NotNull
public Map<Class<? extends Event>, Set<RegisteredListener>> createRegisteredListeners(@NotNull Listener listener, @NotNull final Plugin plugin) {
Map<Class<? extends Event>, Set<RegisteredListener>> ret = new HashMap<>();
Set<Method> methods;
try {
Class<?> listenerClazz = listener.getClass();
methods = Sets.union(
Set.of(listenerClazz.getMethods()),
Set.of(listenerClazz.getDeclaredMethods())
);
} catch (NoClassDefFoundError e) {
plugin.getLogger().severe("Failed to register events for " + listener.getClass() + " because " + e.getMessage() + " does not exist.");
return ret;
}
for (final Method method : methods) {
final EventHandler eh = method.getAnnotation(EventHandler.class);
if (eh == null) continue;
// Do not register bridge or synthetic methods to avoid event duplication
// Fixes SPIGOT-893
if (method.isBridge() || method.isSynthetic()) {
continue;
}
final Class<?> checkClass;
if (method.getParameterTypes().length != 1 || !Event.class.isAssignableFrom(checkClass = method.getParameterTypes()[0])) {
plugin.getLogger().severe(plugin.getPluginMeta().getDisplayName() + " attempted to register an invalid EventHandler method signature \"" + method.toGenericString() + "\" in " + listener.getClass());
continue;
}
final Class<? extends Event> eventClass = checkClass.asSubclass(Event.class);
method.setAccessible(true);
Set<RegisteredListener> eventSet = ret.computeIfAbsent(eventClass, k -> new HashSet<>());
for (Class<?> clazz = eventClass; Event.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) {
// This loop checks for extending deprecated events
if (clazz.getAnnotation(Deprecated.class) != null) {
Warning warning = clazz.getAnnotation(Warning.class);
Warning.WarningState warningState = this.server.getWarningState();
if (!warningState.printFor(warning)) {
break;
}
plugin.getLogger().log(
Level.WARNING,
String.format(
"\"%s\" has registered a listener for %s on method \"%s\", but the event is Deprecated. \"%s\"; please notify the authors %s.",
plugin.getPluginMeta().getDisplayName(),
clazz.getName(),
method.toGenericString(),
(warning != null && warning.reason().length() != 0) ? warning.reason() : "Server performance will be affected",
Arrays.toString(plugin.getPluginMeta().getAuthors().toArray())),
warningState == Warning.WarningState.ON ? new AuthorNagException(null) : null);
break;
}
}
EventExecutor executor = new TimedEventExecutor(EventExecutor.create(method, eventClass), plugin, method, eventClass);
eventSet.add(new RegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled()));
}
return ret;
}
public void clearEvents() {
HandlerList.unregisterAll();
}
}

View File

@@ -0,0 +1,201 @@
package io.papermc.paper.plugin.manager;
import com.google.common.collect.ImmutableSet;
import io.papermc.paper.plugin.PermissionManager;
import org.bukkit.permissions.Permissible;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
/**
* See
* {@link StupidSPMPermissionManagerWrapper}
*/
abstract class PaperPermissionManager implements PermissionManager {
public abstract Map<String, Permission> permissions();
public abstract Map<Boolean, Set<Permission>> defaultPerms();
public abstract Map<String, Map<Permissible, Boolean>> permSubs();
public abstract Map<Boolean, Map<Permissible, Boolean>> defSubs();
@Override
@Nullable
public Permission getPermission(@NotNull String name) {
return this.permissions().get(name.toLowerCase(java.util.Locale.ENGLISH));
}
@Override
public void addPermission(@NotNull Permission perm) {
this.addPermission(perm, true);
}
@Override
public void addPermissions(@NotNull List<Permission> permissions) {
for (Permission permission : permissions) {
this.addPermission(permission, false);
}
this.dirtyPermissibles();
}
// Allow suppressing permission default calculations
private void addPermission(@NotNull Permission perm, boolean dirty) {
String name = perm.getName().toLowerCase(java.util.Locale.ENGLISH);
if (this.permissions().containsKey(name)) {
throw new IllegalArgumentException("The permission " + name + " is already defined!");
}
this.permissions().put(name, perm);
this.calculatePermissionDefault(perm, dirty);
}
@Override
@NotNull
public Set<Permission> getDefaultPermissions(boolean op) {
return ImmutableSet.copyOf(this.defaultPerms().get(op));
}
@Override
public void removePermission(@NotNull Permission perm) {
this.removePermission(perm.getName());
}
@Override
public void removePermission(@NotNull String name) {
this.permissions().remove(name.toLowerCase(java.util.Locale.ENGLISH));
}
@Override
public void recalculatePermissionDefaults(@NotNull Permission perm) {
// we need a null check here because some plugins for some unknown reason pass null into this?
if (perm != null && this.permissions().containsKey(perm.getName().toLowerCase(Locale.ROOT))) {
this.defaultPerms().get(true).remove(perm);
this.defaultPerms().get(false).remove(perm);
this.calculatePermissionDefault(perm, true);
}
}
private void calculatePermissionDefault(@NotNull Permission perm, boolean dirty) {
if ((perm.getDefault() == PermissionDefault.OP) || (perm.getDefault() == PermissionDefault.TRUE)) {
this.defaultPerms().get(true).add(perm);
if (dirty) {
this.dirtyPermissibles(true);
}
}
if ((perm.getDefault() == PermissionDefault.NOT_OP) || (perm.getDefault() == PermissionDefault.TRUE)) {
this.defaultPerms().get(false).add(perm);
if (dirty) {
this.dirtyPermissibles(false);
}
}
}
@Override
public void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible) {
String name = permission.toLowerCase(java.util.Locale.ENGLISH);
Map<Permissible, Boolean> map = this.permSubs().computeIfAbsent(name, k -> new WeakHashMap<>());
map.put(permissible, true);
}
@Override
public void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible) {
String name = permission.toLowerCase(java.util.Locale.ENGLISH);
Map<Permissible, Boolean> map = this.permSubs().get(name);
if (map != null) {
map.remove(permissible);
if (map.isEmpty()) {
this.permSubs().remove(name);
}
}
}
@Override
@NotNull
public Set<Permissible> getPermissionSubscriptions(@NotNull String permission) {
String name = permission.toLowerCase(java.util.Locale.ENGLISH);
Map<Permissible, Boolean> map = this.permSubs().get(name);
if (map == null) {
return ImmutableSet.of();
} else {
return ImmutableSet.copyOf(map.keySet());
}
}
@Override
public void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible) {
Map<Permissible, Boolean> map = this.defSubs().computeIfAbsent(op, k -> new WeakHashMap<>());
map.put(permissible, true);
}
@Override
public void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible) {
Map<Permissible, Boolean> map = this.defSubs().get(op);
if (map != null) {
map.remove(permissible);
if (map.isEmpty()) {
this.defSubs().remove(op);
}
}
}
@Override
@NotNull
public Set<Permissible> getDefaultPermSubscriptions(boolean op) {
Map<Permissible, Boolean> map = this.defSubs().get(op);
if (map == null) {
return ImmutableSet.of();
} else {
return ImmutableSet.copyOf(map.keySet());
}
}
@Override
@NotNull
public Set<Permission> getPermissions() {
return new HashSet<>(this.permissions().values());
}
@Override
public void clearPermissions() {
this.permissions().clear();
this.defaultPerms().get(true).clear();
this.defaultPerms().get(false).clear();
}
void dirtyPermissibles(boolean op) {
Set<Permissible> permissibles = this.getDefaultPermSubscriptions(op);
for (Permissible p : permissibles) {
p.recalculatePermissions();
}
}
void dirtyPermissibles() {
this.dirtyPermissibles(true);
this.dirtyPermissibles(false);
}
}

View File

@@ -0,0 +1,317 @@
package io.papermc.paper.plugin.manager;
import com.google.common.base.Preconditions;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.MutableGraph;
import io.papermc.paper.plugin.configuration.PluginMeta;
import io.papermc.paper.plugin.entrypoint.Entrypoint;
import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
import io.papermc.paper.plugin.entrypoint.dependency.SimpleMetaDependencyTree;
import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException;
import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage;
import io.papermc.paper.plugin.provider.source.DirectoryProviderSource;
import io.papermc.paper.plugin.provider.source.FileArrayProviderSource;
import io.papermc.paper.plugin.provider.source.FileProviderSource;
import java.io.File;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.command.Command;
import org.bukkit.command.CommandMap;
import org.bukkit.command.PluginCommandYamlParser;
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
import org.bukkit.event.HandlerList;
import org.bukkit.event.server.PluginDisableEvent;
import org.bukkit.event.server.PluginEnableEvent;
import org.bukkit.plugin.InvalidPluginException;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.UnknownDependencyException;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
@SuppressWarnings("UnstableApiUsage")
class PaperPluginInstanceManager {
private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("File '%s'"::formatted);
private final List<Plugin> plugins = new ArrayList<>();
private final Map<String, Plugin> lookupNames = new HashMap<>();
private final PluginManager pluginManager;
private final CommandMap commandMap;
private final Server server;
private final MetaDependencyTree dependencyTree = new SimpleMetaDependencyTree(GraphBuilder.directed().build());
public PaperPluginInstanceManager(PluginManager pluginManager, CommandMap commandMap, Server server) {
this.commandMap = commandMap;
this.server = server;
this.pluginManager = pluginManager;
}
public @Nullable Plugin getPlugin(@NotNull String name) {
return this.lookupNames.get(name.replace(' ', '_').toLowerCase(java.util.Locale.ENGLISH)); // Paper
}
public @NotNull Plugin[] getPlugins() {
return this.plugins.toArray(new Plugin[0]);
}
public boolean isPluginEnabled(@NotNull String name) {
Plugin plugin = this.getPlugin(name);
return this.isPluginEnabled(plugin);
}
public synchronized boolean isPluginEnabled(@Nullable Plugin plugin) {
if ((plugin != null) && (this.plugins.contains(plugin))) {
return plugin.isEnabled();
} else {
return false;
}
}
public void loadPlugin(Plugin provided) {
PluginMeta configuration = provided.getPluginMeta();
this.plugins.add(provided);
this.lookupNames.put(configuration.getName().toLowerCase(java.util.Locale.ENGLISH), provided);
for (String providedPlugin : configuration.getProvidedPlugins()) {
this.lookupNames.putIfAbsent(providedPlugin.toLowerCase(java.util.Locale.ENGLISH), provided);
}
this.dependencyTree.add(configuration);
}
// InvalidDescriptionException is never used, because the old JavaPluginLoader would wrap the exception.
public @Nullable Plugin loadPlugin(@NotNull Path path) throws InvalidPluginException, UnknownDependencyException {
RuntimePluginEntrypointHandler<SingularRuntimePluginProviderStorage> runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new SingularRuntimePluginProviderStorage(this.dependencyTree));
try {
path = FILE_PROVIDER_SOURCE.prepareContext(path);
FILE_PROVIDER_SOURCE.registerProviders(runtimePluginEntrypointHandler, path);
} catch (IllegalArgumentException exception) {
return null; // Return null when the plugin file is not valid / plugin type is unknown
} catch (PluginGraphCycleException exception) {
throw new InvalidPluginException("Cannot import plugin that causes cyclic dependencies!");
} catch (Exception e) {
throw new InvalidPluginException(e);
}
try {
runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN);
} catch (Throwable e) {
throw new InvalidPluginException(e);
}
return runtimePluginEntrypointHandler.getPluginProviderStorage().getSingleLoaded()
.orElseThrow(() -> new InvalidPluginException("Plugin didn't load any plugin providers?"));
}
public @NotNull Plugin[] loadPlugins(@NotNull File[] files) {
RuntimePluginEntrypointHandler<MultiRuntimePluginProviderStorage> runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new MultiRuntimePluginProviderStorage(this.dependencyTree));
try {
List<Path> paths = FileArrayProviderSource.INSTANCE.prepareContext(files);
DirectoryProviderSource.INSTANCE.registerProviders(runtimePluginEntrypointHandler, paths);
runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN);
} catch (Exception e) {
// This should never happen, any errors that occur in this provider should instead be logged.
this.server.getLogger().log(Level.SEVERE, "Unknown error occurred while loading plugins through PluginManager.", e);
}
return runtimePluginEntrypointHandler.getPluginProviderStorage().getLoaded().toArray(new JavaPlugin[0]);
}
// The behavior of this is that all errors are logged instead of being thrown
public @NotNull Plugin[] loadPlugins(@NotNull Path directory) {
Preconditions.checkArgument(Files.isDirectory(directory), "Directory must be a directory"); // Avoid creating a directory if it doesn't exist
RuntimePluginEntrypointHandler<MultiRuntimePluginProviderStorage> runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new MultiRuntimePluginProviderStorage(this.dependencyTree));
try {
List<Path> files = DirectoryProviderSource.INSTANCE.prepareContext(directory);
DirectoryProviderSource.INSTANCE.registerProviders(runtimePluginEntrypointHandler, files);
runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN);
} catch (Exception e) {
// This should never happen, any errors that occur in this provider should instead be logged.
this.server.getLogger().log(Level.SEVERE, "Unknown error occurred while loading plugins through PluginManager.", e);
}
return runtimePluginEntrypointHandler.getPluginProviderStorage().getLoaded().toArray(new JavaPlugin[0]);
}
// Plugins are disabled in order like this inorder to "rougly" prevent
// their dependencies unloading first. But, eh.
public void disablePlugins() {
Plugin[] plugins = this.getPlugins();
for (int i = plugins.length - 1; i >= 0; i--) {
this.disablePlugin(plugins[i]);
}
}
public void clearPlugins() {
synchronized (this) {
this.disablePlugins();
this.plugins.clear();
this.lookupNames.clear();
}
}
public synchronized void enablePlugin(@NotNull Plugin plugin) {
if (plugin.isEnabled()) {
return;
}
if (plugin.getPluginMeta() instanceof PluginDescriptionFile) {
List<Command> bukkitCommands = PluginCommandYamlParser.parse(plugin);
if (!bukkitCommands.isEmpty()) {
this.commandMap.registerAll(plugin.getPluginMeta().getName(), bukkitCommands);
}
}
try {
String enableMsg = "Enabling " + plugin.getPluginMeta().getDisplayName();
if (plugin.getPluginMeta() instanceof PluginDescriptionFile descriptionFile && CraftMagicNumbers.isLegacy(descriptionFile)) {
enableMsg += "*";
}
plugin.getLogger().info(enableMsg);
JavaPlugin jPlugin = (JavaPlugin) plugin;
if (jPlugin.getClass().getClassLoader() instanceof ConfiguredPluginClassLoader classLoader) { // Paper
if (PaperClassLoaderStorage.instance().registerUnsafePlugin(classLoader)) {
this.server.getLogger().log(Level.WARNING, "Enabled plugin with unregistered ConfiguredPluginClassLoader " + plugin.getPluginMeta().getDisplayName());
}
} // Paper
try {
jPlugin.setEnabled(true);
} catch (Throwable ex) {
this.server.getLogger().log(Level.SEVERE, "Error occurred while enabling " + plugin.getPluginMeta().getDisplayName() + " (Is it up to date?)", ex);
// Paper start - Disable plugins that fail to load
this.server.getPluginManager().disablePlugin(jPlugin);
return;
// Paper end
}
// Perhaps abort here, rather than continue going, but as it stands,
// an abort is not possible the way it's currently written
this.server.getPluginManager().callEvent(new PluginEnableEvent(plugin));
} catch (Throwable ex) {
this.handlePluginException("Error occurred (in the plugin loader) while enabling "
+ plugin.getPluginMeta().getDisplayName() + " (Is it up to date?)", ex, plugin);
}
HandlerList.bakeAll();
}
public synchronized void disablePlugin(@NotNull Plugin plugin) {
if (!(plugin instanceof JavaPlugin javaPlugin)) {
throw new IllegalArgumentException("Only expects java plugins.");
}
if (!plugin.isEnabled()) {
return;
}
String pluginName = plugin.getPluginMeta().getDisplayName();
try {
plugin.getLogger().info("Disabling %s".formatted(pluginName));
this.server.getPluginManager().callEvent(new PluginDisableEvent(plugin));
try {
javaPlugin.setEnabled(false);
} catch (Throwable ex) {
this.server.getLogger().log(Level.SEVERE, "Error occurred while disabling " + pluginName, ex);
}
ClassLoader classLoader = plugin.getClass().getClassLoader();
if (classLoader instanceof ConfiguredPluginClassLoader configuredPluginClassLoader) {
try {
configuredPluginClassLoader.close();
} catch (IOException ex) {
this.server.getLogger().log(Level.WARNING, "Error closing the classloader for '" + pluginName + "'", ex); // Paper - log exception
}
// Remove from the classloader pool inorder to prevent plugins from trying
// to access classes
PaperClassLoaderStorage.instance().unregisterClassloader(configuredPluginClassLoader);
}
} catch (Throwable ex) {
this.handlePluginException("Error occurred (in the plugin loader) while disabling "
+ pluginName + " (Is it up to date?)", ex, plugin); // Paper
}
try {
this.server.getScheduler().cancelTasks(plugin);
} catch (Throwable ex) {
this.handlePluginException("Error occurred (in the plugin loader) while cancelling tasks for "
+ pluginName + " (Is it up to date?)", ex, plugin); // Paper
}
try {
this.server.getServicesManager().unregisterAll(plugin);
} catch (Throwable ex) {
this.handlePluginException("Error occurred (in the plugin loader) while unregistering services for "
+ pluginName + " (Is it up to date?)", ex, plugin); // Paper
}
try {
HandlerList.unregisterAll(plugin);
} catch (Throwable ex) {
this.handlePluginException("Error occurred (in the plugin loader) while unregistering events for "
+ pluginName + " (Is it up to date?)", ex, plugin); // Paper
}
try {
this.server.getMessenger().unregisterIncomingPluginChannel(plugin);
this.server.getMessenger().unregisterOutgoingPluginChannel(plugin);
} catch (Throwable ex) {
this.handlePluginException("Error occurred (in the plugin loader) while unregistering plugin channels for "
+ pluginName + " (Is it up to date?)", ex, plugin); // Paper
}
try {
for (World world : this.server.getWorlds()) {
world.removePluginChunkTickets(plugin);
}
} catch (Throwable ex) {
this.handlePluginException("Error occurred (in the plugin loader) while removing chunk tickets for " + pluginName + " (Is it up to date?)", ex, plugin); // Paper
}
}
// TODO: Implement event part in future patch (paper patch move up, this patch is lower)
private void handlePluginException(String msg, Throwable ex, Plugin plugin) {
Bukkit.getServer().getLogger().log(Level.SEVERE, msg, ex);
this.pluginManager.callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerPluginEnableDisableException(msg, ex, plugin)));
}
public boolean isTransitiveDepend(@NotNull PluginMeta plugin, @NotNull PluginMeta depend) {
return this.dependencyTree.isTransitiveDependency(plugin, depend);
}
public boolean hasDependency(String pluginIdentifier) {
return this.getPlugin(pluginIdentifier) != null;
}
// Debug only
@ApiStatus.Internal
public MutableGraph<String> getDependencyGraph() {
return this.dependencyTree.getGraph();
}
}

View File

@@ -0,0 +1,246 @@
package io.papermc.paper.plugin.manager;
import com.google.common.graph.MutableGraph;
import io.papermc.paper.plugin.PermissionManager;
import io.papermc.paper.plugin.configuration.PluginMeta;
import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.command.CommandMap;
import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.event.Event;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.permissions.Permissible;
import org.bukkit.permissions.Permission;
import org.bukkit.plugin.EventExecutor;
import org.bukkit.plugin.InvalidDescriptionException;
import org.bukkit.plugin.InvalidPluginException;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginLoader;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.SimplePluginManager;
import org.bukkit.plugin.UnknownDependencyException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.List;
import java.util.Set;
public class PaperPluginManagerImpl implements PluginManager, DependencyContext {
final PaperPluginInstanceManager instanceManager;
final PaperEventManager paperEventManager;
PermissionManager permissionManager;
public PaperPluginManagerImpl(Server server, CommandMap commandMap, @Nullable SimplePluginManager permissionManager) {
this.instanceManager = new PaperPluginInstanceManager(this, commandMap, server);
this.paperEventManager = new PaperEventManager(server);
if (permissionManager == null) {
this.permissionManager = new NormalPaperPermissionManager();
} else {
this.permissionManager = new StupidSPMPermissionManagerWrapper(permissionManager); // TODO: See comment when SimplePermissionManager is removed
}
}
// REMOVE THIS WHEN SimplePluginManager is removed.
// Just cast and use Bukkit.getServer().getPluginManager()
public static PaperPluginManagerImpl getInstance() {
return ((CraftServer) (Bukkit.getServer())).paperPluginManager;
}
// Plugin Manipulation
@Override
public @Nullable Plugin getPlugin(@NotNull String name) {
return this.instanceManager.getPlugin(name);
}
@Override
public @NotNull Plugin[] getPlugins() {
return this.instanceManager.getPlugins();
}
@Override
public boolean isPluginEnabled(@NotNull String name) {
return this.instanceManager.isPluginEnabled(name);
}
@Override
public boolean isPluginEnabled(@Nullable Plugin plugin) {
return this.instanceManager.isPluginEnabled(plugin);
}
public void loadPlugin(Plugin plugin) {
this.instanceManager.loadPlugin(plugin);
}
@Override
public @Nullable Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, InvalidDescriptionException, UnknownDependencyException {
return this.instanceManager.loadPlugin(file.toPath());
}
@Override
public @NotNull Plugin[] loadPlugins(@NotNull File directory) {
return this.instanceManager.loadPlugins(directory.toPath());
}
@Override
public @NotNull Plugin[] loadPlugins(final @NotNull File[] files) {
return this.instanceManager.loadPlugins(files);
}
@Override
public void disablePlugins() {
this.instanceManager.disablePlugins();
}
@Override
public synchronized void clearPlugins() {
this.instanceManager.clearPlugins();
this.permissionManager.clearPermissions();
this.paperEventManager.clearEvents();
}
@Override
public void enablePlugin(@NotNull Plugin plugin) {
this.instanceManager.enablePlugin(plugin);
}
@Override
public void disablePlugin(@NotNull Plugin plugin) {
this.instanceManager.disablePlugin(plugin);
}
@Override
public boolean isTransitiveDependency(PluginMeta pluginMeta, PluginMeta dependencyConfig) {
return this.instanceManager.isTransitiveDepend(pluginMeta, dependencyConfig);
}
@Override
public boolean hasDependency(String pluginIdentifier) {
return this.instanceManager.hasDependency(pluginIdentifier);
}
// Event manipulation
@Override
public void callEvent(@NotNull Event event) throws IllegalStateException {
this.paperEventManager.callEvent(event);
}
@Override
public void registerEvents(@NotNull Listener listener, @NotNull Plugin plugin) {
this.paperEventManager.registerEvents(listener, plugin);
}
@Override
public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin) {
this.paperEventManager.registerEvent(event, listener, priority, executor, plugin);
}
@Override
public void registerEvent(@NotNull Class<? extends Event> event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin, boolean ignoreCancelled) {
this.paperEventManager.registerEvent(event, listener, priority, executor, plugin, ignoreCancelled);
}
// Permission manipulation
@Override
public @Nullable Permission getPermission(@NotNull String name) {
return this.permissionManager.getPermission(name);
}
@Override
public void addPermission(@NotNull Permission perm) {
this.permissionManager.addPermission(perm);
}
@Override
public void removePermission(@NotNull Permission perm) {
this.permissionManager.removePermission(perm);
}
@Override
public void removePermission(@NotNull String name) {
this.permissionManager.removePermission(name);
}
@Override
public @NotNull Set<Permission> getDefaultPermissions(boolean op) {
return this.permissionManager.getDefaultPermissions(op);
}
@Override
public void recalculatePermissionDefaults(@NotNull Permission perm) {
this.permissionManager.recalculatePermissionDefaults(perm);
}
@Override
public void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible) {
this.permissionManager.subscribeToPermission(permission, permissible);
}
@Override
public void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible) {
this.permissionManager.unsubscribeFromPermission(permission, permissible);
}
@Override
public @NotNull Set<Permissible> getPermissionSubscriptions(@NotNull String permission) {
return this.permissionManager.getPermissionSubscriptions(permission);
}
@Override
public void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible) {
this.permissionManager.subscribeToDefaultPerms(op, permissible);
}
@Override
public void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible) {
this.permissionManager.unsubscribeFromDefaultPerms(op, permissible);
}
@Override
public @NotNull Set<Permissible> getDefaultPermSubscriptions(boolean op) {
return this.permissionManager.getDefaultPermSubscriptions(op);
}
@Override
public @NotNull Set<Permission> getPermissions() {
return this.permissionManager.getPermissions();
}
@Override
public void addPermissions(@NotNull List<Permission> perm) {
this.permissionManager.addPermissions(perm);
}
@Override
public void clearPermissions() {
this.permissionManager.clearPermissions();
}
@Override
public void overridePermissionManager(@NotNull Plugin plugin, @Nullable PermissionManager permissionManager) {
this.permissionManager = permissionManager;
}
// Etc
@Override
public boolean useTimings() {
return co.aikar.timings.Timings.isTimingsEnabled();
}
@Override
public void registerInterface(@NotNull Class<? extends PluginLoader> loader) throws IllegalArgumentException {
throw new UnsupportedOperationException();
}
public MutableGraph<String> getInstanceManagerGraph() {
return instanceManager.getDependencyGraph();
}
}

View File

@@ -0,0 +1,47 @@
package io.papermc.paper.plugin.manager;
import com.destroystokyo.paper.util.SneakyThrow;
import io.papermc.paper.plugin.entrypoint.Entrypoint;
import io.papermc.paper.plugin.entrypoint.EntrypointHandler;
import io.papermc.paper.plugin.provider.PluginProvider;
import io.papermc.paper.plugin.storage.ProviderStorage;
import org.bukkit.plugin.InvalidPluginException;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
/**
* Used for loading plugins during runtime, only supporting providers that are plugins.
* This is only used for the plugin manager, as it only allows plugins to be
* registered to a provider storage.
*/
class RuntimePluginEntrypointHandler<T extends ProviderStorage<JavaPlugin>> implements EntrypointHandler {
private final T providerStorage;
RuntimePluginEntrypointHandler(T providerStorage) {
this.providerStorage = providerStorage;
}
@Override
public <T> void register(Entrypoint<T> entrypoint, PluginProvider<T> provider) {
if (!entrypoint.equals(Entrypoint.PLUGIN)) {
SneakyThrow.sneaky(new InvalidPluginException("Plugin cannot register entrypoints other than PLUGIN during runtime. Tried registering %s!".formatted(entrypoint)));
// We have to throw an invalid plugin exception for legacy reasons
}
this.providerStorage.register((PluginProvider<JavaPlugin>) provider);
}
@Override
public void enter(Entrypoint<?> entrypoint) {
if (entrypoint != Entrypoint.PLUGIN) {
throw new IllegalArgumentException("Only plugin entrypoint supported");
}
this.providerStorage.enter();
}
@NotNull
public T getPluginProviderStorage() {
return this.providerStorage;
}
}

View File

@@ -0,0 +1,79 @@
package io.papermc.paper.plugin.manager;
import com.destroystokyo.paper.util.SneakyThrow;
import io.papermc.paper.plugin.entrypoint.Entrypoint;
import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler;
import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
import io.papermc.paper.plugin.provider.PluginProvider;
import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent;
import io.papermc.paper.plugin.storage.ServerPluginProviderStorage;
import org.bukkit.plugin.InvalidPluginException;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.UnknownDependencyException;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Used for registering a single plugin provider.
* This has special behavior in that some errors are thrown instead of logged.
*/
class SingularRuntimePluginProviderStorage extends ServerPluginProviderStorage {
private final MetaDependencyTree dependencyTree;
private PluginProvider<JavaPlugin> lastProvider;
private JavaPlugin singleLoaded;
SingularRuntimePluginProviderStorage(MetaDependencyTree dependencyTree) {
this.dependencyTree = dependencyTree;
}
@Override
public void register(PluginProvider<JavaPlugin> provider) {
super.register(provider);
if (this.lastProvider != null) {
SneakyThrow.sneaky(new InvalidPluginException("Plugin registered two JavaPlugins"));
}
if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
throw new IllegalStateException("Cannot register paper plugins during runtime!");
}
this.lastProvider = provider;
// Register the provider into the server entrypoint, this allows it to show in /plugins correctly.
// Generally it might be better in the future to make a separate storage, as putting it into the entrypoint handlers doesn't make much sense.
LaunchEntryPointHandler.INSTANCE.register(Entrypoint.PLUGIN, provider);
}
@Override
public void enter() {
PluginProvider<JavaPlugin> provider = this.lastProvider;
if (provider == null) {
return;
}
// Go through normal plugin loading logic
super.enter();
}
@Override
public void processProvided(PluginProvider<JavaPlugin> provider, JavaPlugin provided) {
super.processProvided(provider, provided);
this.singleLoaded = provided;
}
@Override
public boolean throwOnCycle() {
return false;
}
public Optional<JavaPlugin> getSingleLoaded() {
return Optional.ofNullable(this.singleLoaded);
}
@Override
public MetaDependencyTree createDependencyTree() {
return this.dependencyTree;
}
}

View File

@@ -0,0 +1,42 @@
package io.papermc.paper.plugin.manager;
import org.bukkit.permissions.Permissible;
import org.bukkit.permissions.Permission;
import org.bukkit.plugin.SimplePluginManager;
import java.util.Map;
import java.util.Set;
/*
This is actually so cursed I hate it.
We need to wrap these in fields as people override the fields, so we need to access them lazily at all times.
// TODO: When SimplePluginManager is GONE remove this and cleanup the PaperPermissionManager to use actual fields.
*/
class StupidSPMPermissionManagerWrapper extends PaperPermissionManager {
private final SimplePluginManager simplePluginManager;
public StupidSPMPermissionManagerWrapper(SimplePluginManager simplePluginManager) {
this.simplePluginManager = simplePluginManager;
}
@Override
public Map<String, Permission> permissions() {
return this.simplePluginManager.permissions;
}
@Override
public Map<Boolean, Set<Permission>> defaultPerms() {
return this.simplePluginManager.defaultPerms;
}
@Override
public Map<String, Map<Permissible, Boolean>> permSubs() {
return this.simplePluginManager.permSubs;
}
@Override
public Map<Boolean, Map<Permissible, Boolean>> defSubs() {
return this.simplePluginManager.defSubs;
}
}