Adding external plugin support (#4)

* Adding archetype

* Update RuneLiteConfig.java

* Update Plugin.java

* Update PluginManager.java

* Adding pluginwatcher & classloader

* Update RuneLite.java

* Update pom.xml

* Update settings.xml

* Update pom.xml

* Update pom.xml

* Removing old example plugin

* Fixing the fix of the fix for plugin archetype.
This commit is contained in:
Ganom
2019-04-18 23:22:30 -04:00
committed by Tyler Bochard
parent b5d2700b07
commit bfe1482705
14 changed files with 542 additions and 4 deletions

10
pom.xml
View File

@@ -42,7 +42,7 @@
<maven.javadoc.skip>true</maven.javadoc.skip>
<checkstyle.skip>true</checkstyle.skip>
<archetype.test.skip>true</archetype.test.skip>
<rs.version>179</rs.version>
</properties>
@@ -120,6 +120,7 @@
<module>runelite-mixins</module>
<module>runelite-script-assembler-plugin</module>
<module>runescape-api</module>
<module>runelite-plugin-archetype</module>
<module>http-api</module>
<module>http-service</module>
<module>protocol-api</module>
@@ -151,6 +152,13 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.archetype</groupId>
<artifactId>archetype-packaging</artifactId>
<version>3.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -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);

View File

@@ -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",

View File

@@ -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)
{

View File

@@ -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);
}
}
}

View File

@@ -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)
{

View File

@@ -0,0 +1,273 @@
/*
* 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 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<Plugin> 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);
}
}
}

View File

@@ -82,8 +82,8 @@
<!-- RuneScape classes do not verify
under Java 7+ due to the obfuscation
-->
<source>1.6</source>
<target>1.6</target>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>net.runelite</groupId>
<artifactId>runelite-parent</artifactId>
<version>1.5.21-SNAPSHOT</version>
</parent>
<artifactId>runelite-plugin-archetype</artifactId>
<packaging>maven-archetype</packaging>
<name>RuneLite Plugin Archetype</name>
<build>
<!-- filter archetype resource's pom to put project version in it -->
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>archetype-resources/pom.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>archetype-resources/pom.xml</exclude>
</excludes>
</resource>
</resources>
<extensions>
<extension>
<groupId>org.apache.maven.archetype</groupId>
<artifactId>archetype-packaging</artifactId>
<version>3.0.1</version>
</extension>
</extensions>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-archetype-plugin</artifactId>
<version>3.0.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
<!-- allow escaping variables in filtered
resources - https://stackoverflow.com/a/7223084
-->
<configuration>
<escapeString>\</escapeString>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<archetype-descriptor xsi:schemaLocation="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-descriptor/1.0.0 http://maven.apache.org/xsd/archetype-descriptor-1.0.0.xsd" name="example-plugin"
xmlns="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-descriptor/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<fileSets>
<fileSet filtered="true" packaged="true" encoding="UTF-8">
<directory>src/main/java</directory>
<includes>
<include>**/*.java</include>
</includes>
</fileSet>
</fileSets>
</archetype-descriptor>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>\${groupId}</groupId>
<artifactId>\${artifactId}</artifactId>
<version>\${version}</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>runelite</id>
<name>RuneLite</name>
<url>http://repo.runelite.net</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>net.runelite</groupId>
<artifactId>client</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.8</version>
<executions>
<execution>
<phase>install</phase>
<configuration>
<target>
<copy file="\${project.build.directory}/\${project.build.finalName}.jar"
tofile="\${user.home}/.runelite/plugins/\${project.build.finalName}.jar"/>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,5 @@
sourceEncoding=UTF-8
groupId=org.example.runelite
artifactId=exampleplugin
version=1.0.0-SNAPSHOT
package=org.example.runelite.exampleplugin

View File

@@ -259,6 +259,7 @@ under the License.
<properties>
<maven.javadoc.skip>true</maven.javadoc.skip>
<checkstyle.skip>true</checkstyle.skip>
<archetype.test.skip>false</archetype.test.skip>
</properties>
</profile>
</profiles>