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.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<Plugin> 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)
|
||||
|
||||
@@ -42,4 +42,6 @@ public @interface ConfigItem
|
||||
String description();
|
||||
|
||||
boolean hidden() default false;
|
||||
|
||||
String confirmationWarining() default "";
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ public class ConfigManager
|
||||
{
|
||||
List<Injector> injectors = new ArrayList<>();
|
||||
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<>();
|
||||
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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.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<Plugin> plugins = new ArrayList<>();
|
||||
@Inject
|
||||
PluginWatcher pluginWatcher;
|
||||
|
||||
public void loadPlugins() throws IOException
|
||||
private final List<Plugin> plugins = new CopyOnWriteArrayList<>();
|
||||
|
||||
public List<Plugin> loadCorePlugins() throws IOException
|
||||
{
|
||||
boolean developerPlugins = false;
|
||||
if (RuneLite.getOptions().has("developer-mode"))
|
||||
return scanAndInstantiate(getClass().getClassLoader(), PLUGIN_PACKAGE);
|
||||
}
|
||||
|
||||
public void startCorePlugins(List<Plugin> 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<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)
|
||||
{
|
||||
Class<?> clazz = classInfo.load();
|
||||
@@ -107,16 +137,88 @@ public class PluginManager
|
||||
Plugin plugin;
|
||||
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;
|
||||
}
|
||||
|
||||
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) ->
|
||||
{
|
||||
binder.bind((Class<Plugin>) 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<Plugin> getAllPlugins()
|
||||
void add(Plugin plugin)
|
||||
{
|
||||
return plugins;
|
||||
plugins.add(plugin);
|
||||
}
|
||||
|
||||
void remove(Plugin plugin)
|
||||
{
|
||||
plugins.remove(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get running plugins
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Collection<Plugin> getPlugins()
|
||||
{
|
||||
return manager.servicesByState().get(Service.State.RUNNING)
|
||||
.stream()
|
||||
.map(s -> (Plugin) s)
|
||||
.collect(Collectors.toList());
|
||||
return plugins;
|
||||
}
|
||||
|
||||
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.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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user