@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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 + '}';
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<>());
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user