diff --git a/runelite-client/src/main/java/net/runelite/client/RuneLite.java b/runelite-client/src/main/java/net/runelite/client/RuneLite.java index 3b1cd1f257..d06da358d5 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -43,6 +43,7 @@ import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; +import java.util.List; import java.util.concurrent.ScheduledExecutorService; import javax.imageio.ImageIO; import javax.inject.Singleton; @@ -60,6 +61,7 @@ import net.runelite.client.config.ConfigManager; import net.runelite.client.events.SessionClose; import net.runelite.client.events.SessionOpen; import net.runelite.client.menus.MenuManager; +import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginManager; import net.runelite.client.ui.ClientUI; import net.runelite.http.api.account.AccountClient; @@ -75,6 +77,7 @@ public class RuneLite public static final File RUNELITE_DIR = new File(System.getProperty("user.home"), ".runelite"); public static final File PROFILES_DIR = new File(RUNELITE_DIR, "profiles"); public static final File SESSION_FILE = new File(RUNELITE_DIR, "session"); + public static final File PLUGIN_DIR = new File(RUNELITE_DIR, "plugins"); public static Image ICON; @@ -165,16 +168,20 @@ public class RuneLite // Load the plugins, but does not start them yet. // This will initialize configuration - pluginManager.loadPlugins(); + List plugins = pluginManager.loadCorePlugins(); // Plugins have provided their config, so set default config // to main settings configManager.loadDefault(); // Start plugins - pluginManager.start(); + pluginManager.startCorePlugins(plugins); + // Load the session, including saved configuration loadSession(); + + // Begin watching for new plugins + pluginManager.watch(); } public void setTitle(String extra) diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigItem.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigItem.java index af2fc2aeeb..c91ccbbb55 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigItem.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigItem.java @@ -42,4 +42,6 @@ public @interface ConfigItem String description(); boolean hidden() default false; + + String confirmationWarining() default ""; } diff --git a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java index 61709a12ae..921947dc80 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java +++ b/runelite-client/src/main/java/net/runelite/client/config/ConfigManager.java @@ -92,7 +92,7 @@ public class ConfigManager { List injectors = new ArrayList<>(); injectors.add(RuneLite.getInjector()); - pluginManager.getAllPlugins().forEach(pl -> injectors.add(pl.getInjector())); + pluginManager.getPlugins().forEach(pl -> injectors.add(pl.getInjector())); List list = new ArrayList<>(); for (Injector injector : injectors) @@ -235,7 +235,7 @@ public class ConfigManager throw new RuntimeException("Non-public configuration classes can't have default methods invoked"); } - T t = (T) Proxy.newProxyInstance(getClass().getClassLoader(), new Class[] + T t = (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz }, handler); @@ -350,6 +350,10 @@ public class ConfigManager return new ConfigDescriptor(group, items); } + /** + * Initialize the configuration from the default settings + * @param proxy + */ private void setDefaultConfiguration(Object proxy) { Class clazz = proxy.getClass().getInterfaces()[0]; diff --git a/runelite-client/src/main/java/net/runelite/client/config/RuneliteConfig.java b/runelite-client/src/main/java/net/runelite/client/config/RuneliteConfig.java index 27ba95c33e..e05cd77731 100644 --- a/runelite-client/src/main/java/net/runelite/client/config/RuneliteConfig.java +++ b/runelite-client/src/main/java/net/runelite/client/config/RuneliteConfig.java @@ -40,4 +40,17 @@ public interface RuneliteConfig extends Config { return false; } + + @ConfigItem( + keyName = "enablePlugins", + name = "Enable loading of external plugins", + description = "Enable loading of external plugins", + confirmationWarining = "WARNING: Using untrusted third party plugins is a SECURITY RISK\n" + + " and can result in loss of YOUR ACCOUNT, and compromise the security\n" + + "of your computer. Are you sure you want to do this?" + ) + default boolean enablePlugins() + { + return false; + } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/Plugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/Plugin.java index 9998b661fe..b30d174f02 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/Plugin.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/Plugin.java @@ -24,31 +24,29 @@ */ package net.runelite.client.plugins; -import com.google.common.util.concurrent.AbstractIdleService; import com.google.inject.Binder; import com.google.inject.Injector; import com.google.inject.Module; +import java.io.File; import java.util.Collection; import java.util.Collections; -import java.util.concurrent.Executor; -import javax.swing.SwingUtilities; import net.runelite.client.ui.overlay.Overlay; -public abstract class Plugin extends AbstractIdleService implements Module +public abstract class Plugin implements Module { protected Injector injector; + File file; + PluginClassLoader loader; @Override public void configure(Binder binder) { } - @Override protected void startUp() throws Exception { } - @Override protected void shutDown() throws Exception { } @@ -68,16 +66,4 @@ public abstract class Plugin extends AbstractIdleService implements Module Overlay overlay = getOverlay(); return overlay != null ? Collections.singletonList(overlay) : Collections.EMPTY_LIST; } - - /** - * Override AbstractIdleService's default executor to instead execute in - * the AWT event dispatch thread. - * - * @return - */ - @Override - protected Executor executor() - { - return r -> SwingUtilities.invokeLater(r); - } } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/PluginClassLoader.java b/runelite-client/src/main/java/net/runelite/client/plugins/PluginClassLoader.java new file mode 100644 index 0000000000..eab03e8a73 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/PluginClassLoader.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2016-2017, Adam + * 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 java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; + +/** + * A classloader for external plugins + * + * @author Adam + */ +public class PluginClassLoader extends URLClassLoader +{ + private final ClassLoader parent; + + public PluginClassLoader(File plugin, ClassLoader parent) throws MalformedURLException + { + super( + new URL[] + { + plugin.toURI().toURL() + }, + null // null or else class path scanning includes everything from the main class loader + ); + + this.parent = parent; + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException + { + try + { + return super.loadClass(name); + } + catch (ClassNotFoundException ex) + { + // fall back to main class loader + return parent.loadClass(name); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/PluginInstantiationException.java b/runelite-client/src/main/java/net/runelite/client/plugins/PluginInstantiationException.java new file mode 100644 index 0000000000..f4f9033392 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/PluginInstantiationException.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2016-2017, Adam + * 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; + +public class PluginInstantiationException extends Exception +{ + public PluginInstantiationException(Throwable cause) + { + super(cause); + } + +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java index 185412d39d..56900a8bde 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java @@ -28,20 +28,20 @@ import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.EventBus; import com.google.common.reflect.ClassPath; import com.google.common.reflect.ClassPath.ClassInfo; -import com.google.common.util.concurrent.MoreExecutors; -import com.google.common.util.concurrent.Service; -import com.google.common.util.concurrent.ServiceManager; import com.google.inject.Binder; +import com.google.inject.CreationException; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Module; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.stream.Collectors; +import java.util.concurrent.CopyOnWriteArrayList; import javax.inject.Singleton; +import javax.swing.SwingUtilities; import net.runelite.client.RuneLite; import net.runelite.client.task.Schedule; import net.runelite.client.task.ScheduledMethod; @@ -54,29 +54,59 @@ public class PluginManager { private static final Logger logger = LoggerFactory.getLogger(PluginManager.class); + /** + * Base package where the core plugins are + */ private static final String PLUGIN_PACKAGE = "net.runelite.client.plugins"; @Inject - private EventBus eventBus; + EventBus eventBus; @Inject - private Scheduler scheduler; + Scheduler scheduler; - private ServiceManager manager; - private final List plugins = new ArrayList<>(); + @Inject + PluginWatcher pluginWatcher; - public void loadPlugins() throws IOException + private final List plugins = new CopyOnWriteArrayList<>(); + + public List loadCorePlugins() throws IOException { - boolean developerPlugins = false; - if (RuneLite.getOptions().has("developer-mode")) + return scanAndInstantiate(getClass().getClassLoader(), PLUGIN_PACKAGE); + } + + public void startCorePlugins(List scannedPlugins) + { + for (Plugin plugin : scannedPlugins) { - logger.info("Loading developer plugins"); - developerPlugins = true; + try + { + startPlugin(plugin); + } + catch (PluginInstantiationException ex) + { + logger.warn("Unable to start plugin {}", plugin.getClass().getSimpleName(), ex); + continue; + } + + plugins.add(plugin); } + } - ClassPath classPath = ClassPath.from(getClass().getClassLoader()); + public void watch() + { + pluginWatcher.start(); + } - ImmutableSet classes = classPath.getTopLevelClassesRecursive(PLUGIN_PACKAGE); + List scanAndInstantiate(ClassLoader classLoader, String packageName) throws IOException + { + boolean developerPlugins = RuneLite.getOptions().has("developer-mode"); + + List scannedPlugins = new ArrayList<>(); + ClassPath classPath = ClassPath.from(classLoader); + + ImmutableSet classes = packageName == null ? classPath.getAllClasses() + : classPath.getTopLevelClassesRecursive(packageName); for (ClassInfo classInfo : classes) { Class clazz = classInfo.load(); @@ -107,16 +137,88 @@ public class PluginManager Plugin plugin; try { - plugin = (Plugin) clazz.newInstance(); + plugin = instantiate(pluginDescriptor, (Class) clazz); } - catch (InstantiationException | IllegalAccessException ex) + catch (PluginInstantiationException ex) { - logger.warn("error initializing plugin", ex); + logger.warn("error instantiating plugin!", ex); continue; } - plugins.add(plugin); + scannedPlugins.add(plugin); + } + return scannedPlugins; + } + + void startPlugin(Plugin plugin) throws PluginInstantiationException + { + try + { + // plugins always start in the event thread + SwingUtilities.invokeAndWait(() -> + { + try + { + plugin.startUp(); + } + catch (Exception ex) + { + throw new RuntimeException(ex); + } + }); + + logger.debug("Plugin {} is now running", plugin.getClass().getSimpleName()); + eventBus.register(plugin); + schedule(plugin); + } + catch (InterruptedException | InvocationTargetException ex) + { + throw new PluginInstantiationException(ex); + } + } + + void stopPlugin(Plugin plugin) throws PluginInstantiationException + { + try + { + unschedule(plugin); + eventBus.unregister(plugin); + + // plugins always stop in the event thread + SwingUtilities.invokeAndWait(() -> + { + try + { + plugin.shutDown(); + } + catch (Exception ex) + { + throw new RuntimeException(ex); + } + }); + + } + catch (InterruptedException | InvocationTargetException ex) + { + throw new PluginInstantiationException(ex); + } + } + + Plugin instantiate(PluginDescriptor pluginDescriptor, Class clazz) throws PluginInstantiationException + { + Plugin plugin; + try + { + plugin = (Plugin) clazz.newInstance(); + } + catch (InstantiationException | IllegalAccessException ex) + { + throw new PluginInstantiationException(ex); + } + + try + { Module pluginModule = (Binder binder) -> { binder.bind((Class) clazz).toInstance(plugin); @@ -125,78 +227,29 @@ public class PluginManager Injector pluginInjector = RuneLite.getInjector().createChildInjector(pluginModule); pluginInjector.injectMembers(plugin); plugin.injector = pluginInjector; - - logger.debug("Loaded plugin {}", pluginDescriptor.name()); } - } - - public void start() - { - // Add plugin listeners - for (Plugin plugin : plugins) + catch (CreationException ex) { - Service.Listener listener = new Service.Listener() - { - @Override - public void running() - { - logger.debug("Plugin {} is now running", plugin); - eventBus.register(plugin); - - schedule(plugin); - } - - @Override - public void stopping(Service.State from) - { - logger.debug("Plugin {} is stopping", plugin); - eventBus.unregister(plugin); - unschedule(plugin); - } - - @Override - public void failed(Service.State from, Throwable failure) - { - logger.warn("Plugin {} has failed", plugin, failure); - - if (from == Service.State.RUNNING) - { - eventBus.unregister(plugin); - unschedule(plugin); - } - } - }; - - plugin.addListener(listener, MoreExecutors.directExecutor()); + throw new PluginInstantiationException(ex); } - manager = new ServiceManager(plugins); - - logger.debug("Starting plugins..."); - manager.startAsync(); + logger.debug("Loaded plugin {}", pluginDescriptor.name()); + return plugin; } - /** - * Get all plugins regardless of state - * - * @return - */ - public Collection getAllPlugins() + void add(Plugin plugin) { - return plugins; + plugins.add(plugin); + } + + void remove(Plugin plugin) + { + plugins.remove(plugin); } - /** - * Get running plugins - * - * @return - */ public Collection getPlugins() { - return manager.servicesByState().get(Service.State.RUNNING) - .stream() - .map(s -> (Plugin) s) - .collect(Collectors.toList()); + return plugins; } private void schedule(Plugin plugin) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/PluginWatcher.java b/runelite-client/src/main/java/net/runelite/client/plugins/PluginWatcher.java new file mode 100644 index 0000000000..292f1c4327 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/PluginWatcher.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2016-2017, Adam + * 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 java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URLClassLoader; +import java.nio.file.FileSystems; +import java.nio.file.Path; +import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; +import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; +import java.nio.file.WatchEvent; +import java.nio.file.WatchEvent.Kind; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.util.List; +import javax.inject.Inject; +import javax.inject.Singleton; +import net.runelite.client.RuneLite; +import net.runelite.client.config.RuneliteConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public class PluginWatcher extends Thread +{ + private static final Logger logger = LoggerFactory.getLogger(PluginWatcher.class); + private static final File BASE = RuneLite.PLUGIN_DIR; + + private final RuneliteConfig runeliteConfig; + private final PluginManager pluginManager; + private final WatchService watchService; + private final WatchKey watchKey; + + @Inject + public PluginWatcher(RuneliteConfig runeliteConfig, PluginManager pluginManager) throws IOException + { + this.runeliteConfig = runeliteConfig; + this.pluginManager = pluginManager; + + setName("Plugin Watcher"); + setDaemon(true); + + watchService = FileSystems.getDefault().newWatchService(); + BASE.mkdirs(); + Path dir = BASE.toPath(); + watchKey = dir.register(watchService, ENTRY_MODIFY, ENTRY_DELETE); + } + + public void cancel() + { + watchKey.cancel(); + } + + @Override + public void run() + { + if (runeliteConfig.enablePlugins()) + { + scan(); + } + + for (;;) + { + try + { + WatchKey key = watchService.take(); + Thread.sleep(50); + + if (!runeliteConfig.enablePlugins()) + { + key.reset(); + continue; + } + + for (WatchEvent event : key.pollEvents()) + { + Kind kind = event.kind(); + Path path = (Path) event.context(); + File file = new File(BASE, path.toFile().getName()); + + logger.debug("Event {} file {}", kind, file); + + if (kind == ENTRY_MODIFY) + { + Plugin existing = findPluginForFile(file); + if (existing != null) + { + logger.info("Reloading plugin {}", file); + unload(existing); + } + else + { + logger.info("Loading plugin {}", file); + } + + load(file); + } + else if (kind == ENTRY_DELETE) + { + Plugin existing = findPluginForFile(file); + if (existing != null) + { + logger.info("Unloading plugin {}", file); + + unload(existing); + } + } + } + key.reset(); + + } + catch (InterruptedException ex) + { + logger.warn("error polling for plugins", ex); + } + } + } + + private void scan() + { + for (File file : BASE.listFiles()) + { + if (!file.getName().endsWith(".jar")) + { + continue; + } + + logger.info("Loading plugin from {}", file); + load(file); + } + } + + private Plugin findPluginForFile(File file) + { + for (Plugin plugin : pluginManager.getPlugins()) + { + if (plugin.file != null && plugin.file.equals(file)) + { + return plugin; + } + } + return null; + } + + private void load(File pluginFile) + { + PluginClassLoader loader; + try + { + loader = new PluginClassLoader(pluginFile, getClass().getClassLoader()); + } + catch (MalformedURLException ex) + { + logger.warn("Error loading plugin", ex); + return; + } + + List loadedPlugins; + try + { + loadedPlugins = pluginManager.scanAndInstantiate(loader, null); + } + catch (IOException ex) + { + close(loader); + logger.warn("Error loading plugin", ex); + return; + } + + if (loadedPlugins.isEmpty()) + { + close(loader); + logger.warn("No plugin found in plugin {}", pluginFile); + return; + } + + if (loadedPlugins.size() != 1) + { + close(loader); + logger.warn("You can not have more than one plugin per jar"); + return; + } + + Plugin plugin = loadedPlugins.get(0); + plugin.file = pluginFile; + plugin.loader = loader; + + try + { + pluginManager.startPlugin(plugin); + } + catch (PluginInstantiationException ex) + { + close(loader); + logger.warn("unable to start plugin", ex); + return; + } + + // Plugin is now running + pluginManager.add(plugin); + } + + private void unload(Plugin plugin) + { + try + { + pluginManager.stopPlugin(plugin); + } + catch (PluginInstantiationException ex) + { + logger.warn("unable to stop plugin", ex); + } + + pluginManager.remove(plugin); // remove it regardless + + close(plugin.loader); + } + + private void close(URLClassLoader classLoader) + { + try + { + classLoader.close(); + } + catch (IOException ex1) + { + logger.warn(null, ex1); + } + } + +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java b/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java index 4e545ac999..3864d92a78 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/config/ConfigPanel.java @@ -49,6 +49,10 @@ import javax.swing.JComponent; import javax.swing.JFormattedTextField; import javax.swing.JFrame; import javax.swing.JLabel; +import javax.swing.JOptionPane; +import static javax.swing.JOptionPane.WARNING_MESSAGE; +import static javax.swing.JOptionPane.YES_NO_OPTION; +import static javax.swing.JOptionPane.YES_OPTION; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSpinner; @@ -58,6 +62,7 @@ import javax.swing.SpinnerNumberModel; import javax.swing.SwingConstants; import javax.swing.border.EmptyBorder; import net.runelite.client.config.ConfigDescriptor; +import net.runelite.client.config.ConfigItem; import net.runelite.client.config.ConfigItemDescriptor; import net.runelite.client.config.ConfigManager; import net.runelite.client.ui.PluginPanel; @@ -136,9 +141,23 @@ public class ConfigPanel extends PluginPanel private void changeConfiguration(JComponent component, ConfigDescriptor cd, ConfigItemDescriptor cid) { + ConfigItem configItem = cid.getItem(); + if (component instanceof JCheckBox) { JCheckBox checkbox = (JCheckBox) component; + if (checkbox.isSelected() && !configItem.confirmationWarining().isEmpty()) + { + int value = JOptionPane.showOptionDialog(component, configItem.confirmationWarining(), + "Are you sure?", YES_NO_OPTION, WARNING_MESSAGE, + null, new String[] { "Yes", "No" }, "No"); + if (value != YES_OPTION) + { + checkbox.setSelected(false); + return; + } + } + configManager.setConfiguration(cd.getGroup().keyName(), cid.getItem().keyName(), "" + checkbox.isSelected()); } diff --git a/runelite-client/src/test/java/net/runelite/client/plugins/PluginManagerTest.java b/runelite-client/src/test/java/net/runelite/client/plugins/PluginManagerTest.java index d2514ac1b0..c8b2e15d10 100644 --- a/runelite-client/src/test/java/net/runelite/client/plugins/PluginManagerTest.java +++ b/runelite-client/src/test/java/net/runelite/client/plugins/PluginManagerTest.java @@ -74,7 +74,7 @@ public class PluginManagerTest public void testLoadPlugins() throws Exception { PluginManager pluginManager = new PluginManager(); - pluginManager.loadPlugins(); + pluginManager.loadCorePlugins(); } @Test @@ -85,8 +85,8 @@ public class PluginManagerTest modules.add(new RuneliteModule()); PluginManager pluginManager = new PluginManager(); - pluginManager.loadPlugins(); - for (Plugin p : pluginManager.getAllPlugins()) + pluginManager.loadCorePlugins(); + for (Plugin p : pluginManager.getPlugins()) { modules.add(p); }