diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/MissingDependenciesException.java b/runelite-client/src/main/java/net/runelite/client/plugins/MissingDependenciesException.java new file mode 100644 index 0000000000..2e80e0df2e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/MissingDependenciesException.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021, ThatGamerBlue + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.runelite.client.plugins; + +import com.google.common.collect.Multimap; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.pf4j.DependencyResolver; + +@Value +@EqualsAndHashCode(callSuper = true) +public class MissingDependenciesException extends DependencyResolver.DependenciesNotFoundException +{ + Multimap reverseDependencyMap; + + public MissingDependenciesException(List dependencies, Multimap reverseDependencyMap) + { + super(dependencies); + this.reverseDependencyMap = reverseDependencyMap; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/OPRSExternalPf4jPluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/OPRSExternalPf4jPluginManager.java index cbbe334f48..4c6ec404fe 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/OPRSExternalPf4jPluginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/OPRSExternalPf4jPluginManager.java @@ -1,5 +1,7 @@ package net.runelite.client.plugins; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; import com.openosrs.client.OpenOSRS; import java.io.Closeable; import java.io.IOException; @@ -25,6 +27,8 @@ import org.pf4j.JarPluginLoader; import org.pf4j.JarPluginRepository; import org.pf4j.ManifestPluginDescriptorFinder; import org.pf4j.PluginAlreadyLoadedException; +import org.pf4j.PluginDependency; +import org.pf4j.PluginDescriptor; import org.pf4j.PluginDescriptorFinder; import org.pf4j.PluginLoader; import org.pf4j.PluginRepository; @@ -37,6 +41,8 @@ import org.pf4j.RuntimeMode; @Slf4j class OPRSExternalPf4jPluginManager extends DefaultPluginManager { + private final Set disabledPlugins = new HashSet<>(); + public OPRSExternalPf4jPluginManager() { super(OpenOSRS.EXTERNALPLUGIN_DIR.toPath()); @@ -186,15 +192,26 @@ class OPRSExternalPf4jPluginManager extends DefaultPluginManager { // retrieves the plugins descriptors List descriptors = new ArrayList<>(); + Multimap reverseDepMap = MultimapBuilder.hashKeys().hashSetValues().build(); for (PluginWrapper plugin : plugins.values()) { descriptors.add(plugin.getDescriptor()); + + for (PluginDependency dependency : plugin.getDescriptor().getDependencies()) + { + reverseDepMap.put(dependency.getPluginId(), plugin.getPluginId()); + } } // retrieves the plugins descriptors from the resolvedPlugins list. This allows to load plugins that have already loaded dependencies. for (PluginWrapper plugin : resolvedPlugins) { descriptors.add(plugin.getDescriptor()); + + for (PluginDependency dependency : plugin.getDescriptor().getDependencies()) + { + reverseDepMap.put(dependency.getPluginId(), plugin.getPluginId()); + } } DependencyResolver.Result result = dependencyResolver.resolve(descriptors); @@ -207,7 +224,7 @@ class OPRSExternalPf4jPluginManager extends DefaultPluginManager List notFoundDependencies = result.getNotFoundDependencies(); if (!notFoundDependencies.isEmpty()) { - throw new DependencyResolver.DependenciesNotFoundException(notFoundDependencies); + throw new MissingDependenciesException(notFoundDependencies, reverseDepMap); } List wrongVersionDependencies = result.getWrongVersionDependencies(); @@ -366,6 +383,92 @@ class OPRSExternalPf4jPluginManager extends DefaultPluginManager return pluginRepository.deletePluginPath(pluginPath); } + @Override + protected PluginWrapper loadPluginFromPath(Path pluginPath) + { + // Test for plugin path duplication + String pluginId = idForPath(pluginPath); + if (pluginId != null) + { + throw new PluginAlreadyLoadedException(pluginId, pluginPath); + } + + // Retrieve and validate the plugin descriptor + PluginDescriptorFinder pluginDescriptorFinder = getPluginDescriptorFinder(); + log.debug("Use '{}' to find plugins descriptors", pluginDescriptorFinder); + log.debug("Finding plugin descriptor for plugin '{}'", pluginPath); + PluginDescriptor pluginDescriptor = pluginDescriptorFinder.find(pluginPath); + validatePluginDescriptor(pluginDescriptor); + + // OPRS START - don't load plugins that failed dependency resolution + if (disabledPlugins.contains(pluginDescriptor.getPluginId())) + { + log.debug("Skipping loading {}, was previously disabled.", pluginDescriptor.getPluginId()); + return null; + } + // OPRS END + + // Check there are no loaded plugins with the retrieved id + pluginId = pluginDescriptor.getPluginId(); + if (plugins.containsKey(pluginId)) + { + PluginWrapper loadedPlugin = getPlugin(pluginId); + throw new PluginRuntimeException("There is an already loaded plugin ({}) " + + "with the same id ({}) as the plugin at path '{}'. Simultaneous loading " + + "of plugins with the same PluginId is not currently supported.\n" + + "As a workaround you may include PluginVersion and PluginProvider " + + "in PluginId.", + loadedPlugin, pluginId, pluginPath); + } + + log.debug("Found descriptor {}", pluginDescriptor); + String pluginClassName = pluginDescriptor.getPluginClass(); + log.debug("Class '{}' for plugin '{}'", pluginClassName, pluginPath); + + // load plugin + log.debug("Loading plugin '{}'", pluginPath); + ClassLoader pluginClassLoader = getPluginLoader().loadPlugin(pluginPath, pluginDescriptor); + log.debug("Loaded plugin '{}' with class loader '{}'", pluginPath, pluginClassLoader); + + // create the plugin wrapper + log.debug("Creating wrapper for plugin '{}'", pluginPath); + PluginWrapper pluginWrapper = new PluginWrapper(this, pluginDescriptor, pluginPath, pluginClassLoader); + pluginWrapper.setPluginFactory(getPluginFactory()); + + // test for disabled plugin + if (isPluginDisabled(pluginDescriptor.getPluginId())) + { + log.info("Plugin '{}' is disabled", pluginPath); + pluginWrapper.setPluginState(PluginState.DISABLED); + } + + // validate the plugin + if (!isPluginValid(pluginWrapper)) + { + log.warn("Plugin '{}' is invalid and it will be disabled", pluginPath); + pluginWrapper.setPluginState(PluginState.DISABLED); + } + + log.debug("Created wrapper '{}' for plugin '{}'", pluginWrapper, pluginPath); + + pluginId = pluginDescriptor.getPluginId(); + + // add plugin to the list with plugins + plugins.put(pluginId, pluginWrapper); + getUnresolvedPlugins().add(pluginWrapper); + + // add plugin class loader to the list with class loaders + getPluginClassLoaders().put(pluginId, pluginClassLoader); + + return pluginWrapper; + } + + void disableLoading(String pluginId) + { + unloadPlugin(pluginId); + disabledPlugins.add(pluginId); + } + private boolean isPluginEligibleForLoading(Path path) { return path.toFile().getName().endsWith(".jar"); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/OPRSExternalPluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/OPRSExternalPluginManager.java index a4e7a1879b..98b1cb2424 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/OPRSExternalPluginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/OPRSExternalPluginManager.java @@ -25,6 +25,7 @@ package net.runelite.client.plugins; import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; import com.google.common.graph.GraphBuilder; import com.google.common.graph.Graphs; import com.google.common.graph.MutableGraph; @@ -205,15 +206,25 @@ public class OPRSExternalPluginManager } catch (Exception ex) { - if (ex instanceof DependencyResolver.DependenciesNotFoundException) + if (ex instanceof MissingDependenciesException) { - List deps = ((DependencyResolver.DependenciesNotFoundException) ex).getDependencies(); + List deps = ((MissingDependenciesException) ex).getDependencies(); + Multimap reverseDepMap = ((MissingDependenciesException) ex).getReverseDependencyMap(); - log.error("The following dependencies are missing: {}", deps); - - for (String dep : deps) + for (String dependency : deps) { - updateManager.installPlugin(dep, null); + Collection dependentPlugins = reverseDepMap.get(dependency); + + log.error("Dependency {} is missing, but is required by {}, attempting install.", dependency, dependentPlugins); + try + { + updateManager.installPlugin(dependency, null); + } + catch (PluginRuntimeException ex2) + { + log.error("Dependency {} is missing and couldn't be installed. Disabling loading of {} as they depend on it.", dependency, dependentPlugins); + dependentPlugins.forEach(s -> ((OPRSExternalPf4jPluginManager) externalPluginManager).disableLoading(s)); + } } startExternalPluginManager();