From a8c05a69fbc181d2c08f22b6e9e4d1104afe12b2 Mon Sep 17 00:00:00 2001 From: Park Date: Sat, 30 May 2026 03:51:03 +0900 Subject: [PATCH] Add CLI options to load plugins from extra jars and directories --- .../velocitypowered/proxy/ProxyOptions.java | 27 +++++++ .../velocitypowered/proxy/VelocityServer.java | 15 ++-- .../proxy/plugin/VelocityPluginManager.java | 70 +++++++++++-------- 3 files changed, 76 insertions(+), 36 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java b/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java index d0b7f34f24..fd1a15d4af 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java @@ -21,6 +21,7 @@ import com.velocitypowered.proxy.util.AddressUtil; import java.io.IOException; import java.net.InetSocketAddress; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; import joptsimple.OptionParser; @@ -28,6 +29,8 @@ import joptsimple.OptionSpec; import joptsimple.ValueConversionException; import joptsimple.ValueConverter; +import joptsimple.util.PathConverter; +import joptsimple.util.PathProperties; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.Nullable; @@ -43,6 +46,8 @@ public final class ProxyOptions { private final @Nullable Boolean haproxy; private final boolean ignoreConfigServers; private final List servers; + private final List extraPluginJars; + private final List extraPluginDirectories; ProxyOptions(final String[] args) { final OptionParser parser = new OptionParser(); @@ -64,6 +69,18 @@ public final class ProxyOptions { final OptionSpec ignoreConfigServers = parser.accepts("ignore-config-servers", "Skip registering servers from the config file. " + "Useful in dynamic setups or with the --add-server flag."); + final OptionSpec pluginFiles = parser.acceptsAll( + Arrays.asList("add-plugin", "add-extra-plugin"), + "Load an additional plugin from the specified jar file.") + .withRequiredArg() + .withValuesConvertedBy(new PathConverter(PathProperties.FILE_EXISTING, + PathProperties.READABLE)); + final OptionSpec pluginDirectories = parser.acceptsAll( + Arrays.asList("add-plugin-dir", "add-extra-plugin-dir"), + "Load plugins from an additional directory.") + .withRequiredArg() + .withValuesConvertedBy(new PathConverter(PathProperties.DIRECTORY_EXISTING, + PathProperties.READABLE)); final OptionSet set = parser.parse(args); this.help = set.has(help); @@ -71,6 +88,8 @@ public final class ProxyOptions { this.haproxy = haproxy.value(set); this.servers = servers.values(set); this.ignoreConfigServers = set.has(ignoreConfigServers); + this.extraPluginJars = pluginFiles.values(set); + this.extraPluginDirectories = pluginDirectories.values(set); if (this.help) { try { @@ -101,6 +120,14 @@ public List getServers() { return this.servers; } + public List getExtraPluginJars() { + return this.extraPluginJars; + } + + public List getExtraPluginDirectories() { + return this.extraPluginDirectories; + } + private static class ServerInfoConverter implements ValueConverter { @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 95f10bcbb0..3dd9c8e5ce 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -426,15 +426,14 @@ private void loadPlugins() { if (!pluginPath.toFile().exists()) { Files.createDirectory(pluginPath); - } else { - if (!pluginPath.toFile().isDirectory()) { - logger.warn("Plugin location {} is not a directory, continuing without loading plugins", - pluginPath); - return; - } - - pluginManager.loadPlugins(pluginPath); + } else if (!pluginPath.toFile().isDirectory()) { + logger.warn("Plugin location {} is not a directory, continuing without loading plugins", + pluginPath); + return; } + + pluginManager.loadPlugins(pluginPath, options.getExtraPluginDirectories(), + options.getExtraPluginJars()); } catch (Exception e) { logger.error("Couldn't load plugins", e); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java index 6bd0e00850..eb6b183060 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java @@ -80,40 +80,43 @@ public void registerPlugin(PluginContainer plugin) { } /** - * Loads all plugins from the specified {@code directory}. + * Loads all plugins found by scanning the given {@code directory} and + * {@code extraDirectories}, along with the individual plugin jars given in {@code extraJars}. * - * @param directory the directory to load from - * @throws IOException if we could not open the directory + * @param directory the main directory to scan for plugin jars + * @param extraDirectories additional directories to scan for plugin jars + * @param extraJars individual plugin jars to load + * @throws IOException if we could not open one of the directories */ - @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE", - justification = "I looked carefully and there's no way SpotBugs is right.") - public void loadPlugins(Path directory) throws IOException { - checkNotNull(directory, "directory"); - checkArgument(directory.toFile().isDirectory(), "provided path isn't a directory"); - + public void loadPlugins(Path directory, List extraDirectories, List extraJars) + throws IOException { Map foundCandidates = new LinkedHashMap<>(); JavaPluginLoader loader = new JavaPluginLoader(server, directory); - try (DirectoryStream stream = Files.newDirectoryStream(directory, - p -> p.toFile().isFile() && p.toString().endsWith(".jar"))) { - for (Path path : stream) { - try { - PluginDescription candidate = loader.loadCandidate(path); - - // If we found a duplicate candidate (with the same ID), don't load it. - PluginDescription maybeExistingCandidate = foundCandidates.putIfAbsent( - candidate.getId(), candidate); - - if (maybeExistingCandidate != null) { - logger.error("Refusing to load plugin at path {} since we already " - + "loaded a plugin with the same ID {} from {}", - candidate.getSource().map(Objects::toString).orElse(""), - candidate.getId(), - maybeExistingCandidate.getSource().map(Objects::toString).orElse("")); - } - } catch (Throwable e) { - logger.error("Unable to load plugin {}", path, e); + List candidates = new ArrayList<>(); + collectPluginJars(directory, candidates); + for (Path extraDirectory : extraDirectories) { + collectPluginJars(extraDirectory, candidates); + } + candidates.addAll(extraJars); + + for (Path path : candidates) { + try { + PluginDescription candidate = loader.loadCandidate(path); + + // If we found a duplicate candidate (with the same ID), don't load it. + PluginDescription maybeExistingCandidate = foundCandidates.putIfAbsent( + candidate.getId(), candidate); + + if (maybeExistingCandidate != null) { + logger.error("Refusing to load plugin at path {} since we already " + + "loaded a plugin with the same ID {} from {}", + candidate.getSource().map(Objects::toString).orElse(""), + candidate.getId(), + maybeExistingCandidate.getSource().map(Objects::toString).orElse("")); } + } catch (Throwable e) { + logger.error("Unable to load plugin {}", path, e); } } @@ -182,6 +185,17 @@ protected void configure() { } } + @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE", + justification = "I looked carefully and there's no way SpotBugs is right.") + private static void collectPluginJars(Path directory, List candidates) throws IOException { + try (DirectoryStream stream = Files.newDirectoryStream(directory, + p -> p.toFile().isFile() && p.toString().endsWith(".jar"))) { + for (Path path : stream) { + candidates.add(path); + } + } + } + @Override public Optional fromInstance(Object instance) { checkNotNull(instance, "instance");