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,25 @@
package io.papermc.paper.plugin.entrypoint;
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
import org.bukkit.plugin.java.JavaPlugin;
/**
* Used to mark a certain place that {@link EntrypointHandler} will register {@link io.papermc.paper.plugin.provider.PluginProvider} under.
* Used for loading only certain providers at a certain time.
* @param <T> provider type
*/
public final class Entrypoint<T> {
public static final Entrypoint<PluginBootstrap> BOOTSTRAPPER = new Entrypoint<>("bootstrapper");
public static final Entrypoint<JavaPlugin> PLUGIN = new Entrypoint<>("plugin");
private final String debugName;
private Entrypoint(String debugName) {
this.debugName = debugName;
}
public String getDebugName() {
return debugName;
}
}

View File

@@ -0,0 +1,14 @@
package io.papermc.paper.plugin.entrypoint;
import io.papermc.paper.plugin.provider.PluginProvider;
/**
* Represents a register that will register providers at a certain {@link Entrypoint},
* where then when the given {@link Entrypoint} is registered those will be loaded.
*/
public interface EntrypointHandler {
<T> void register(Entrypoint<T> entrypoint, PluginProvider<T> provider);
void enter(Entrypoint<?> entrypoint);
}

View File

@@ -0,0 +1,74 @@
package io.papermc.paper.plugin.entrypoint;
import io.papermc.paper.plugin.provider.PluginProvider;
import io.papermc.paper.plugin.storage.BootstrapProviderStorage;
import io.papermc.paper.plugin.storage.ProviderStorage;
import io.papermc.paper.plugin.storage.ServerPluginProviderStorage;
import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
import it.unimi.dsi.fastutil.objects.Object2BooleanOpenHashMap;
import org.jetbrains.annotations.ApiStatus;
import java.util.HashMap;
import java.util.Map;
/**
* Used by the server to register/load plugin bootstrappers and plugins.
*/
public class LaunchEntryPointHandler implements EntrypointHandler {
public static final LaunchEntryPointHandler INSTANCE = new LaunchEntryPointHandler();
private final Map<Entrypoint<?>, ProviderStorage<?>> storage = new HashMap<>();
private final Object2BooleanMap<Entrypoint<?>> enteredMap = new Object2BooleanOpenHashMap<>();
LaunchEntryPointHandler() {
this.populateProviderStorage();
this.enteredMap.defaultReturnValue(false);
}
// Utility
public static void enterBootstrappers() {
LaunchEntryPointHandler.INSTANCE.enter(Entrypoint.BOOTSTRAPPER);
}
@Override
public void enter(Entrypoint<?> entrypoint) {
ProviderStorage<?> storage = this.storage.get(entrypoint);
if (storage == null) {
throw new IllegalArgumentException("No storage registered for entrypoint %s.".formatted(entrypoint));
}
storage.enter();
this.enteredMap.put(entrypoint, true);
}
@Override
public <T> void register(Entrypoint<T> entrypoint, PluginProvider<T> provider) {
ProviderStorage<T> storage = this.get(entrypoint);
if (storage == null) {
throw new IllegalArgumentException("No storage registered for entrypoint %s.".formatted(entrypoint));
}
storage.register(provider);
}
@SuppressWarnings("unchecked")
public <T> ProviderStorage<T> get(Entrypoint<T> entrypoint) {
return (ProviderStorage<T>) this.storage.get(entrypoint);
}
// Debug only
@ApiStatus.Internal
public Map<Entrypoint<?>, ProviderStorage<?>> getStorage() {
return storage;
}
public boolean hasEntered(Entrypoint<?> entrypoint) {
return this.enteredMap.getBoolean(entrypoint);
}
// Reload only
public void populateProviderStorage() {
this.storage.put(Entrypoint.BOOTSTRAPPER, new BootstrapProviderStorage());
this.storage.put(Entrypoint.PLUGIN, new ServerPluginProviderStorage());
}
}

View File

@@ -0,0 +1,22 @@
package io.papermc.paper.plugin.entrypoint.classloader;
import io.papermc.paper.plugin.configuration.PluginMeta;
import net.kyori.adventure.util.Services;
import org.jetbrains.annotations.ApiStatus;
@ApiStatus.Internal
public interface ClassloaderBytecodeModifier {
static ClassloaderBytecodeModifier bytecodeModifier() {
return Provider.INSTANCE;
}
byte[] modify(PluginMeta config, byte[] bytecode);
class Provider {
private static final ClassloaderBytecodeModifier INSTANCE = Services.service(ClassloaderBytecodeModifier.class).orElseThrow();
}
}

View File

@@ -0,0 +1,12 @@
package io.papermc.paper.plugin.entrypoint.classloader;
import io.papermc.paper.plugin.configuration.PluginMeta;
// Stub, implement in future.
public class PaperClassloaderBytecodeModifier implements ClassloaderBytecodeModifier {
@Override
public byte[] modify(PluginMeta configuration, byte[] bytecode) {
return bytecode;
}
}

View File

@@ -0,0 +1,209 @@
package io.papermc.paper.plugin.entrypoint.classloader;
import io.papermc.paper.plugin.configuration.PluginMeta;
import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
import io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage;
import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage;
import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
import org.bukkit.Bukkit;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarFile;
import java.util.logging.Logger;
/**
* This is similar to a {@link org.bukkit.plugin.java.PluginClassLoader} but is completely kept hidden from the api.
* This is only used with Paper plugins.
*
* @see PaperPluginClassLoaderStorage
*/
public class PaperPluginClassLoader extends PaperSimplePluginClassLoader implements ConfiguredPluginClassLoader {
static {
registerAsParallelCapable();
}
private final URLClassLoader libraryLoader;
private final Set<String> seenIllegalAccess = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final Logger logger;
@Nullable
private JavaPlugin loadedJavaPlugin;
@Nullable
private PluginClassLoaderGroup group;
public PaperPluginClassLoader(Logger logger, Path source, JarFile file, PaperPluginMeta configuration, ClassLoader parentLoader, URLClassLoader libraryLoader) throws IOException {
super(source, file, configuration, parentLoader);
this.libraryLoader = libraryLoader;
this.logger = logger;
if (this.configuration().hasOpenClassloader()) {
this.group = PaperClassLoaderStorage.instance().registerOpenGroup(this);
}
}
private PaperPluginMeta configuration() {
return (PaperPluginMeta) this.configuration;
}
public void refreshClassloaderDependencyTree(DependencyContext dependencyContext) {
if (this.configuration().hasOpenClassloader()) {
return;
}
if (this.group != null) {
// We need to unregister the classloader inorder to allow for dependencies
// to be recalculated
PaperClassLoaderStorage.instance().unregisterClassloader(this);
}
this.group = PaperClassLoaderStorage.instance().registerAccessBackedGroup(this, (classLoader) -> {
return dependencyContext.isTransitiveDependency(PaperPluginClassLoader.this.configuration, classLoader.getConfiguration());
});
}
@Override
public URL getResource(String name) {
URL resource = findResource(name);
if (resource == null && this.libraryLoader != null) {
return this.libraryLoader.getResource(name);
}
return resource;
}
@Override
public Enumeration<URL> getResources(String name) throws IOException {
List<URL> resources = new ArrayList<>();
this.addEnumeration(resources, this.findResources(name));
if (this.libraryLoader != null) {
addEnumeration(resources, this.libraryLoader.getResources(name));
}
return Collections.enumeration(resources);
}
private <T> void addEnumeration(List<T> list, Enumeration<T> enumeration) {
while (enumeration.hasMoreElements()) {
list.add(enumeration.nextElement());
}
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
return this.loadClass(name, resolve, true, true);
}
@Override
public PluginMeta getConfiguration() {
return this.configuration;
}
@Override
public Class<?> loadClass(@NotNull String name, boolean resolve, boolean checkGroup, boolean checkLibraries) throws ClassNotFoundException {
try {
Class<?> result = super.loadClass(name, resolve);
// SPIGOT-6749: Library classes will appear in the above, but we don't want to return them to other plugins
if (checkGroup || result.getClassLoader() == this) {
return result;
}
} catch (ClassNotFoundException ignored) {
}
if (checkLibraries) {
try {
return this.libraryLoader.loadClass(name);
} catch (ClassNotFoundException ignored) {
}
}
if (checkGroup) {
// This ignores the libraries of other plugins, unless they are transitive dependencies.
if (this.group == null) {
throw new IllegalStateException("Tried to resolve class while group was not yet initialized");
}
Class<?> clazz = this.group.getClassByName(name, resolve, this);
if (clazz != null) {
return clazz;
}
}
throw new ClassNotFoundException(name);
}
@Override
public void init(JavaPlugin plugin) {
PluginMeta config = this.configuration;
PluginDescriptionFile pluginDescriptionFile = new PluginDescriptionFile(
config.getName(),
config.getName(),
config.getProvidedPlugins(),
config.getMainClass(),
"", // Classloader load order api
config.getPluginDependencies(), // Dependencies
config.getPluginSoftDependencies(), // Soft Depends
config.getLoadBeforePlugins(), // Load Before
config.getVersion(),
Map.of(), // Commands, we use a separate system
config.getDescription(),
config.getAuthors(),
config.getContributors(),
config.getWebsite(),
config.getLoggerPrefix(),
config.getLoadOrder(),
config.getPermissions(),
config.getPermissionDefault(),
Set.of(), // Aware api
config.getAPIVersion(),
List.of() // Libraries
);
File dataFolder = new File(Bukkit.getPluginsFolder(), pluginDescriptionFile.getName());
plugin.init(Bukkit.getServer(), pluginDescriptionFile, dataFolder, this.source.toFile(), this, config, this.logger);
this.loadedJavaPlugin = plugin;
}
@Nullable
@Override
public JavaPlugin getPlugin() {
return this.loadedJavaPlugin;
}
@Override
public String toString() {
return "PaperPluginClassLoader{" +
"libraryLoader=" + this.libraryLoader +
", seenIllegalAccess=" + this.seenIllegalAccess +
", loadedJavaPlugin=" + this.loadedJavaPlugin +
'}';
}
@Override
public void close() throws IOException {
try (this.jar; this.libraryLoader) {
super.close();
}
}
@Override
public @Nullable PluginClassLoaderGroup getGroup() {
return this.group;
}
}

View File

@@ -0,0 +1,116 @@
package io.papermc.paper.plugin.entrypoint.classloader;
import io.papermc.paper.plugin.configuration.PluginMeta;
import io.papermc.paper.plugin.util.NamespaceChecker;
import org.jetbrains.annotations.ApiStatus;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.security.CodeSigner;
import java.security.CodeSource;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
/**
* Represents a simple classloader used for paper plugin bootstrappers.
*/
@ApiStatus.Internal
public class PaperSimplePluginClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
protected final PluginMeta configuration;
protected final Path source;
protected final Manifest jarManifest;
protected final URL jarUrl;
protected final JarFile jar;
public PaperSimplePluginClassLoader(Path source, JarFile file, PluginMeta configuration, ClassLoader parentLoader) throws IOException {
super(source.getFileName().toString(), new URL[]{source.toUri().toURL()}, parentLoader);
this.source = source;
this.jarManifest = file.getManifest();
this.jarUrl = source.toUri().toURL();
this.configuration = configuration;
this.jar = file;
}
@Override
public URL getResource(String name) {
return this.findResource(name);
}
@Override
public Enumeration<URL> getResources(String name) throws IOException {
return this.findResources(name);
}
// Bytecode modification supported loader
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
NamespaceChecker.validateNameSpaceForClassloading(name);
// See UrlClassLoader#findClass(String)
String path = name.replace('.', '/').concat(".class");
JarEntry entry = this.jar.getJarEntry(path);
if (entry == null) {
throw new ClassNotFoundException(name);
}
// See URLClassLoader#defineClass(String, Resource)
byte[] classBytes;
try (InputStream is = this.jar.getInputStream(entry)) {
classBytes = is.readAllBytes();
} catch (IOException ex) {
throw new ClassNotFoundException(name, ex);
}
classBytes = ClassloaderBytecodeModifier.bytecodeModifier().modify(this.configuration, classBytes);
int dot = name.lastIndexOf('.');
if (dot != -1) {
String pkgName = name.substring(0, dot);
// Get defined package does not correctly handle sealed packages.
if (this.getDefinedPackage(pkgName) == null) {
try {
if (this.jarManifest != null) {
this.definePackage(pkgName, this.jarManifest, this.jarUrl);
} else {
this.definePackage(pkgName, null, null, null, null, null, null, null);
}
} catch (IllegalArgumentException ex) {
// parallel-capable class loaders: re-verify in case of a
// race condition
if (this.getDefinedPackage(pkgName) == null) {
// Should never happen
throw new IllegalStateException("Cannot find package " + pkgName);
}
}
}
}
CodeSigner[] signers = entry.getCodeSigners();
CodeSource source = new CodeSource(this.jarUrl, signers);
return this.defineClass(name, classBytes, 0, classBytes.length, source);
}
@Override
public String toString() {
return "PaperSimplePluginClassLoader{" +
"configuration=" + this.configuration +
", source=" + this.source +
", jarManifest=" + this.jarManifest +
", jarUrl=" + this.jarUrl +
", jar=" + this.jar +
'}';
}
}

View File

@@ -0,0 +1,47 @@
package io.papermc.paper.plugin.entrypoint.classloader.group;
import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
import org.jetbrains.annotations.ApiStatus;
import java.util.ArrayList;
@ApiStatus.Internal
public class DependencyBasedPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup {
private final GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup;
private final ClassLoaderAccess access;
public DependencyBasedPluginClassLoaderGroup(GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup, ClassLoaderAccess access) {
super(new ArrayList<>());
this.access = access;
this.globalPluginClassLoaderGroup = globalPluginClassLoaderGroup;
}
/**
* This will refresh the dependencies of the current classloader.
*/
public void populateDependencies() {
this.classloaders.clear();
for (ConfiguredPluginClassLoader configuredPluginClassLoader : this.globalPluginClassLoaderGroup.getClassLoaders()) {
if (this.access.canAccess(configuredPluginClassLoader)) {
this.classloaders.add(configuredPluginClassLoader);
}
}
}
@Override
public ClassLoaderAccess getAccess() {
return this.access;
}
@Override
public String toString() {
return "DependencyBasedPluginClassLoaderGroup{" +
"globalPluginClassLoaderGroup=" + this.globalPluginClassLoaderGroup +
", access=" + this.access +
", classloaders=" + this.classloaders +
'}';
}
}

View File

@@ -0,0 +1,18 @@
package io.papermc.paper.plugin.entrypoint.classloader.group;
import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
import org.jetbrains.annotations.ApiStatus;
@ApiStatus.Internal
public class GlobalPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup {
@Override
public ClassLoaderAccess getAccess() {
return (v) -> true;
}
@Override
public String toString() {
return "GLOBAL:" + super.toString();
}
}

View File

@@ -0,0 +1,76 @@
package io.papermc.paper.plugin.entrypoint.classloader.group;
import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@ApiStatus.Internal
public class LockingClassLoaderGroup implements PluginClassLoaderGroup {
private final PluginClassLoaderGroup parent;
private final Map<String, ClassLockEntry> classLoadLock = new HashMap<>();
public LockingClassLoaderGroup(PluginClassLoaderGroup parent) {
this.parent = parent;
}
@Override
public @Nullable Class<?> getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) {
// make MT safe
ClassLockEntry lock;
synchronized (this.classLoadLock) {
lock = this.classLoadLock.computeIfAbsent(name, (x) -> new ClassLockEntry(new AtomicInteger(0), new java.util.concurrent.locks.ReentrantReadWriteLock()));
lock.count.incrementAndGet();
}
lock.reentrantReadWriteLock.writeLock().lock();
try {
return parent.getClassByName(name, resolve, requester);
} finally {
synchronized (this.classLoadLock) {
lock.reentrantReadWriteLock.writeLock().unlock();
if (lock.count.get() == 1) {
this.classLoadLock.remove(name);
} else {
lock.count.decrementAndGet();
}
}
}
}
@Override
public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) {
this.parent.remove(configuredPluginClassLoader);
}
@Override
public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) {
this.parent.add(configuredPluginClassLoader);
}
@Override
public ClassLoaderAccess getAccess() {
return this.parent.getAccess();
}
public PluginClassLoaderGroup getParent() {
return parent;
}
record ClassLockEntry(AtomicInteger count, ReentrantReadWriteLock reentrantReadWriteLock) {
}
@Override
public String toString() {
return "LockingClassLoaderGroup{" +
"parent=" + this.parent +
", classLoadLock=" + this.classLoadLock +
'}';
}
}

View File

@@ -0,0 +1,93 @@
package io.papermc.paper.plugin.entrypoint.classloader.group;
import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage;
import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
import org.bukkit.plugin.java.PluginClassLoader;
import org.jetbrains.annotations.ApiStatus;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* This is used for connecting multiple classloaders.
*/
public final class PaperPluginClassLoaderStorage implements PaperClassLoaderStorage {
private final GlobalPluginClassLoaderGroup globalGroup = new GlobalPluginClassLoaderGroup();
private final List<PluginClassLoaderGroup> groups = new CopyOnWriteArrayList<>();
public PaperPluginClassLoaderStorage() {
this.groups.add(this.globalGroup);
}
@Override
public PluginClassLoaderGroup registerSpigotGroup(PluginClassLoader pluginClassLoader) {
return this.registerGroup(pluginClassLoader, new SpigotPluginClassLoaderGroup(this.globalGroup, (library) -> {
return pluginClassLoader.dependencyContext.isTransitiveDependency(pluginClassLoader.getConfiguration(), library.getConfiguration());
}, pluginClassLoader));
}
@Override
public PluginClassLoaderGroup registerOpenGroup(ConfiguredPluginClassLoader classLoader) {
return this.registerGroup(classLoader, this.globalGroup);
}
@Override
public PluginClassLoaderGroup registerAccessBackedGroup(ConfiguredPluginClassLoader classLoader, ClassLoaderAccess access) {
List<ConfiguredPluginClassLoader> allowedLoaders = new ArrayList<>();
for (ConfiguredPluginClassLoader configuredPluginClassLoader : this.globalGroup.getClassLoaders()) {
if (access.canAccess(configuredPluginClassLoader)) {
allowedLoaders.add(configuredPluginClassLoader);
}
}
return this.registerGroup(classLoader, new StaticPluginClassLoaderGroup(allowedLoaders, access, classLoader));
}
private PluginClassLoaderGroup registerGroup(ConfiguredPluginClassLoader classLoader, PluginClassLoaderGroup group) {
// Now add this classloader to any groups that allows it (includes global)
for (PluginClassLoaderGroup loaderGroup : this.groups) {
if (loaderGroup.getAccess().canAccess(classLoader)) {
loaderGroup.add(classLoader);
}
}
group = new LockingClassLoaderGroup(group);
this.groups.add(group);
return group;
}
@Override
public void unregisterClassloader(ConfiguredPluginClassLoader configuredPluginClassLoader) {
this.globalGroup.remove(configuredPluginClassLoader);
this.groups.remove(configuredPluginClassLoader.getGroup());
for (PluginClassLoaderGroup group : this.groups) {
group.remove(configuredPluginClassLoader);
}
}
@Override
public boolean registerUnsafePlugin(ConfiguredPluginClassLoader pluginLoader) {
if (this.globalGroup.getClassLoaders().contains(pluginLoader)) {
return false;
} else {
this.globalGroup.add(pluginLoader);
return true;
}
}
// Debug only
@ApiStatus.Internal
public GlobalPluginClassLoaderGroup getGlobalGroup() {
return this.globalGroup;
}
// Debug only
@ApiStatus.Internal
public List<PluginClassLoaderGroup> getGroups() {
return this.groups;
}
}

View File

@@ -0,0 +1,69 @@
package io.papermc.paper.plugin.entrypoint.classloader.group;
import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@ApiStatus.Internal
public abstract class SimpleListPluginClassLoaderGroup implements PluginClassLoaderGroup {
private static final boolean DISABLE_CLASS_PRIORITIZATION = Boolean.getBoolean("Paper.DisableClassPrioritization");
protected final List<ConfiguredPluginClassLoader> classloaders;
protected SimpleListPluginClassLoaderGroup() {
this(new CopyOnWriteArrayList<>());
}
protected SimpleListPluginClassLoaderGroup(List<ConfiguredPluginClassLoader> classloaders) {
this.classloaders = classloaders;
}
@Override
public @Nullable Class<?> getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) {
if (!DISABLE_CLASS_PRIORITIZATION) {
try {
return this.lookupClass(name, false, requester); // First check the requester
} catch (ClassNotFoundException ignored) {
}
}
for (ConfiguredPluginClassLoader loader : this.classloaders) {
try {
return this.lookupClass(name, resolve, loader);
} catch (ClassNotFoundException ignored) {
}
}
return null;
}
protected Class<?> lookupClass(String name, boolean resolve, ConfiguredPluginClassLoader current) throws ClassNotFoundException {
return current.loadClass(name, resolve, false, true);
}
@Override
public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) {
this.classloaders.remove(configuredPluginClassLoader);
}
@Override
public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) {
this.classloaders.add(configuredPluginClassLoader);
}
public List<ConfiguredPluginClassLoader> getClassLoaders() {
return classloaders;
}
@Override
public String toString() {
return "SimpleListPluginClassLoaderGroup{" +
"classloaders=" + this.classloaders +
'}';
}
}

View File

@@ -0,0 +1,60 @@
package io.papermc.paper.plugin.entrypoint.classloader.group;
import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
@ApiStatus.Internal
public class SingletonPluginClassLoaderGroup implements PluginClassLoaderGroup {
private final ConfiguredPluginClassLoader configuredPluginClassLoader;
private final Access access;
public SingletonPluginClassLoaderGroup(ConfiguredPluginClassLoader configuredPluginClassLoader) {
this.configuredPluginClassLoader = configuredPluginClassLoader;
this.access = new Access();
}
@Override
public @Nullable Class<?> getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) {
try {
return this.configuredPluginClassLoader.loadClass(name, resolve, false, true);
} catch (ClassNotFoundException ignored) {
}
return null;
}
@Override
public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) {
}
@Override
public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) {
}
@Override
public ClassLoaderAccess getAccess() {
return this.access;
}
@ApiStatus.Internal
private class Access implements ClassLoaderAccess {
@Override
public boolean canAccess(ConfiguredPluginClassLoader classLoader) {
return SingletonPluginClassLoaderGroup.this.configuredPluginClassLoader == classLoader;
}
}
@Override
public String toString() {
return "SingletonPluginClassLoaderGroup{" +
"configuredPluginClassLoader=" + this.configuredPluginClassLoader +
", access=" + this.access +
'}';
}
}

View File

@@ -0,0 +1,58 @@
package io.papermc.paper.plugin.entrypoint.classloader.group;
import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
import org.bukkit.plugin.java.PluginClassLoader;
import org.jetbrains.annotations.ApiStatus;
import java.util.function.Predicate;
/**
* Spigot classloaders have the ability to see everything.
* However, libraries are ONLY shared depending on their dependencies.
*/
@ApiStatus.Internal
public class SpigotPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup {
private final Predicate<ConfiguredPluginClassLoader> libraryClassloaderPredicate;
private final PluginClassLoader pluginClassLoader;
public SpigotPluginClassLoaderGroup(GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup, Predicate<ConfiguredPluginClassLoader> libraryClassloaderPredicate, PluginClassLoader pluginClassLoader) {
super(globalPluginClassLoaderGroup.getClassLoaders());
this.libraryClassloaderPredicate = libraryClassloaderPredicate;
this.pluginClassLoader = pluginClassLoader;
}
// Mirrors global list
@Override
public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) {
}
@Override
public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) {
}
// Don't allow other plugins to access spigot dependencies, they should instead reference the global list
@Override
public ClassLoaderAccess getAccess() {
return v -> false;
}
@Override
protected Class<?> lookupClass(String name, boolean resolve, ConfiguredPluginClassLoader current) throws ClassNotFoundException {
return current.loadClass(name, resolve, false, this.libraryClassloaderPredicate.test(current));
}
// DEBUG
public PluginClassLoader getPluginClassLoader() {
return pluginClassLoader;
}
@Override
public String toString() {
return "SpigotPluginClassLoaderGroup{" +
"libraryClassloaderPredicate=" + this.libraryClassloaderPredicate +
", classloaders=" + this.classloaders +
'}';
}
}

View File

@@ -0,0 +1,40 @@
package io.papermc.paper.plugin.entrypoint.classloader.group;
import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess;
import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader;
import org.jetbrains.annotations.ApiStatus;
import java.util.List;
@ApiStatus.Internal
public class StaticPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup {
private final ClassLoaderAccess access;
// Debug only
private final ConfiguredPluginClassLoader mainClassloaderHolder;
public StaticPluginClassLoaderGroup(List<ConfiguredPluginClassLoader> classloaders, ClassLoaderAccess access, ConfiguredPluginClassLoader mainClassloaderHolder) {
super(classloaders);
this.access = access;
this.mainClassloaderHolder = mainClassloaderHolder;
}
@Override
public ClassLoaderAccess getAccess() {
return this.access;
}
// DEBUG
@ApiStatus.Internal
public ConfiguredPluginClassLoader getPluginClassloader() {
return this.mainClassloaderHolder;
}
@Override
public String toString() {
return "StaticPluginClassLoaderGroup{" +
"access=" + this.access +
", classloaders=" + this.classloaders +
'}';
}
}

View File

@@ -0,0 +1,44 @@
package io.papermc.paper.plugin.entrypoint.dependency;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.MutableGraph;
import io.papermc.paper.plugin.configuration.PluginMeta;
import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
public class BootstrapMetaDependencyTree extends MetaDependencyTree {
public BootstrapMetaDependencyTree() {
this(GraphBuilder.directed().build());
}
public BootstrapMetaDependencyTree(MutableGraph<String> graph) {
super(graph);
}
@Override
protected void registerDependencies(final String identifier, final PluginMeta meta) {
if (!(meta instanceof PaperPluginMeta paperPluginMeta)) {
throw new IllegalStateException("Only paper plugins can have a bootstrapper!");
}
// Build a validated provider's dependencies into the graph
for (String dependency : paperPluginMeta.getBootstrapDependencies().keySet()) {
this.graph.putEdge(identifier, dependency);
}
}
@Override
protected void unregisterDependencies(final String identifier, final PluginMeta meta) {
if (!(meta instanceof PaperPluginMeta paperPluginMeta)) {
throw new IllegalStateException("PluginMeta must be a PaperPluginMeta");
}
// Build a validated provider's dependencies into the graph
for (String dependency : paperPluginMeta.getBootstrapDependencies().keySet()) {
this.graph.removeEdge(identifier, dependency);
}
}
@Override
public String toString() {
return "BootstrapDependencyTree{" + "graph=" + this.graph + '}';
}
}

View File

@@ -0,0 +1,9 @@
package io.papermc.paper.plugin.entrypoint.dependency;
import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
public interface DependencyContextHolder {
void setContext(DependencyContext context);
}

View File

@@ -0,0 +1,54 @@
package io.papermc.paper.plugin.entrypoint.dependency;
import com.google.common.graph.Graph;
import com.google.common.graph.Graphs;
import com.google.common.graph.MutableGraph;
import io.papermc.paper.plugin.configuration.PluginMeta;
import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
import java.util.Set;
@SuppressWarnings("UnstableApiUsage")
public class GraphDependencyContext implements DependencyContext {
private final MutableGraph<String> dependencyGraph;
public GraphDependencyContext(MutableGraph<String> dependencyGraph) {
this.dependencyGraph = dependencyGraph;
}
@Override
public boolean isTransitiveDependency(PluginMeta plugin, PluginMeta depend) {
String pluginIdentifier = plugin.getName();
if (this.dependencyGraph.nodes().contains(pluginIdentifier)) {
Set<String> reachableNodes = Graphs.reachableNodes(this.dependencyGraph, pluginIdentifier);
if (reachableNodes.contains(depend.getName())) {
return true;
}
for (String provided : depend.getProvidedPlugins()) {
if (reachableNodes.contains(provided)) {
return true;
}
}
}
return false;
}
@Override
public boolean hasDependency(String pluginIdentifier) {
return this.dependencyGraph.nodes().contains(pluginIdentifier);
}
public MutableGraph<String> getDependencyGraph() {
return dependencyGraph;
}
@Override
public String toString() {
return "GraphDependencyContext{" +
"dependencyGraph=" + this.dependencyGraph +
'}';
}
}

View File

@@ -0,0 +1,110 @@
package io.papermc.paper.plugin.entrypoint.dependency;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.Graphs;
import com.google.common.graph.MutableGraph;
import io.papermc.paper.plugin.configuration.PluginMeta;
import io.papermc.paper.plugin.provider.PluginProvider;
import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.Set;
public abstract class MetaDependencyTree implements DependencyContext {
protected final MutableGraph<String> graph;
// We need to upkeep a separate collection since when populating
// a graph it adds nodes even if they are not present
protected final Set<String> dependencies = new HashSet<>();
public MetaDependencyTree() {
this(GraphBuilder.directed().build());
}
public MetaDependencyTree(MutableGraph<String> graph) {
this.graph = graph;
}
public void add(PluginMeta configuration) {
String identifier = configuration.getName();
// Build a validated provider's dependencies into the graph
this.registerDependencies(identifier, configuration);
this.graph.addNode(identifier); // Make sure dependencies at least have a node
// Add the provided plugins to the graph as well
for (String provides : configuration.getProvidedPlugins()) {
this.graph.putEdge(identifier, provides);
this.dependencies.add(provides);
}
this.dependencies.add(identifier);
}
protected abstract void registerDependencies(String identifier, PluginMeta meta);
public void remove(PluginMeta configuration) {
String identifier = configuration.getName();
// Remove a validated provider's dependencies into the graph
this.unregisterDependencies(identifier, configuration);
this.graph.removeNode(identifier); // Remove root node
// Remove the provided plugins to the graph as well
for (String provides : configuration.getProvidedPlugins()) {
this.graph.removeEdge(identifier, provides);
this.dependencies.remove(provides);
}
this.dependencies.remove(identifier);
}
protected abstract void unregisterDependencies(String identifier, PluginMeta meta);
@Override
public boolean isTransitiveDependency(@NotNull PluginMeta plugin, @NotNull PluginMeta depend) {
String pluginIdentifier = plugin.getName();
if (this.graph.nodes().contains(pluginIdentifier)) {
Set<String> reachableNodes = Graphs.reachableNodes(this.graph, pluginIdentifier);
if (reachableNodes.contains(depend.getName())) {
return true;
}
for (String provided : depend.getProvidedPlugins()) {
if (reachableNodes.contains(provided)) {
return true;
}
}
}
return false;
}
@Override
public boolean hasDependency(@NotNull String pluginIdentifier) {
return this.dependencies.contains(pluginIdentifier);
}
public void addDirectDependency(String dependency) {
this.dependencies.add(dependency);
}
@Override
public String toString() {
return "SimpleDependencyTree{" +
"graph=" + this.graph +
'}';
}
public MutableGraph<String> getGraph() {
return graph;
}
public void add(PluginProvider<?> provider) {
this.add(provider.getMeta());
}
public void remove(PluginProvider<?> provider) {
this.remove(provider.getMeta());
}
}

View File

@@ -0,0 +1,40 @@
package io.papermc.paper.plugin.entrypoint.dependency;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.Graphs;
import com.google.common.graph.MutableGraph;
import io.papermc.paper.plugin.configuration.PluginMeta;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.Set;
public class SimpleMetaDependencyTree extends MetaDependencyTree {
public SimpleMetaDependencyTree() {
}
public SimpleMetaDependencyTree(final MutableGraph<String> graph) {
super(graph);
}
@Override
protected void registerDependencies(final String identifier, final PluginMeta meta) {
for (String dependency : meta.getPluginDependencies()) {
this.graph.putEdge(identifier, dependency);
}
for (String dependency : meta.getPluginSoftDependencies()) {
this.graph.putEdge(identifier, dependency);
}
}
@Override
protected void unregisterDependencies(final String identifier, final PluginMeta meta) {
for (String dependency : meta.getPluginDependencies()) {
this.graph.removeEdge(identifier, dependency);
}
for (String dependency : meta.getPluginSoftDependencies()) {
this.graph.removeEdge(identifier, dependency);
}
}
}

View File

@@ -0,0 +1,354 @@
/*
* (C) Copyright 2013-2021, by Nikolay Ognyanov and Contributors.
*
* JGraphT : a free Java graph-theory library
*
* See the CONTRIBUTORS.md file distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the
* GNU Lesser General Public License v2.1 or later
* which is available at
* http://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html.
*
* SPDX-License-Identifier: EPL-2.0 OR LGPL-2.1-or-later
*/
// MODIFICATIONS:
// - Modified to use a guava graph directly
package io.papermc.paper.plugin.entrypoint.strategy;
import com.google.common.base.Preconditions;
import com.google.common.graph.Graph;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.MutableGraph;
import com.mojang.datafixers.util.Pair;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
* Find all simple cycles of a directed graph using the Johnson's algorithm.
*
* <p>
* See:<br>
* D.B.Johnson, Finding all the elementary circuits of a directed graph, SIAM J. Comput., 4 (1975),
* pp. 77-84.
*
* @param <V> the vertex type.
*
* @author Nikolay Ognyanov
*/
public class JohnsonSimpleCycles<V>
{
// The graph.
private Graph<V> graph;
// The main state of the algorithm.
private Consumer<List<V>> cycleConsumer = null;
private BiConsumer<V, V> cycleVertexSuccessorConsumer = null; // Paper
private V[] iToV = null;
private Map<V, Integer> vToI = null;
private Set<V> blocked = null;
private Map<V, Set<V>> bSets = null;
private ArrayDeque<V> stack = null;
// The state of the embedded Tarjan SCC algorithm.
private List<Set<V>> foundSCCs = null;
private int index = 0;
private Map<V, Integer> vIndex = null;
private Map<V, Integer> vLowlink = null;
private ArrayDeque<V> path = null;
private Set<V> pathSet = null;
/**
* Create a simple cycle finder for the specified graph.
*
* @param graph - the DirectedGraph in which to find cycles.
*
* @throws IllegalArgumentException if the graph argument is <code>
* null</code>.
*/
public JohnsonSimpleCycles(Graph<V> graph)
{
Preconditions.checkState(graph.isDirected(), "Graph must be directed");
this.graph = graph;
}
/**
* Find the simple cycles of the graph.
*
* @return The list of all simple cycles. Possibly empty but never <code>null</code>.
*/
public List<List<V>> findAndRemoveSimpleCycles()
{
List<List<V>> result = new ArrayList<>();
findSimpleCycles(result::add, (v, s) -> ((MutableGraph<V>) graph).removeEdge(v, s)); // Paper
return result;
}
/**
* Find the simple cycles of the graph.
*
* @param consumer Consumer that will be called with each cycle found.
*/
public void findSimpleCycles(Consumer<List<V>> consumer, BiConsumer<V, V> vertexSuccessorConsumer) // Paper
{
if (graph == null) {
throw new IllegalArgumentException("Null graph.");
}
cycleVertexSuccessorConsumer = vertexSuccessorConsumer; // Paper
initState(consumer);
int startIndex = 0;
int size = graph.nodes().size();
while (startIndex < size) {
Pair<Graph<V>, Integer> minSCCGResult = findMinSCSG(startIndex);
if (minSCCGResult != null) {
startIndex = minSCCGResult.getSecond();
Graph<V> scg = minSCCGResult.getFirst();
V startV = toV(startIndex);
for (V v : scg.successors(startV)) {
blocked.remove(v);
getBSet(v).clear();
}
findCyclesInSCG(startIndex, startIndex, scg);
startIndex++;
} else {
break;
}
}
clearState();
}
private Pair<Graph<V>, Integer> findMinSCSG(int startIndex)
{
/*
* Per Johnson : "adjacency structure of strong component $K$ with least vertex in subgraph
* of $G$ induced by $(s, s + 1, n)$". Or in contemporary terms: the strongly connected
* component of the subgraph induced by $(v_1, \dotso ,v_n)$ which contains the minimum
* (among those SCCs) vertex index. We return that index together with the graph.
*/
initMinSCGState();
List<Set<V>> foundSCCs = findSCCS(startIndex);
// find the SCC with the minimum index
int minIndexFound = Integer.MAX_VALUE;
Set<V> minSCC = null;
for (Set<V> scc : foundSCCs) {
for (V v : scc) {
int t = toI(v);
if (t < minIndexFound) {
minIndexFound = t;
minSCC = scc;
}
}
}
if (minSCC == null) {
return null;
}
// build a graph for the SCC found
MutableGraph<V> dependencyGraph = GraphBuilder.directed().allowsSelfLoops(true).build();
for (V v : minSCC) {
for (V w : minSCC) {
if (graph.hasEdgeConnecting(v, w)) {
dependencyGraph.putEdge(v, w);
}
}
}
Pair<Graph<V>, Integer> result = Pair.of(dependencyGraph, minIndexFound);
clearMinSCCState();
return result;
}
private List<Set<V>> findSCCS(int startIndex)
{
// Find SCCs in the subgraph induced
// by vertices startIndex and beyond.
// A call to StrongConnectivityAlgorithm
// would be too expensive because of the
// need to materialize the subgraph.
// So - do a local search by the Tarjan's
// algorithm and pretend that vertices
// with an index smaller than startIndex
// do not exist.
for (V v : graph.nodes()) {
int vI = toI(v);
if (vI < startIndex) {
continue;
}
if (!vIndex.containsKey(v)) {
getSCCs(startIndex, vI);
}
}
List<Set<V>> result = foundSCCs;
foundSCCs = null;
return result;
}
private void getSCCs(int startIndex, int vertexIndex)
{
V vertex = toV(vertexIndex);
vIndex.put(vertex, index);
vLowlink.put(vertex, index);
index++;
path.push(vertex);
pathSet.add(vertex);
Set<V> edges = graph.successors(vertex);
for (V successor : edges) {
int successorIndex = toI(successor);
if (successorIndex < startIndex) {
continue;
}
if (!vIndex.containsKey(successor)) {
getSCCs(startIndex, successorIndex);
vLowlink.put(vertex, Math.min(vLowlink.get(vertex), vLowlink.get(successor)));
} else if (pathSet.contains(successor)) {
vLowlink.put(vertex, Math.min(vLowlink.get(vertex), vIndex.get(successor)));
}
}
if (vLowlink.get(vertex).equals(vIndex.get(vertex))) {
Set<V> result = new HashSet<>();
V temp;
do {
temp = path.pop();
pathSet.remove(temp);
result.add(temp);
} while (!vertex.equals(temp));
if (result.size() == 1) {
V v = result.iterator().next();
if (graph.edges().contains(vertex)) {
foundSCCs.add(result);
}
} else {
foundSCCs.add(result);
}
}
}
private boolean findCyclesInSCG(int startIndex, int vertexIndex, Graph<V> scg)
{
/*
* Find cycles in a strongly connected graph per Johnson.
*/
boolean foundCycle = false;
V vertex = toV(vertexIndex);
stack.push(vertex);
blocked.add(vertex);
for (V successor : scg.successors(vertex)) {
int successorIndex = toI(successor);
if (successorIndex == startIndex) {
List<V> cycle = new ArrayList<>(stack.size());
stack.descendingIterator().forEachRemaining(cycle::add);
cycleConsumer.accept(cycle);
cycleVertexSuccessorConsumer.accept(vertex, successor); // Paper
//foundCycle = true; // Paper
} else if (!blocked.contains(successor)) {
boolean gotCycle = findCyclesInSCG(startIndex, successorIndex, scg);
foundCycle = foundCycle || gotCycle;
}
}
if (foundCycle) {
unblock(vertex);
} else {
for (V w : scg.successors(vertex)) {
Set<V> bSet = getBSet(w);
bSet.add(vertex);
}
}
stack.pop();
return foundCycle;
}
private void unblock(V vertex)
{
blocked.remove(vertex);
Set<V> bSet = getBSet(vertex);
while (bSet.size() > 0) {
V w = bSet.iterator().next();
bSet.remove(w);
if (blocked.contains(w)) {
unblock(w);
}
}
}
@SuppressWarnings("unchecked")
private void initState(Consumer<List<V>> consumer)
{
cycleConsumer = consumer;
iToV = (V[]) graph.nodes().toArray();
vToI = new HashMap<>();
blocked = new HashSet<>();
bSets = new HashMap<>();
stack = new ArrayDeque<>();
for (int i = 0; i < iToV.length; i++) {
vToI.put(iToV[i], i);
}
}
private void clearState()
{
cycleConsumer = null;
iToV = null;
vToI = null;
blocked = null;
bSets = null;
stack = null;
}
private void initMinSCGState()
{
index = 0;
foundSCCs = new ArrayList<>();
vIndex = new HashMap<>();
vLowlink = new HashMap<>();
path = new ArrayDeque<>();
pathSet = new HashSet<>();
}
private void clearMinSCCState()
{
index = 0;
foundSCCs = null;
vIndex = null;
vLowlink = null;
path = null;
pathSet = null;
}
private Integer toI(V vertex)
{
return vToI.get(vertex);
}
private V toV(Integer i)
{
return iToV[i];
}
private Set<V> getBSet(V v)
{
// B sets typically not all needed,
// so instantiate lazily.
return bSets.computeIfAbsent(v, k -> new HashSet<>());
}
}

View File

@@ -0,0 +1,271 @@
package io.papermc.paper.plugin.entrypoint.strategy;
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.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 org.bukkit.plugin.UnknownDependencyException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
@SuppressWarnings("UnstableApiUsage")
public class LegacyPluginLoadingStrategy<T> implements ProviderLoadingStrategy<T> {
private static final Logger LOGGER = Logger.getLogger("LegacyPluginLoadingStrategy");
private final ProviderConfiguration<T> configuration;
public LegacyPluginLoadingStrategy(ProviderConfiguration<T> onLoad) {
this.configuration = onLoad;
}
@Override
public List<ProviderPair<T>> loadProviders(List<PluginProvider<T>> providers, MetaDependencyTree dependencyTree) {
List<ProviderPair<T>> javapluginsLoaded = new ArrayList<>();
MutableGraph<String> dependencyGraph = dependencyTree.getGraph();
Map<String, PluginProvider<T>> providersToLoad = new HashMap<>();
Set<String> loadedPlugins = new HashSet<>();
Map<String, String> pluginsProvided = new HashMap<>();
Map<String, Collection<String>> dependencies = new HashMap<>();
Map<String, Collection<String>> softDependencies = new HashMap<>();
for (PluginProvider<T> provider : providers) {
PluginMeta configuration = provider.getMeta();
PluginProvider<T> replacedProvider = providersToLoad.put(configuration.getName(), provider);
dependencyTree.addDirectDependency(configuration.getName()); // add to dependency tree
if (replacedProvider != null) {
LOGGER.severe(String.format(
"Ambiguous plugin name `%s' for files `%s' and `%s' in `%s'",
configuration.getName(),
provider.getSource(),
replacedProvider.getSource(),
replacedProvider.getParentSource()
));
}
String removedProvided = pluginsProvided.remove(configuration.getName());
if (removedProvided != null) {
LOGGER.warning(String.format(
"Ambiguous plugin name `%s'. It is also provided by `%s'",
configuration.getName(),
removedProvided
));
}
for (String provided : configuration.getProvidedPlugins()) {
PluginProvider<T> pluginProvider = providersToLoad.get(provided);
if (pluginProvider != null) {
LOGGER.warning(String.format(
"`%s provides `%s' while this is also the name of `%s' in `%s'",
provider.getSource(),
provided,
pluginProvider.getSource(),
provider.getParentSource()
));
} else {
String replacedPlugin = pluginsProvided.put(provided, configuration.getName());
dependencyTree.addDirectDependency(provided); // add to dependency tree
if (replacedPlugin != null) {
LOGGER.warning(String.format(
"`%s' is provided by both `%s' and `%s'",
provided,
configuration.getName(),
replacedPlugin
));
}
}
}
Collection<String> softDependencySet = provider.getMeta().getPluginSoftDependencies();
if (softDependencySet != null && !softDependencySet.isEmpty()) {
if (softDependencies.containsKey(configuration.getName())) {
// Duplicates do not matter, they will be removed together if applicable
softDependencies.get(configuration.getName()).addAll(softDependencySet);
} else {
softDependencies.put(configuration.getName(), new LinkedList<String>(softDependencySet));
}
for (String depend : softDependencySet) {
dependencyGraph.putEdge(configuration.getName(), depend);
}
}
Collection<String> dependencySet = provider.getMeta().getPluginDependencies();
if (dependencySet != null && !dependencySet.isEmpty()) {
dependencies.put(configuration.getName(), new LinkedList<String>(dependencySet));
for (String depend : dependencySet) {
dependencyGraph.putEdge(configuration.getName(), depend);
}
}
Collection<String> loadBeforeSet = provider.getMeta().getLoadBeforePlugins();
if (loadBeforeSet != null && !loadBeforeSet.isEmpty()) {
for (String loadBeforeTarget : loadBeforeSet) {
if (softDependencies.containsKey(loadBeforeTarget)) {
softDependencies.get(loadBeforeTarget).add(configuration.getName());
} else {
// softDependencies is never iterated, so 'ghost' plugins aren't an issue
Collection<String> shortSoftDependency = new LinkedList<String>();
shortSoftDependency.add(configuration.getName());
softDependencies.put(loadBeforeTarget, shortSoftDependency);
}
dependencyGraph.putEdge(loadBeforeTarget, configuration.getName());
}
}
}
while (!providersToLoad.isEmpty()) {
boolean missingDependency = true;
Iterator<Map.Entry<String, PluginProvider<T>>> providerIterator = providersToLoad.entrySet().iterator();
while (providerIterator.hasNext()) {
Map.Entry<String, PluginProvider<T>> entry = providerIterator.next();
String providerIdentifier = entry.getKey();
if (dependencies.containsKey(providerIdentifier)) {
Iterator<String> dependencyIterator = dependencies.get(providerIdentifier).iterator();
final Set<String> missingHardDependencies = new HashSet<>(dependencies.get(providerIdentifier).size()); // Paper - list all missing hard depends
while (dependencyIterator.hasNext()) {
String dependency = dependencyIterator.next();
// Dependency loaded
if (loadedPlugins.contains(dependency)) {
dependencyIterator.remove();
// We have a dependency not found
} else if (!providersToLoad.containsKey(dependency) && !pluginsProvided.containsKey(dependency)) {
// Paper start
missingHardDependencies.add(dependency);
}
}
if (!missingHardDependencies.isEmpty()) {
// Paper end
missingDependency = false;
providerIterator.remove();
pluginsProvided.values().removeIf(s -> s.equals(providerIdentifier)); // Paper - remove provided plugins
softDependencies.remove(providerIdentifier);
dependencies.remove(providerIdentifier);
LOGGER.log(
Level.SEVERE,
"Could not load '" + entry.getValue().getSource() + "' in folder '" + entry.getValue().getParentSource() + "'", // Paper
new UnknownDependencyException(missingHardDependencies, providerIdentifier)); // Paper
}
if (dependencies.containsKey(providerIdentifier) && dependencies.get(providerIdentifier).isEmpty()) {
dependencies.remove(providerIdentifier);
}
}
if (softDependencies.containsKey(providerIdentifier)) {
Iterator<String> softDependencyIterator = softDependencies.get(providerIdentifier).iterator();
while (softDependencyIterator.hasNext()) {
String softDependency = softDependencyIterator.next();
// Soft depend is no longer around
if (!providersToLoad.containsKey(softDependency) && !pluginsProvided.containsKey(softDependency)) {
softDependencyIterator.remove();
}
}
if (softDependencies.get(providerIdentifier).isEmpty()) {
softDependencies.remove(providerIdentifier);
}
}
if (!(dependencies.containsKey(providerIdentifier) || softDependencies.containsKey(providerIdentifier)) && providersToLoad.containsKey(providerIdentifier)) {
// We're clear to load, no more soft or hard dependencies left
PluginProvider<T> file = providersToLoad.get(providerIdentifier);
providerIterator.remove();
pluginsProvided.values().removeIf(s -> s.equals(providerIdentifier)); // Paper - remove provided plugins
missingDependency = false;
try {
this.configuration.applyContext(file, dependencyTree);
T loadedPlugin = file.createInstance();
this.warnIfPaperPlugin(file);
if (this.configuration.load(file, loadedPlugin)) {
loadedPlugins.add(file.getMeta().getName());
loadedPlugins.addAll(file.getMeta().getProvidedPlugins());
javapluginsLoaded.add(new ProviderPair<>(file, loadedPlugin));
}
} catch (Throwable ex) {
LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "'", ex); // Paper
}
}
}
if (missingDependency) {
// We now iterate over plugins until something loads
// This loop will ignore soft dependencies
providerIterator = providersToLoad.entrySet().iterator();
while (providerIterator.hasNext()) {
Map.Entry<String, PluginProvider<T>> entry = providerIterator.next();
String plugin = entry.getKey();
if (!dependencies.containsKey(plugin)) {
softDependencies.remove(plugin);
missingDependency = false;
PluginProvider<T> file = entry.getValue();
providerIterator.remove();
try {
this.configuration.applyContext(file, dependencyTree);
T loadedPlugin = file.createInstance();
this.warnIfPaperPlugin(file);
if (this.configuration.load(file, loadedPlugin)) {
loadedPlugins.add(file.getMeta().getName());
loadedPlugins.addAll(file.getMeta().getProvidedPlugins());
javapluginsLoaded.add(new ProviderPair<>(file, loadedPlugin));
}
break;
} catch (Throwable ex) {
LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "'", ex); // Paper
}
}
}
// We have no plugins left without a depend
if (missingDependency) {
softDependencies.clear();
dependencies.clear();
Iterator<PluginProvider<T>> failedPluginIterator = providersToLoad.values().iterator();
while (failedPluginIterator.hasNext()) {
PluginProvider<T> file = failedPluginIterator.next();
failedPluginIterator.remove();
LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "': circular dependency detected"); // Paper
}
}
}
}
return javapluginsLoaded;
}
private void warnIfPaperPlugin(PluginProvider<T> provider) {
if (provider instanceof PaperPluginParent.PaperServerPluginProvider) {
provider.getLogger().warn("Loading Paper plugin in the legacy plugin loading logic. This is not recommended and may introduce some differences into load order. It's highly recommended you move away from this if you are wanting to use Paper plugins.");
}
}
}

View File

@@ -0,0 +1,19 @@
package io.papermc.paper.plugin.entrypoint.strategy;
import java.util.List;
/**
* Indicates a dependency cycle within a provider loading sequence.
*/
public class PluginGraphCycleException extends RuntimeException {
private final List<List<String>> cycles;
public PluginGraphCycleException(List<List<String>> cycles) {
this.cycles = cycles;
}
public List<List<String>> getCycles() {
return this.cycles;
}
}

View File

@@ -0,0 +1,25 @@
package io.papermc.paper.plugin.entrypoint.strategy;
import io.papermc.paper.plugin.provider.PluginProvider;
import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
/**
* Used to share code with the modern and legacy plugin load strategy.
*
* @param <T>
*/
public interface ProviderConfiguration<T> {
void applyContext(PluginProvider<T> provider, DependencyContext dependencyContext);
boolean load(PluginProvider<T> provider, T provided);
default boolean preloadProvider(PluginProvider<T> provider) {
return true;
}
default void onGenericError(PluginProvider<T> provider) {
}
}

View File

@@ -0,0 +1,22 @@
package io.papermc.paper.plugin.entrypoint.strategy;
import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
import io.papermc.paper.plugin.provider.PluginProvider;
import java.util.List;
/**
* Used by a {@link io.papermc.paper.plugin.storage.SimpleProviderStorage} to load plugin providers in a certain order.
* <p>
* Returns providers loaded.
*
* @param <P> provider type
*/
public interface ProviderLoadingStrategy<P> {
List<ProviderPair<P>> loadProviders(List<PluginProvider<P>> providers, MetaDependencyTree dependencyTree);
record ProviderPair<P>(PluginProvider<P> provider, P provided) {
}
}

View File

@@ -0,0 +1,58 @@
package io.papermc.paper.plugin.entrypoint.strategy;
import com.google.common.graph.Graph;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
public final class TopographicGraphSorter {
// Topographically sort dependencies
public static <N> List<N> sortGraph(Graph<N> graph) throws PluginGraphCycleException {
List<N> sorted = new ArrayList<>();
Deque<N> roots = new ArrayDeque<>();
Object2IntMap<N> nonRoots = new Object2IntOpenHashMap<>();
for (N node : graph.nodes()) {
// Is a node being referred to by any other nodes?
int degree = graph.inDegree(node);
if (degree == 0) {
// Is a root
roots.add(node);
} else {
// Isn't a root, the number represents how many nodes connect to it.
nonRoots.put(node, degree);
}
}
// Pick from nodes that aren't referred to anywhere else
N next;
while ((next = roots.poll()) != null) {
for (N successor : graph.successors(next)) {
// Traverse through, moving down a degree
int newInDegree = nonRoots.removeInt(successor) - 1;
if (newInDegree == 0) {
roots.add(successor);
} else {
nonRoots.put(successor, newInDegree);
}
}
sorted.add(next);
}
if (!nonRoots.isEmpty()) {
throw new GraphCycleException();
}
return sorted;
}
public static final class GraphCycleException extends RuntimeException {
}
}

View File

@@ -0,0 +1,121 @@
package io.papermc.paper.plugin.entrypoint.strategy.modern;
import com.google.common.collect.Lists;
import com.google.common.graph.MutableGraph;
import com.mojang.logging.LogUtils;
import io.papermc.paper.plugin.configuration.PluginMeta;
import io.papermc.paper.plugin.entrypoint.strategy.JohnsonSimpleCycles;
import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException;
import io.papermc.paper.plugin.entrypoint.strategy.TopographicGraphSorter;
import io.papermc.paper.plugin.provider.PluginProvider;
import io.papermc.paper.plugin.provider.configuration.LoadOrderConfiguration;
import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta;
import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider;
import org.slf4j.Logger;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
class LoadOrderTree {
private static final Logger LOGGER = LogUtils.getClassLogger();
private final Map<String, PluginProvider<?>> providerMap;
private final MutableGraph<String> graph;
public LoadOrderTree(Map<String, PluginProvider<?>> providerMapMirror, MutableGraph<String> graph) {
this.providerMap = providerMapMirror;
this.graph = graph;
}
public void add(PluginProvider<?> provider) {
LoadOrderConfiguration configuration = provider.createConfiguration(this.providerMap);
// Build a validated provider's load order changes
String identifier = configuration.getMeta().getName();
for (String dependency : configuration.getLoadAfter()) {
if (this.providerMap.containsKey(dependency)) {
this.graph.putEdge(identifier, dependency);
}
}
for (String loadBeforeTarget : configuration.getLoadBefore()) {
if (this.providerMap.containsKey(loadBeforeTarget)) {
this.graph.putEdge(loadBeforeTarget, identifier);
}
}
this.graph.addNode(identifier); // Make sure load order has at least one node
}
public List<String> getLoadOrder() throws PluginGraphCycleException {
List<String> reversedTopographicSort;
try {
reversedTopographicSort = Lists.reverse(TopographicGraphSorter.sortGraph(this.graph));
} catch (TopographicGraphSorter.GraphCycleException exception) {
List<List<String>> cycles = new JohnsonSimpleCycles<>(this.graph).findAndRemoveSimpleCycles();
// Only log an error if at least non-Spigot plugin is present in the cycle
// Due to Spigot plugin metadata making no distinction between load order and dependencies (= class loader access), cycles are an unfortunate reality we have to deal with
Set<String> cyclingPlugins = new HashSet<>();
cycles.forEach(cyclingPlugins::addAll);
if (cyclingPlugins.stream().anyMatch(plugin -> {
PluginProvider<?> pluginProvider = this.providerMap.get(plugin);
return pluginProvider != null && !(pluginProvider instanceof SpigotPluginProvider);
})) {
logCycleError(cycles, this.providerMap);
}
// Try again after hopefully having removed all cycles
try {
reversedTopographicSort = Lists.reverse(TopographicGraphSorter.sortGraph(this.graph));
} catch (TopographicGraphSorter.GraphCycleException e) {
throw new PluginGraphCycleException(cycles);
}
}
return reversedTopographicSort;
}
private void logCycleError(List<List<String>> cycles, Map<String, PluginProvider<?>> providerMapMirror) {
LOGGER.error("=================================");
LOGGER.error("Circular plugin loading detected:");
for (int i = 0; i < cycles.size(); i++) {
List<String> cycle = cycles.get(i);
LOGGER.error("{}) {} -> {}", i + 1, String.join(" -> ", cycle), cycle.get(0));
for (String pluginName : cycle) {
PluginProvider<?> pluginProvider = providerMapMirror.get(pluginName);
if (pluginProvider == null) {
return;
}
logPluginInfo(pluginProvider.getMeta());
}
}
LOGGER.error("Please report this to the plugin authors of the first plugin of each loop or join the PaperMC Discord server for further help.");
LOGGER.error("=================================");
}
private void logPluginInfo(PluginMeta meta) {
if (!meta.getLoadBeforePlugins().isEmpty()) {
LOGGER.error(" {} loadbefore: {}", meta.getName(), meta.getLoadBeforePlugins());
}
if (meta instanceof PaperPluginMeta paperPluginMeta) {
if (!paperPluginMeta.getLoadAfterPlugins().isEmpty()) {
LOGGER.error(" {} loadafter: {}", meta.getName(), paperPluginMeta.getLoadAfterPlugins());
}
} else {
List<String> dependencies = new ArrayList<>();
dependencies.addAll(meta.getPluginDependencies());
dependencies.addAll(meta.getPluginSoftDependencies());
if (!dependencies.isEmpty()) {
LOGGER.error(" {} depend/softdepend: {}", meta.getName(), dependencies);
}
}
}
}

View File

@@ -0,0 +1,138 @@
package io.papermc.paper.plugin.entrypoint.strategy.modern;
import com.google.common.collect.Maps;
import com.google.common.graph.GraphBuilder;
import com.mojang.logging.LogUtils;
import io.papermc.paper.plugin.configuration.PluginMeta;
import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
import io.papermc.paper.plugin.entrypoint.dependency.MetaDependencyTree;
import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration;
import io.papermc.paper.plugin.entrypoint.strategy.ProviderLoadingStrategy;
import io.papermc.paper.plugin.provider.PluginProvider;
import org.bukkit.plugin.UnknownDependencyException;
import org.slf4j.Logger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@SuppressWarnings("UnstableApiUsage")
public class ModernPluginLoadingStrategy<T> implements ProviderLoadingStrategy<T> {
private static final Logger LOGGER = LogUtils.getClassLogger();
private final ProviderConfiguration<T> configuration;
public ModernPluginLoadingStrategy(ProviderConfiguration<T> onLoad) {
this.configuration = onLoad;
}
@Override
public List<ProviderPair<T>> loadProviders(List<PluginProvider<T>> pluginProviders, MetaDependencyTree dependencyTree) {
Map<String, PluginProviderEntry<T>> providerMap = new HashMap<>();
Map<String, PluginProvider<?>> providerMapMirror = Maps.transformValues(providerMap, (entry) -> entry.provider);
List<PluginProvider<T>> validatedProviders = new ArrayList<>();
// Populate provider map
for (PluginProvider<T> provider : pluginProviders) {
PluginMeta providerConfig = provider.getMeta();
PluginProviderEntry<T> entry = new PluginProviderEntry<>(provider);
PluginProviderEntry<T> replacedProvider = providerMap.put(providerConfig.getName(), entry);
if (replacedProvider != null) {
LOGGER.error(String.format(
"Ambiguous plugin name '%s' for files '%s' and '%s' in '%s'",
providerConfig.getName(),
provider.getSource(),
replacedProvider.provider.getSource(),
replacedProvider.provider.getParentSource()
));
this.configuration.onGenericError(replacedProvider.provider);
}
for (String extra : providerConfig.getProvidedPlugins()) {
PluginProviderEntry<T> replacedExtraProvider = providerMap.putIfAbsent(extra, entry);
if (replacedExtraProvider != null) {
LOGGER.warn(String.format(
"`%s' is provided by both `%s' and `%s'",
extra,
providerConfig.getName(),
replacedExtraProvider.provider.getMeta().getName()
));
}
}
}
// Populate dependency tree
for (PluginProvider<?> validated : pluginProviders) {
dependencyTree.add(validated);
}
// Validate providers, ensuring all of them have valid dependencies. Removing those who are invalid
for (PluginProvider<T> provider : pluginProviders) {
PluginMeta configuration = provider.getMeta();
// Populate missing dependencies to capture if there are multiple missing ones.
List<String> missingDependencies = provider.validateDependencies(dependencyTree);
if (missingDependencies.isEmpty()) {
validatedProviders.add(provider);
} else {
LOGGER.error("Could not load '%s' in '%s'".formatted(provider.getSource(), provider.getParentSource()), new UnknownDependencyException(missingDependencies, configuration.getName())); // Paper
// Because the validator is invalid, remove it from the provider map
providerMap.remove(configuration.getName());
// Cleanup plugins that failed to load
dependencyTree.remove(provider);
this.configuration.onGenericError(provider);
}
}
LoadOrderTree loadOrderTree = new LoadOrderTree(providerMapMirror, GraphBuilder.directed().build());
// Populate load order tree
for (PluginProvider<?> validated : validatedProviders) {
loadOrderTree.add(validated);
}
// Reverse the topographic search to let us see which providers we can load first.
List<String> reversedTopographicSort = loadOrderTree.getLoadOrder();
List<ProviderPair<T>> loadedPlugins = new ArrayList<>();
for (String providerIdentifier : reversedTopographicSort) {
// It's possible that this will be null because the above dependencies for soft/load before aren't validated if they exist.
// The graph could be MutableGraph<PluginProvider<T>>, but we would have to check if each dependency exists there... just
// nicer to do it here TBH.
PluginProviderEntry<T> retrievedProviderEntry = providerMap.get(providerIdentifier);
if (retrievedProviderEntry == null || retrievedProviderEntry.provided) {
// OR if this was already provided (most likely from a plugin that already "provides" that dependency)
// This won't matter since the provided plugin is loaded as a dependency, meaning it should have been loaded correctly anyways
continue; // Skip provider that doesn't exist....
}
retrievedProviderEntry.provided = true;
PluginProvider<T> retrievedProvider = retrievedProviderEntry.provider;
try {
this.configuration.applyContext(retrievedProvider, dependencyTree);
if (this.configuration.preloadProvider(retrievedProvider)) {
T instance = retrievedProvider.createInstance();
if (this.configuration.load(retrievedProvider, instance)) {
loadedPlugins.add(new ProviderPair<>(retrievedProvider, instance));
}
}
} catch (Throwable ex) {
LOGGER.error("Could not load plugin '%s' in folder '%s'".formatted(retrievedProvider.getFileName(), retrievedProvider.getParentSource()), ex); // Paper
}
}
return loadedPlugins;
}
private static class PluginProviderEntry<T> {
private final PluginProvider<T> provider;
private boolean provided;
private PluginProviderEntry(PluginProvider<T> provider) {
this.provider = provider;
}
}
}