From 8c506842eb0c608a9be832763f230810a07b1b0f Mon Sep 17 00:00:00 2001 From: theminecoder Date: Thu, 1 Jan 2026 21:01:20 +1100 Subject: [PATCH] Implement loading of extra plugin directories --- .../velocitypowered/proxy/ProxyOptions.java | 14 ++++ .../velocitypowered/proxy/VelocityServer.java | 19 +++--- .../proxy/plugin/VelocityPluginManager.java | 66 ++++++++++++------- 3 files changed, 66 insertions(+), 33 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..cecc6b57f9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java @@ -19,6 +19,7 @@ import com.velocitypowered.api.proxy.server.ServerInfo; import com.velocitypowered.proxy.util.AddressUtil; +import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.util.Arrays; @@ -43,6 +44,7 @@ public final class ProxyOptions { private final @Nullable Boolean haproxy; private final boolean ignoreConfigServers; private final List servers; + private final List extraPluginDirectories; ProxyOptions(final String[] args) { final OptionParser parser = new OptionParser(); @@ -64,6 +66,13 @@ 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 extraPluginDirectories = parser.acceptsAll(List.of("add-plugin-dir", "add-extra-plugin-dir"), + "Specify paths to extra plugin directories to be loaded in addition to the plugins folder. " + + "This argument can be specified multiple times, once for each extra plugin dir path.") + .withRequiredArg() + .ofType(File.class) + .defaultsTo(new File[] {}) + .describedAs("Plugin directory"); final OptionSet set = parser.parse(args); this.help = set.has(help); @@ -71,6 +80,7 @@ public final class ProxyOptions { this.haproxy = haproxy.value(set); this.servers = servers.values(set); this.ignoreConfigServers = set.has(ignoreConfigServers); + this.extraPluginDirectories = extraPluginDirectories.values(set); if (this.help) { try { @@ -101,6 +111,10 @@ public List getServers() { return this.servers; } + 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 ccfc5f1409..1c6424958c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -74,6 +74,7 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.channel.EventLoopGroup; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; @@ -86,6 +87,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -305,7 +307,7 @@ void start() { ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit()); commandRateLimiter = Ratelimiters.createWithMilliseconds(configuration.getCommandRatelimit()); tabCompleteRateLimiter = Ratelimiters.createWithMilliseconds(configuration.getTabCompleteRatelimit()); - loadPlugins(); + loadPlugins(options.getExtraPluginDirectories()); // Go ahead and fire the proxy initialization event. We block since plugins should have a chance // to fully initialize before we accept any connections to the server. @@ -419,7 +421,7 @@ private void doStartupConfigLoad() { } } - private void loadPlugins() { + private void loadPlugins(List extraPluginDirectories) { logger.info("Loading plugins..."); try { @@ -427,15 +429,12 @@ 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); } + + List pluginDirectories = new ArrayList<>(); + pluginDirectories.add(pluginPath); + pluginDirectories.addAll(extraPluginDirectories.stream().map(File::toPath).toList()); + pluginManager.loadPlugins(pluginDirectories, pluginPath); } 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..f3660e9924 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java @@ -85,34 +85,54 @@ public void registerPlugin(PluginContainer plugin) { * @param directory the directory to load from * @throws IOException if we could not open the directory */ + @Deprecated + public void loadPlugins(Path directory) throws IOException { + loadPlugins(Collections.singletonList(directory), directory); + } + + /** + * Loads all plugins from the specified {@code directories}. + * + * @param directories the directories to load from + * @param baseDirectory the base directory to give plugins + * @throws IOException if we could not open the directory + */ @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(List directories, Path baseDirectory) throws IOException { + checkNotNull(directories, "directories"); + checkArgument(!directories.isEmpty(), "no directories provided"); + checkArgument(!directories.contains(null), "null directories provided"); + checkArgument(directories.contains(baseDirectory), "base directory not provided in directories search path"); + checkArgument(baseDirectory.toFile().isDirectory(), "provided baseDirectory path isn't a directory"); 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("")); + JavaPluginLoader loader = new JavaPluginLoader(server, baseDirectory); + + for (Path directory : directories) { + if (!directory.toFile().isDirectory()) { + continue; + } + + 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); } - } catch (Throwable e) { - logger.error("Unable to load plugin {}", path, e); } } }