runelite-client: add support for dynamically loaded plugins
This commit is contained in:
@@ -43,6 +43,7 @@ import java.io.FileInputStream;
|
|||||||
import java.io.FileWriter;
|
import java.io.FileWriter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import javax.inject.Singleton;
|
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.SessionClose;
|
||||||
import net.runelite.client.events.SessionOpen;
|
import net.runelite.client.events.SessionOpen;
|
||||||
import net.runelite.client.menus.MenuManager;
|
import net.runelite.client.menus.MenuManager;
|
||||||
|
import net.runelite.client.plugins.Plugin;
|
||||||
import net.runelite.client.plugins.PluginManager;
|
import net.runelite.client.plugins.PluginManager;
|
||||||
import net.runelite.client.ui.ClientUI;
|
import net.runelite.client.ui.ClientUI;
|
||||||
import net.runelite.http.api.account.AccountClient;
|
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 RUNELITE_DIR = new File(System.getProperty("user.home"), ".runelite");
|
||||||
public static final File PROFILES_DIR = new File(RUNELITE_DIR, "profiles");
|
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 SESSION_FILE = new File(RUNELITE_DIR, "session");
|
||||||
|
public static final File PLUGIN_DIR = new File(RUNELITE_DIR, "plugins");
|
||||||
|
|
||||||
public static Image ICON;
|
public static Image ICON;
|
||||||
|
|
||||||
@@ -165,16 +168,20 @@ public class RuneLite
|
|||||||
|
|
||||||
// Load the plugins, but does not start them yet.
|
// Load the plugins, but does not start them yet.
|
||||||
// This will initialize configuration
|
// This will initialize configuration
|
||||||
pluginManager.loadPlugins();
|
List<Plugin> plugins = pluginManager.loadCorePlugins();
|
||||||
|
|
||||||
// Plugins have provided their config, so set default config
|
// Plugins have provided their config, so set default config
|
||||||
// to main settings
|
// to main settings
|
||||||
configManager.loadDefault();
|
configManager.loadDefault();
|
||||||
|
|
||||||
// Start plugins
|
// Start plugins
|
||||||
pluginManager.start();
|
pluginManager.startCorePlugins(plugins);
|
||||||
|
|
||||||
|
// Load the session, including saved configuration
|
||||||
loadSession();
|
loadSession();
|
||||||
|
|
||||||
|
// Begin watching for new plugins
|
||||||
|
pluginManager.watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTitle(String extra)
|
public void setTitle(String extra)
|
||||||
|
|||||||
@@ -42,4 +42,6 @@ public @interface ConfigItem
|
|||||||
String description();
|
String description();
|
||||||
|
|
||||||
boolean hidden() default false;
|
boolean hidden() default false;
|
||||||
|
|
||||||
|
String confirmationWarining() default "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ public class ConfigManager
|
|||||||
{
|
{
|
||||||
List<Injector> injectors = new ArrayList<>();
|
List<Injector> injectors = new ArrayList<>();
|
||||||
injectors.add(RuneLite.getInjector());
|
injectors.add(RuneLite.getInjector());
|
||||||
pluginManager.getAllPlugins().forEach(pl -> injectors.add(pl.getInjector()));
|
pluginManager.getPlugins().forEach(pl -> injectors.add(pl.getInjector()));
|
||||||
|
|
||||||
List<Config> list = new ArrayList<>();
|
List<Config> list = new ArrayList<>();
|
||||||
for (Injector injector : injectors)
|
for (Injector injector : injectors)
|
||||||
@@ -235,7 +235,7 @@ public class ConfigManager
|
|||||||
throw new RuntimeException("Non-public configuration classes can't have default methods invoked");
|
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
|
clazz
|
||||||
}, handler);
|
}, handler);
|
||||||
@@ -350,6 +350,10 @@ public class ConfigManager
|
|||||||
return new ConfigDescriptor(group, items);
|
return new ConfigDescriptor(group, items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the configuration from the default settings
|
||||||
|
* @param proxy
|
||||||
|
*/
|
||||||
private void setDefaultConfiguration(Object proxy)
|
private void setDefaultConfiguration(Object proxy)
|
||||||
{
|
{
|
||||||
Class<?> clazz = proxy.getClass().getInterfaces()[0];
|
Class<?> clazz = proxy.getClass().getInterfaces()[0];
|
||||||
|
|||||||
@@ -40,4 +40,17 @@ public interface RuneliteConfig extends Config
|
|||||||
{
|
{
|
||||||
return false;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,31 +24,29 @@
|
|||||||
*/
|
*/
|
||||||
package net.runelite.client.plugins;
|
package net.runelite.client.plugins;
|
||||||
|
|
||||||
import com.google.common.util.concurrent.AbstractIdleService;
|
|
||||||
import com.google.inject.Binder;
|
import com.google.inject.Binder;
|
||||||
import com.google.inject.Injector;
|
import com.google.inject.Injector;
|
||||||
import com.google.inject.Module;
|
import com.google.inject.Module;
|
||||||
|
import java.io.File;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
import javax.swing.SwingUtilities;
|
|
||||||
import net.runelite.client.ui.overlay.Overlay;
|
import net.runelite.client.ui.overlay.Overlay;
|
||||||
|
|
||||||
public abstract class Plugin extends AbstractIdleService implements Module
|
public abstract class Plugin implements Module
|
||||||
{
|
{
|
||||||
protected Injector injector;
|
protected Injector injector;
|
||||||
|
File file;
|
||||||
|
PluginClassLoader loader;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure(Binder binder)
|
public void configure(Binder binder)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void startUp() throws Exception
|
protected void startUp() throws Exception
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void shutDown() throws Exception
|
protected void shutDown() throws Exception
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -68,16 +66,4 @@ public abstract class Plugin extends AbstractIdleService implements Module
|
|||||||
Overlay overlay = getOverlay();
|
Overlay overlay = getOverlay();
|
||||||
return overlay != null ? Collections.singletonList(overlay) : Collections.EMPTY_LIST;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2016-2017, Adam <Adam@sigterm.info>
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2016-2017, Adam <Adam@sigterm.info>
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -28,20 +28,20 @@ import com.google.common.collect.ImmutableSet;
|
|||||||
import com.google.common.eventbus.EventBus;
|
import com.google.common.eventbus.EventBus;
|
||||||
import com.google.common.reflect.ClassPath;
|
import com.google.common.reflect.ClassPath;
|
||||||
import com.google.common.reflect.ClassPath.ClassInfo;
|
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.Binder;
|
||||||
|
import com.google.inject.CreationException;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Injector;
|
import com.google.inject.Injector;
|
||||||
import com.google.inject.Module;
|
import com.google.inject.Module;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
import net.runelite.client.RuneLite;
|
import net.runelite.client.RuneLite;
|
||||||
import net.runelite.client.task.Schedule;
|
import net.runelite.client.task.Schedule;
|
||||||
import net.runelite.client.task.ScheduledMethod;
|
import net.runelite.client.task.ScheduledMethod;
|
||||||
@@ -54,29 +54,59 @@ public class PluginManager
|
|||||||
{
|
{
|
||||||
private static final Logger logger = LoggerFactory.getLogger(PluginManager.class);
|
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";
|
private static final String PLUGIN_PACKAGE = "net.runelite.client.plugins";
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private EventBus eventBus;
|
EventBus eventBus;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
private Scheduler scheduler;
|
Scheduler scheduler;
|
||||||
|
|
||||||
private ServiceManager manager;
|
@Inject
|
||||||
private final List<Plugin> plugins = new ArrayList<>();
|
PluginWatcher pluginWatcher;
|
||||||
|
|
||||||
public void loadPlugins() throws IOException
|
private final List<Plugin> plugins = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
public List<Plugin> loadCorePlugins() throws IOException
|
||||||
{
|
{
|
||||||
boolean developerPlugins = false;
|
return scanAndInstantiate(getClass().getClassLoader(), PLUGIN_PACKAGE);
|
||||||
if (RuneLite.getOptions().has("developer-mode"))
|
}
|
||||||
|
|
||||||
|
public void startCorePlugins(List<Plugin> scannedPlugins)
|
||||||
|
{
|
||||||
|
for (Plugin plugin : scannedPlugins)
|
||||||
{
|
{
|
||||||
logger.info("Loading developer plugins");
|
try
|
||||||
developerPlugins = true;
|
{
|
||||||
|
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<ClassInfo> classes = classPath.getTopLevelClassesRecursive(PLUGIN_PACKAGE);
|
List<Plugin> scanAndInstantiate(ClassLoader classLoader, String packageName) throws IOException
|
||||||
|
{
|
||||||
|
boolean developerPlugins = RuneLite.getOptions().has("developer-mode");
|
||||||
|
|
||||||
|
List<Plugin> scannedPlugins = new ArrayList<>();
|
||||||
|
ClassPath classPath = ClassPath.from(classLoader);
|
||||||
|
|
||||||
|
ImmutableSet<ClassInfo> classes = packageName == null ? classPath.getAllClasses()
|
||||||
|
: classPath.getTopLevelClassesRecursive(packageName);
|
||||||
for (ClassInfo classInfo : classes)
|
for (ClassInfo classInfo : classes)
|
||||||
{
|
{
|
||||||
Class<?> clazz = classInfo.load();
|
Class<?> clazz = classInfo.load();
|
||||||
@@ -107,16 +137,88 @@ public class PluginManager
|
|||||||
Plugin plugin;
|
Plugin plugin;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
plugin = (Plugin) clazz.newInstance();
|
plugin = instantiate(pluginDescriptor, (Class<Plugin>) clazz);
|
||||||
}
|
}
|
||||||
catch (InstantiationException | IllegalAccessException ex)
|
catch (PluginInstantiationException ex)
|
||||||
{
|
{
|
||||||
logger.warn("error initializing plugin", ex);
|
logger.warn("error instantiating plugin!", ex);
|
||||||
continue;
|
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<Plugin> clazz) throws PluginInstantiationException
|
||||||
|
{
|
||||||
|
Plugin plugin;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
plugin = (Plugin) clazz.newInstance();
|
||||||
|
}
|
||||||
|
catch (InstantiationException | IllegalAccessException ex)
|
||||||
|
{
|
||||||
|
throw new PluginInstantiationException(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
Module pluginModule = (Binder binder) ->
|
Module pluginModule = (Binder binder) ->
|
||||||
{
|
{
|
||||||
binder.bind((Class<Plugin>) clazz).toInstance(plugin);
|
binder.bind((Class<Plugin>) clazz).toInstance(plugin);
|
||||||
@@ -125,78 +227,29 @@ public class PluginManager
|
|||||||
Injector pluginInjector = RuneLite.getInjector().createChildInjector(pluginModule);
|
Injector pluginInjector = RuneLite.getInjector().createChildInjector(pluginModule);
|
||||||
pluginInjector.injectMembers(plugin);
|
pluginInjector.injectMembers(plugin);
|
||||||
plugin.injector = pluginInjector;
|
plugin.injector = pluginInjector;
|
||||||
|
|
||||||
logger.debug("Loaded plugin {}", pluginDescriptor.name());
|
|
||||||
}
|
}
|
||||||
}
|
catch (CreationException ex)
|
||||||
|
|
||||||
public void start()
|
|
||||||
{
|
|
||||||
// Add plugin listeners
|
|
||||||
for (Plugin plugin : plugins)
|
|
||||||
{
|
{
|
||||||
Service.Listener listener = new Service.Listener()
|
throw new PluginInstantiationException(ex);
|
||||||
{
|
|
||||||
@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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
manager = new ServiceManager(plugins);
|
logger.debug("Loaded plugin {}", pluginDescriptor.name());
|
||||||
|
return plugin;
|
||||||
logger.debug("Starting plugins...");
|
|
||||||
manager.startAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
void add(Plugin plugin)
|
||||||
* Get all plugins regardless of state
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public Collection<Plugin> getAllPlugins()
|
|
||||||
{
|
{
|
||||||
return plugins;
|
plugins.add(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove(Plugin plugin)
|
||||||
|
{
|
||||||
|
plugins.remove(plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get running plugins
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public Collection<Plugin> getPlugins()
|
public Collection<Plugin> getPlugins()
|
||||||
{
|
{
|
||||||
return manager.servicesByState().get(Service.State.RUNNING)
|
return plugins;
|
||||||
.stream()
|
|
||||||
.map(s -> (Plugin) s)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void schedule(Plugin plugin)
|
private void schedule(Plugin plugin)
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2016-2017, Adam <Adam@sigterm.info>
|
||||||
|
* 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<Plugin> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -49,6 +49,10 @@ import javax.swing.JComponent;
|
|||||||
import javax.swing.JFormattedTextField;
|
import javax.swing.JFormattedTextField;
|
||||||
import javax.swing.JFrame;
|
import javax.swing.JFrame;
|
||||||
import javax.swing.JLabel;
|
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.JPanel;
|
||||||
import javax.swing.JScrollPane;
|
import javax.swing.JScrollPane;
|
||||||
import javax.swing.JSpinner;
|
import javax.swing.JSpinner;
|
||||||
@@ -58,6 +62,7 @@ import javax.swing.SpinnerNumberModel;
|
|||||||
import javax.swing.SwingConstants;
|
import javax.swing.SwingConstants;
|
||||||
import javax.swing.border.EmptyBorder;
|
import javax.swing.border.EmptyBorder;
|
||||||
import net.runelite.client.config.ConfigDescriptor;
|
import net.runelite.client.config.ConfigDescriptor;
|
||||||
|
import net.runelite.client.config.ConfigItem;
|
||||||
import net.runelite.client.config.ConfigItemDescriptor;
|
import net.runelite.client.config.ConfigItemDescriptor;
|
||||||
import net.runelite.client.config.ConfigManager;
|
import net.runelite.client.config.ConfigManager;
|
||||||
import net.runelite.client.ui.PluginPanel;
|
import net.runelite.client.ui.PluginPanel;
|
||||||
@@ -136,9 +141,23 @@ public class ConfigPanel extends PluginPanel
|
|||||||
|
|
||||||
private void changeConfiguration(JComponent component, ConfigDescriptor cd, ConfigItemDescriptor cid)
|
private void changeConfiguration(JComponent component, ConfigDescriptor cd, ConfigItemDescriptor cid)
|
||||||
{
|
{
|
||||||
|
ConfigItem configItem = cid.getItem();
|
||||||
|
|
||||||
if (component instanceof JCheckBox)
|
if (component instanceof JCheckBox)
|
||||||
{
|
{
|
||||||
JCheckBox checkbox = (JCheckBox) component;
|
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());
|
configManager.setConfiguration(cd.getGroup().keyName(), cid.getItem().keyName(), "" + checkbox.isSelected());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ public class PluginManagerTest
|
|||||||
public void testLoadPlugins() throws Exception
|
public void testLoadPlugins() throws Exception
|
||||||
{
|
{
|
||||||
PluginManager pluginManager = new PluginManager();
|
PluginManager pluginManager = new PluginManager();
|
||||||
pluginManager.loadPlugins();
|
pluginManager.loadCorePlugins();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -85,8 +85,8 @@ public class PluginManagerTest
|
|||||||
modules.add(new RuneliteModule());
|
modules.add(new RuneliteModule());
|
||||||
|
|
||||||
PluginManager pluginManager = new PluginManager();
|
PluginManager pluginManager = new PluginManager();
|
||||||
pluginManager.loadPlugins();
|
pluginManager.loadCorePlugins();
|
||||||
for (Plugin p : pluginManager.getAllPlugins())
|
for (Plugin p : pluginManager.getPlugins())
|
||||||
{
|
{
|
||||||
modules.add(p);
|
modules.add(p);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user