Support multiple plugins loaded from the same JAR

This commit is contained in:
Andrew Steinborn
2021-05-15 23:42:54 -04:00
parent 3579fa644b
commit a155f91dfd
4 changed files with 62 additions and 51 deletions

View File

@@ -28,6 +28,7 @@ import com.velocitypowered.api.plugin.Plugin;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
@@ -56,9 +57,6 @@ import javax.tools.StandardLocation;
@SupportedAnnotationTypes({PLUGIN_ANNOTATION_CLASS, SUBSCRIBE_ANNOTATION_CLASS}) @SupportedAnnotationTypes({PLUGIN_ANNOTATION_CLASS, SUBSCRIBE_ANNOTATION_CLASS})
public class ApiAnnotationProcessor extends AbstractProcessor { public class ApiAnnotationProcessor extends AbstractProcessor {
private @Nullable String pluginClassFound;
private boolean warnedAboutMultiplePlugins;
@Override @Override
public SourceVersion getSupportedSourceVersion() { public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported(); return SourceVersion.latestSupported();
@@ -107,6 +105,7 @@ public class ApiAnnotationProcessor extends AbstractProcessor {
} }
if (ProcessorUtils.contains(annotations, Plugin.class)) { if (ProcessorUtils.contains(annotations, Plugin.class)) {
List<SerializedPluginDescription> found = new ArrayList<>();
for (Element element : roundEnv.getElementsAnnotatedWith(Plugin.class)) { for (Element element : roundEnv.getElementsAnnotatedWith(Plugin.class)) {
if (element.getKind() != ElementKind.CLASS) { if (element.getKind() != ElementKind.CLASS) {
processingEnv.getMessager() processingEnv.getMessager()
@@ -117,17 +116,6 @@ public class ApiAnnotationProcessor extends AbstractProcessor {
Name qualifiedName = ((TypeElement) element).getQualifiedName(); Name qualifiedName = ((TypeElement) element).getQualifiedName();
if (Objects.equals(pluginClassFound, qualifiedName.toString())) {
if (!warnedAboutMultiplePlugins) {
processingEnv.getMessager()
.printMessage(Diagnostic.Kind.WARNING,
"Velocity does not yet currently support multiple plugins. "
+ "We are using " + pluginClassFound + " for your plugin's main class.");
warnedAboutMultiplePlugins = true;
}
return false;
}
Plugin plugin = element.getAnnotation(Plugin.class); Plugin plugin = element.getAnnotation(Plugin.class);
if (!SerializedPluginDescription.ID_PATTERN.matcher(plugin.id()).matches()) { if (!SerializedPluginDescription.ID_PATTERN.matcher(plugin.id()).matches()) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
@@ -137,24 +125,25 @@ public class ApiAnnotationProcessor extends AbstractProcessor {
return false; return false;
} }
// All good, generate the velocity-plugin.json. // All good, generate the velocity-plugin-info.json.
SerializedPluginDescription description = SerializedPluginDescription SerializedPluginDescription description = SerializedPluginDescription
.from(plugin, qualifiedName.toString()); .from(plugin, qualifiedName.toString());
found.add(description);
}
try { try {
FileObject object = processingEnv.getFiler() FileObject object = processingEnv.getFiler()
.createResource(StandardLocation.CLASS_OUTPUT, "", "velocity-plugin.json"); .createResource(StandardLocation.CLASS_OUTPUT, "", "velocity-plugin-info.json");
try (Writer writer = new BufferedWriter(object.openWriter())) { try (Writer writer = new BufferedWriter(object.openWriter())) {
new Gson().toJson(description, writer); new Gson().toJson(found, writer);
} }
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
"Wrote velocity-plugin.json to " + object.toUri().toString()); "Wrote velocity-plugin-info.json to " + object.toUri().toString());
pluginClassFound = qualifiedName.toString();
} catch (IOException e) { } catch (IOException e) {
processingEnv.getMessager() processingEnv.getMessager()
.printMessage(Diagnostic.Kind.ERROR, "Unable to generate plugin file"); .printMessage(Diagnostic.Kind.ERROR, "Unable to generate plugin file");
} }
} }
}
return false; return false;
} }

View File

@@ -49,12 +49,10 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
@@ -100,7 +98,7 @@ public class VelocityPluginManager implements PluginManager {
p -> p.toFile().isFile() && p.toString().endsWith(".jar"))) { p -> p.toFile().isFile() && p.toString().endsWith(".jar"))) {
for (Path path : stream) { for (Path path : stream) {
try { try {
found.add(loader.loadPluginDescription(path)); found.addAll(loader.loadPluginCandidates(path));
} catch (Exception e) { } catch (Exception e) {
logger.error("Unable to load plugin {}", path, e); logger.error("Unable to load plugin {}", path, e);
} }
@@ -160,7 +158,7 @@ public class VelocityPluginManager implements PluginManager {
} }
try { try {
PluginDescription realPlugin = loader.loadPlugin(candidate); PluginDescription realPlugin = loader.materializePlugin(candidate);
VelocityPluginContainer container = new VelocityPluginContainer(realPlugin); VelocityPluginContainer container = new VelocityPluginContainer(realPlugin);
pluginContainers.put(container, loader.createModule(container)); pluginContainers.put(container, loader.createModule(container));
loadedPluginsById.put(candidate.id(), container); loadedPluginsById.put(candidate.id(), container);

View File

@@ -21,15 +21,16 @@ import com.google.inject.Module;
import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.PluginDescription;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collection;
/** /**
* This interface is used for loading plugins. * This interface is used for loading plugins.
*/ */
public interface PluginLoader { public interface PluginLoader {
PluginDescription loadPluginDescription(Path source) throws Exception; Collection<PluginDescription> loadPluginCandidates(Path source) throws Exception;
PluginDescription loadPlugin(PluginDescription source) throws Exception; PluginDescription materializePlugin(PluginDescription source) throws Exception;
/** /**
* Creates a {@link Module} for the provided {@link PluginContainer} * Creates a {@link Module} for the provided {@link PluginContainer}

View File

@@ -31,16 +31,23 @@ import com.velocitypowered.proxy.plugin.PluginClassLoader;
import com.velocitypowered.proxy.plugin.loader.PluginLoader; import com.velocitypowered.proxy.plugin.loader.PluginLoader;
import com.velocitypowered.proxy.plugin.loader.VelocityPluginContainer; import com.velocitypowered.proxy.plugin.loader.VelocityPluginContainer;
import com.velocitypowered.proxy.plugin.loader.VelocityPluginDescription; import com.velocitypowered.proxy.plugin.loader.VelocityPluginDescription;
import io.leangen.geantyref.TypeToken;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.io.Reader; import java.io.Reader;
import java.net.URI;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.security.AccessController; import java.security.AccessController;
import java.security.PrivilegedAction; import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarInputStream; import java.util.jar.JarInputStream;
@@ -49,27 +56,28 @@ import org.checkerframework.checker.nullness.qual.Nullable;
public class JavaPluginLoader implements PluginLoader { public class JavaPluginLoader implements PluginLoader {
private final Path baseDirectory; private final Path baseDirectory;
private final Map<URI, PluginClassLoader> classLoaders = new HashMap<>();
public JavaPluginLoader(ProxyServer server, Path baseDirectory) { public JavaPluginLoader(ProxyServer server, Path baseDirectory) {
this.baseDirectory = baseDirectory; this.baseDirectory = baseDirectory;
} }
@Override @Override
public PluginDescription loadPluginDescription(Path source) throws Exception { public Collection<PluginDescription> loadPluginCandidates(Path source) throws Exception {
SerializedPluginDescription serialized = getSerializedPluginInfo(source); List<SerializedPluginDescription> serialized = getSerializedPluginInfo(source);
if (serialized == null) { if (serialized.isEmpty()) {
throw new InvalidPluginException("Did not find a valid velocity-plugin.json."); throw new InvalidPluginException("Did not find a valid velocity-plugin-info.json.");
} }
if (!SerializedPluginDescription.ID_PATTERN.matcher(serialized.getId()).matches()) { List<PluginDescription> candidates = new ArrayList<>();
throw new InvalidPluginException("Plugin ID '" + serialized.getId() + "' is invalid."); for (SerializedPluginDescription description : serialized) {
candidates.add(createCandidateDescription(description, source));
} }
return candidates;
return createCandidateDescription(serialized, source);
} }
@Override @Override
public PluginDescription loadPlugin(PluginDescription source) throws Exception { public PluginDescription materializePlugin(PluginDescription source) throws Exception {
if (!(source instanceof JavaVelocityPluginDescriptionCandidate)) { if (!(source instanceof JavaVelocityPluginDescriptionCandidate)) {
throw new IllegalArgumentException("Description provided isn't of the Java plugin loader"); throw new IllegalArgumentException("Description provided isn't of the Java plugin loader");
} }
@@ -79,11 +87,15 @@ public class JavaPluginLoader implements PluginLoader {
throw new IllegalStateException("JAR path not provided."); throw new IllegalStateException("JAR path not provided.");
} }
URL pluginJarUrl = jarFilePath.toUri().toURL(); URI pluginJarUri = jarFilePath.toUri();
PluginClassLoader loader = AccessController.doPrivileged( URL pluginJarUrl = pluginJarUri.toURL();
PluginClassLoader loader = this.classLoaders.computeIfAbsent(pluginJarUri, (uri) -> {
PluginClassLoader classLoader = AccessController.doPrivileged(
(PrivilegedAction<PluginClassLoader>) () -> new PluginClassLoader(new URL[]{pluginJarUrl}, (PrivilegedAction<PluginClassLoader>) () -> new PluginClassLoader(new URL[]{pluginJarUrl},
source)); source));
loader.addToClassloaders(); classLoader.addToClassloaders();
return classLoader;
});
JavaVelocityPluginDescriptionCandidate candidate = JavaVelocityPluginDescriptionCandidate candidate =
(JavaVelocityPluginDescriptionCandidate) source; (JavaVelocityPluginDescriptionCandidate) source;
@@ -130,32 +142,43 @@ public class JavaPluginLoader implements PluginLoader {
((VelocityPluginContainer) container).setInstance(instance); ((VelocityPluginContainer) container).setInstance(instance);
} }
private @Nullable SerializedPluginDescription getSerializedPluginInfo(Path source) private List<SerializedPluginDescription> getSerializedPluginInfo(Path source)
throws Exception { throws Exception {
boolean foundOldVelocityPlugin = false;
boolean foundBungeeBukkitPluginFile = false; boolean foundBungeeBukkitPluginFile = false;
try (JarInputStream in = new JarInputStream( try (JarInputStream in = new JarInputStream(
new BufferedInputStream(Files.newInputStream(source)))) { new BufferedInputStream(Files.newInputStream(source)))) {
JarEntry entry; JarEntry entry;
while ((entry = in.getNextJarEntry()) != null) { while ((entry = in.getNextJarEntry()) != null) {
if (entry.getName().equals("velocity-plugin.json")) { if (entry.getName().equals("velocity-plugin-info.json")) {
try (Reader pluginInfoReader = new InputStreamReader(in, StandardCharsets.UTF_8)) { try (Reader pluginInfoReader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
return VelocityServer.GENERAL_GSON.fromJson(pluginInfoReader, return VelocityServer.GENERAL_GSON.fromJson(pluginInfoReader,
SerializedPluginDescription.class); new TypeToken<List<SerializedPluginDescription>>() {}.getType());
} }
} }
if (entry.getName().equals("velocity-plugin.json")) {
foundOldVelocityPlugin = true;
}
if (entry.getName().equals("plugin.yml") || entry.getName().equals("bungee.yml")) { if (entry.getName().equals("plugin.yml") || entry.getName().equals("bungee.yml")) {
foundBungeeBukkitPluginFile = true; foundBungeeBukkitPluginFile = true;
} }
} }
if (foundOldVelocityPlugin) {
throw new InvalidPluginException("The plugin file " + source.getFileName() + " appears to "
+ "be developed for an older version of Velocity. Please obtain a newer version of the "
+ "plugin.");
}
if (foundBungeeBukkitPluginFile) { if (foundBungeeBukkitPluginFile) {
throw new InvalidPluginException("The plugin file " + source.getFileName() + " appears to " throw new InvalidPluginException("The plugin file " + source.getFileName() + " appears to "
+ "be a Bukkit or BungeeCord plugin. Velocity does not support Bukkit or BungeeCord " + "be a Bukkit or BungeeCord plugin. Velocity does not support Bukkit or BungeeCord "
+ "plugins."); + "plugins.");
} }
return null; return List.of();
} }
} }