diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 95f10bcbb0..76d4a31adf 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -423,18 +423,24 @@ private void loadPlugins() { try { Path pluginPath = Path.of("plugins"); + String updateFolderName = System.getProperty("velocity.update-folder-name", "update"); + Path updatePath = !updateFolderName.isEmpty() ? pluginPath.resolve(updateFolderName) : null; - if (!pluginPath.toFile().exists()) { + if (!Files.exists(pluginPath)) { 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); + if (updatePath != null && !Files.exists(updatePath)) { + Files.createDirectory(updatePath); } + + if (!Files.isDirectory(pluginPath)) { + logger.warn("Plugin location {} is not a directory, continuing without loading plugins", + pluginPath); + return; + } + + pluginManager.loadPlugins(pluginPath, updatePath); } 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..96cc0c01de 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java @@ -35,6 +35,7 @@ import com.velocitypowered.proxy.plugin.loader.VelocityPluginContainer; import com.velocitypowered.proxy.plugin.loader.java.JavaPluginLoader; import com.velocitypowered.proxy.plugin.util.PluginDependencyUtils; +import edu.umd.cs.findbugs.annotations.Nullable; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.IOException; import java.nio.file.DirectoryStream; @@ -82,20 +83,24 @@ public void registerPlugin(PluginContainer plugin) { /** * Loads all plugins from the specified {@code directory}. * - * @param directory the directory to load from + * @param pluginDirectory the directory to load from + * @param updateDirectory the directory to pull updated plugin files from * @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(Path pluginDirectory, @Nullable Path updateDirectory) throws IOException { + checkNotNull(pluginDirectory, "directory"); + checkArgument(Files.isDirectory(pluginDirectory), "provided path isn't a directory"); + if (updateDirectory != null) { + checkArgument(Files.isDirectory(updateDirectory), "provided path isn't a directory"); + } Map foundCandidates = new LinkedHashMap<>(); - JavaPluginLoader loader = new JavaPluginLoader(server, directory); + JavaPluginLoader loader = new JavaPluginLoader(server, pluginDirectory); - try (DirectoryStream stream = Files.newDirectoryStream(directory, - p -> p.toFile().isFile() && p.toString().endsWith(".jar"))) { + try (DirectoryStream stream = Files.newDirectoryStream(pluginDirectory, + p -> Files.isRegularFile(p) && p.toString().endsWith(".jar"))) { for (Path path : stream) { try { PluginDescription candidate = loader.loadCandidate(path); @@ -122,6 +127,11 @@ public void loadPlugins(Path directory) throws IOException { return; } + // Update plugins if an update folder is defined + if (updateDirectory != null) { + updatePlugins(pluginDirectory, updateDirectory, foundCandidates, loader); + } + List sortedPlugins = PluginDependencyUtils.sortCandidates( new ArrayList<>(foundCandidates.values())); @@ -182,6 +192,68 @@ protected void configure() { } } + private void updatePlugins( + Path pluginDirectory, + Path updateDirectory, + Map found, + JavaPluginLoader loader + ) throws IOException { + checkNotNull(updateDirectory, "updateDirectory"); + + List availableUpdates = new ArrayList<>(); + JavaPluginLoader updateLoader = new JavaPluginLoader(server, updateDirectory); + try ( + DirectoryStream stream = Files.newDirectoryStream(updateDirectory, path -> + Files.isRegularFile(path) && path.toString().endsWith(".jar")) + ) { + for (Path path : stream) { + try { + availableUpdates.add(updateLoader.loadCandidate(path)); + } catch (Exception e) { + logger.error("Unable to load plugin candidate {}", path, e); + } + } + } + + for (PluginDescription availableUpdate : availableUpdates) { + PluginDescription potentialMatch = found.get(availableUpdate.getId()); + + // This should not happen but acts as a failsafe + if (availableUpdate.getSource().isEmpty()) { + logger.warn("No source for plugin {} found", availableUpdate.getId()); + continue; + } + + if (potentialMatch != null) { + if (potentialMatch.getSource().isEmpty()) { + logger.warn("No source for plugin {} found, continuing without update.", + potentialMatch.getId()); + continue; + } + + // Remove outdated plugin file + try { + Files.deleteIfExists(potentialMatch.getSource().get()); + } catch (IOException e) { + logger.error("Unable to delete plugin {} from plugins folder at {}", + potentialMatch.getId(), pluginDirectory.toString(), e); + continue; + } + } + + Path newPath = pluginDirectory.resolve(availableUpdate.getSource().get().getFileName()); + try { + Files.move(availableUpdate.getSource().get(), newPath); + logger.info("Successfully updated plugin {} to version {}", + availableUpdate.getId(), availableUpdate.getVersion()); + PluginDescription movedDescription = loader.loadCandidate(newPath); + found.put(movedDescription.getId(), movedDescription); + } catch (Exception e) { + logger.error("Unable to update plugin {}", availableUpdate.getId(), e); + } + } + } + @Override public Optional fromInstance(Object instance) { checkNotNull(instance, "instance");