diff --git a/pom.xml b/pom.xml index 5d9386effe..2122c9f537 100644 --- a/pom.xml +++ b/pom.xml @@ -42,7 +42,7 @@ true true - + true 179 @@ -120,6 +120,7 @@ runelite-mixins runelite-script-assembler-plugin runescape-api + runelite-plugin-archetype http-api http-service protocol-api @@ -151,6 +152,13 @@ pom import + + org.apache.maven.archetype + archetype-packaging + 3.0.1 + pom + import + 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 ba3169020d..74594a8ed4 100644 --- a/runelite-client/src/main/java/net/runelite/client/RuneLite.java +++ b/runelite-client/src/main/java/net/runelite/client/RuneLite.java @@ -77,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 PLUGIN_DIR = new File(RUNELITE_DIR, "plugins"); public static final File SCREENSHOT_DIR = new File(RUNELITE_DIR, "screenshots"); @Getter @@ -243,6 +244,9 @@ public class RuneLite // Load the session, including saved configuration sessionManager.loadSession(); + // Begin watching for new plugins + pluginManager.watch(); + // Tell the plugin manager if client is outdated or not pluginManager.setOutdated(isOutdated); 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 546f7e77bc..da48d2d116 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 @@ -63,6 +63,17 @@ public interface RuneLiteConfig extends Config return false; } + @ConfigItem( + keyName = "enablePlugins", + name = "Enable loading of external plugins", + description = "Enable loading of external plugins", + position = 10 + ) + default boolean enablePlugins() + { + return false; + } + @ConfigItem( keyName = "containInScreen", name = "Contain in screen", @@ -272,4 +283,4 @@ public interface RuneLiteConfig extends Config { return 35; } -} \ No newline at end of file +} 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 be5efa9081..192f587665 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 @@ -28,10 +28,15 @@ import com.google.inject.Binder; import com.google.inject.Injector; import com.google.inject.Module; +import java.io.File; + public abstract class Plugin implements Module { protected Injector injector; + public File file; + public PluginClassLoader loader; + @Override public void configure(Binder binder) { 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..3f6f44ec81 --- /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/PluginManager.java b/runelite-client/src/main/java/net/runelite/client/plugins/PluginManager.java index 73d6bf32e4..62e8a22aa8 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 @@ -92,6 +92,9 @@ public class PluginManager private final String runeliteGroupName = RuneLiteConfig.class .getAnnotation(ConfigGroup.class).value(); + @Inject + PluginWatcher pluginWatcher; + @Setter boolean isOutdated; @@ -113,6 +116,11 @@ public class PluginManager this.sceneTileManager = sceneTileManager; } + public void watch() + { + pluginWatcher.start(); + } + @Subscribe public void onSessionOpen(SessionOpen event) { 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..031d408c03 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/PluginWatcher.java @@ -0,0 +1,273 @@ +/* + * 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 com.google.inject.Injector; +import com.google.inject.Key; +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 lombok.extern.slf4j.Slf4j; +import net.runelite.client.RuneLite; +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.config.RuneLiteConfig; + +@Singleton +@Slf4j +public class PluginWatcher extends Thread +{ + 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 + private ConfigManager configManager; + + @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()); + + log.debug("Event {} file {}", kind, file); + + if (kind == ENTRY_MODIFY) + { + Plugin existing = findPluginForFile(file); + if (existing != null) + { + log.info("Reloading plugin {}", file); + unload(existing); + } + else + { + log.info("Loading plugin {}", file); + } + + load(file); + } + else if (kind == ENTRY_DELETE) + { + Plugin existing = findPluginForFile(file); + if (existing != null) + { + log.info("Unloading plugin {}", file); + + unload(existing); + } + } + } + key.reset(); + + } + catch (InterruptedException ex) + { + log.warn("error polling for plugins", ex); + } + } + } + + private void scan() + { + for (File file : BASE.listFiles()) + { + if (!file.getName().endsWith(".jar")) + { + continue; + } + + log.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) + { + log.warn("Error loading plugin", ex); + return; + } + + List loadedPlugins; + try + { + loadedPlugins = pluginManager.scanAndInstantiate(loader, null); + } + catch (IOException ex) + { + close(loader); + log.warn("Error loading plugin", ex); + return; + } + + if (loadedPlugins.isEmpty()) + { + close(loader); + log.warn("No plugin found in plugin {}", pluginFile); + return; + } + + if (loadedPlugins.size() != 1) + { + close(loader); + log.warn("You can not have more than one plugin per jar"); + return; + } + + Plugin plugin = loadedPlugins.get(0); + plugin.file = pluginFile; + plugin.loader = loader; + + // Initialize default configuration + Injector injector = plugin.getInjector(); + for (Key key : injector.getAllBindings().keySet()) + { + Class type = key.getTypeLiteral().getRawType(); + if (Config.class.isAssignableFrom(type)) + { + Config config = (Config) injector.getInstance(key); + configManager.setDefaultConfiguration(config, false); + } + } + + try + { + pluginManager.startPlugin(plugin); + } + catch (PluginInstantiationException ex) + { + close(loader); + log.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) + { + log.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) + { + log.warn(null, ex1); + } + } + +} diff --git a/runelite-mixins/pom.xml b/runelite-mixins/pom.xml index e8045675a8..b8b5413e1e 100644 --- a/runelite-mixins/pom.xml +++ b/runelite-mixins/pom.xml @@ -82,8 +82,8 @@ - 1.6 - 1.6 + 1.7 + 1.7 diff --git a/runelite-plugin-archetype/pom.xml b/runelite-plugin-archetype/pom.xml new file mode 100644 index 0000000000..fcb26cedf6 --- /dev/null +++ b/runelite-plugin-archetype/pom.xml @@ -0,0 +1,86 @@ + + + + 4.0.0 + + + net.runelite + runelite-parent + 1.5.21-SNAPSHOT + + + runelite-plugin-archetype + maven-archetype + RuneLite Plugin Archetype + + + + + + src/main/resources + true + + archetype-resources/pom.xml + + + + src/main/resources + false + + archetype-resources/pom.xml + + + + + + + org.apache.maven.archetype + archetype-packaging + 3.0.1 + + + + + + + maven-archetype-plugin + 3.0.1 + + + org.apache.maven.plugins + maven-resources-plugin + 3.0.2 + + + \ + + + + + + diff --git a/runelite-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/runelite-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml new file mode 100644 index 0000000000..b67d8cde67 --- /dev/null +++ b/runelite-plugin-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml @@ -0,0 +1,13 @@ + + + + + src/main/java + + **/*.java + + + + diff --git a/runelite-plugin-archetype/src/main/resources/archetype-resources/pom.xml b/runelite-plugin-archetype/src/main/resources/archetype-resources/pom.xml new file mode 100644 index 0000000000..11882b973e --- /dev/null +++ b/runelite-plugin-archetype/src/main/resources/archetype-resources/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + \${groupId} + \${artifactId} + \${version} + jar + + + UTF-8 + 1.8 + 1.8 + + + + + runelite + RuneLite + http://repo.runelite.net + + + + + + net.runelite + client + ${project.version} + provided + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + install + + + + + + + run + + + + + + + diff --git a/runelite-plugin-archetype/src/test/resources/projects/compilationtest/archetype.properties b/runelite-plugin-archetype/src/test/resources/projects/compilationtest/archetype.properties new file mode 100644 index 0000000000..ace9df8523 --- /dev/null +++ b/runelite-plugin-archetype/src/test/resources/projects/compilationtest/archetype.properties @@ -0,0 +1,5 @@ +sourceEncoding=UTF-8 +groupId=org.example.runelite +artifactId=exampleplugin +version=1.0.0-SNAPSHOT +package=org.example.runelite.exampleplugin \ No newline at end of file diff --git a/runelite-plugin-archetype/src/test/resources/projects/compilationtest/goal.txt b/runelite-plugin-archetype/src/test/resources/projects/compilationtest/goal.txt new file mode 100644 index 0000000000..0b5987362f --- /dev/null +++ b/runelite-plugin-archetype/src/test/resources/projects/compilationtest/goal.txt @@ -0,0 +1 @@ +verify diff --git a/travis/settings.xml b/travis/settings.xml index 6d2fec47d7..06f13271e0 100644 --- a/travis/settings.xml +++ b/travis/settings.xml @@ -259,6 +259,7 @@ under the License. true true + false