project: Rework external plugins, local instances syncing, hotswapping

This commit is contained in:
Owain van Brakel
2020-03-27 21:17:20 +01:00
parent 146ab3beff
commit 3ecdebf542
24 changed files with 1035 additions and 458 deletions

View File

@@ -34,6 +34,7 @@ open class BootstrapTask @Inject constructor(@Input val type: String) : DefaultT
private fun getArtifacts(): Array<JsonBuilder> {
val artifacts = ArrayList<JsonBuilder>()
val artifactsSet = HashSet<String>()
project.configurations["runtimeClasspath"].resolvedConfiguration.resolvedArtifacts.forEach {
val module = it.moduleVersion.id.toString()
@@ -44,7 +45,11 @@ open class BootstrapTask @Inject constructor(@Input val type: String) : DefaultT
val version = splat[2]
lateinit var path: String
if (it.file.name.contains(ProjectVersions.openosrsVersion)) {
if (it.file.name.contains("injected-client") ||
it.file.name.contains("runelite-client") ||
it.file.name.contains("http-api") ||
it.file.name.contains("runescape-api") ||
it.file.name.contains("runelite-api")) {
path = "https://github.com/open-osrs/hosting/raw/master/${type}/${it.file.name}"
} else if (it.file.name.contains("injection-annotations") || it.file.name.contains("rxrelay")) {
path = "https://github.com/open-osrs/hosting/raw/master/" + group.replace(".", "/") + "/${name}/$version/${it.file.name}"
@@ -62,8 +67,11 @@ open class BootstrapTask @Inject constructor(@Input val type: String) : DefaultT
path += "${name}/$version/${name}-$version.jar"
}
val artifactFile = File(it.file.absolutePath)
val filePath = it.file.absolutePath
val artifactFile = File(filePath)
if (!artifactsSet.contains(filePath)) {
artifactsSet.add(filePath)
artifacts.add(JsonBuilder(
"name" to it.file.name,
"path" to path,
@@ -71,6 +79,7 @@ open class BootstrapTask @Inject constructor(@Input val type: String) : DefaultT
"hash" to hash(artifactFile.readBytes())
))
}
}
artifacts.add(JsonBuilder(
"name" to clientJar!!.name,

View File

@@ -62,6 +62,7 @@ object Libraries {
const val guice = "4.2.2"
const val h2 = "1.4.200"
const val hamcrest = "2.2"
const val javagroups = "4.0.0.Final"
const val javax = "1.3.2"
const val javaxInject = "1"
const val jna = "5.5.0"
@@ -108,6 +109,7 @@ object Libraries {
const val guiceTestlib = "com.google.inject.extensions:guice-testlib:${Versions.guice}"
const val h2 = "com.h2database:h2:${Versions.h2}"
const val hamcrest = "org.hamcrest:hamcrest-library:${Versions.hamcrest}"
const val javagroups = "org.jgroups:jgroups:${Versions.javagroups}"
const val javax = "javax.annotation:javax.annotation-api:${Versions.javax}"
const val javaxInject = "javax.inject:javax.inject:${Versions.javaxInject}"
const val jna = "net.java.dev.jna:jna:${Versions.jna}"

View File

@@ -56,6 +56,7 @@ dependencies {
implementation(Libraries.okhttp3)
implementation(Libraries.rxjava)
implementation(Libraries.jna)
implementation(Libraries.javagroups)
implementation(Libraries.jnaPlatform)
implementation(Libraries.discord)
implementation(Libraries.substance)
@@ -134,11 +135,15 @@ tasks {
inputs.properties(tokens)
from("src/main/resources") {
include("open.osrs.properties")
include("sentry.properties")
}
into("${buildDir}/resources/main")
from("src/main/resources/net/runelite/client") {
include("open.osrs.properties")
}
into("${buildDir}/resources/main/net/runelite/client")
filter(ReplaceTokens::class, "tokens" to tokens)
filteringCharset = "UTF-8"
}

View File

@@ -40,6 +40,7 @@ import java.net.PasswordAuthentication;
import java.nio.file.Paths;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
import javax.annotation.Nullable;
import javax.inject.Provider;
import javax.inject.Singleton;
@@ -88,7 +89,7 @@ import net.runelite.client.ui.overlay.arrow.ArrowWorldOverlay;
import net.runelite.client.ui.overlay.infobox.InfoBoxOverlay;
import net.runelite.client.ui.overlay.tooltip.TooltipOverlay;
import net.runelite.client.ui.overlay.worldmap.WorldMapOverlay;
import net.runelite.client.util.AppLock;
import net.runelite.client.util.Groups;
import net.runelite.client.util.WorldUtil;
import net.runelite.client.ws.PartyService;
import net.runelite.http.api.worlds.World;
@@ -112,6 +113,7 @@ public class RuneLite
public static final File DEFAULT_CONFIG_FILE = new File(RUNELITE_DIR, "runeliteplus.properties");
public static final Locale SYSTEM_LOCALE = Locale.getDefault();
public static boolean allowPrivateServer = false;
public static String uuid = UUID.randomUUID().toString();
@Getter
private static Injector injector;
@@ -191,6 +193,9 @@ public class RuneLite
@Inject
private Provider<PartyService> partyService;
@Inject
private Groups groups;
@Inject
private Hooks hooks;
@@ -210,9 +215,6 @@ public class RuneLite
@Inject
private Scheduler scheduler;
@Inject
private AppLock appLock;
public static void main(String[] args) throws Exception
{
Locale.setDefault(Locale.ENGLISH);
@@ -383,15 +385,12 @@ public class RuneLite
// Tell the plugin manager if client is outdated or not
pluginManager.setOutdated(isOutdated);
// Load external plugins
// Load external plugin manager
externalPluginManager.startExternalUpdateManager();
externalPluginManager.startExternalPluginManager();
if (appLock.lock(this.getClass().getName()))
{
RuneLiteSplashScreen.stage(.59, "Updating external plugins");
// Update external plugins
externalPluginManager.update();
}
// Load the plugins, but does not start them yet.
// This will initialize configuration
@@ -510,7 +509,7 @@ public class RuneLite
{
clientSessionManager.shutdown();
discordService.close();
appLock.release();
groups.close();
}
private static class ConfigFileConverter implements ValueConverter<File>

View File

@@ -52,7 +52,7 @@ public class RuneLiteProperties
static
{
try (InputStream in = RuneLiteProperties.class.getResourceAsStream("/open.osrs.properties"))
try (InputStream in = RuneLiteProperties.class.getResourceAsStream("open.osrs.properties"))
{
properties.load(in);
}

View File

@@ -70,7 +70,10 @@ import net.runelite.client.events.ConfigChanged;
import net.runelite.client.plugins.ExternalPluginManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.util.ColorUtil;
import net.runelite.client.util.Groups;
import org.apache.commons.lang3.StringUtils;
import org.jgroups.Message;
import org.jgroups.util.Util;
@Singleton
@Slf4j
@@ -82,15 +85,24 @@ public class ConfigManager
private final Properties properties = new Properties();
private final Map<String, String> pendingChanges = new HashMap<>();
private final File settingsFileInput;
private final Groups groups;
@Inject
EventBus eventBus;
@Inject
public ConfigManager(@Named("config") File config, ScheduledExecutorService scheduledExecutorService)
public ConfigManager(
@Named("config") File config,
ScheduledExecutorService scheduledExecutorService,
Groups groups)
{
this.settingsFileInput = config;
this.groups = groups;
scheduledExecutorService.scheduleWithFixedDelay(this::sendConfig, 30, 30, TimeUnit.SECONDS);
groups.getMessageObjectSubject()
.subscribe(this::receive);
}
@SuppressWarnings("unchecked")
@@ -450,6 +462,7 @@ public class ConfigManager
configChanged.setKey(key);
configChanged.setOldValue(null);
configChanged.setNewValue(value);
eventBus.post(ConfigChanged.class, configChanged);
});
}
@@ -525,7 +538,17 @@ public class ConfigManager
return null;
}
public void setConfiguration(String groupName, String key, Object value)
{
setConfiguration(groupName, key, objectToString(value));
}
public void setConfiguration(String groupName, String key, String value)
{
setConfiguration(groupName, key, value, null);
}
public void setConfiguration(String groupName, String key, String value, String origin)
{
String oldValue = (String) properties.setProperty(groupName + "." + key, value);
@@ -547,16 +570,23 @@ public class ConfigManager
configChanged.setKey(key);
configChanged.setOldValue(oldValue);
configChanged.setNewValue(value);
configChanged.setOrigin(origin == null ? RuneLite.uuid : origin);
configChanged.setPath(settingsFileInput.getAbsolutePath());
eventBus.post(ConfigChanged.class, configChanged);
}
public void setConfiguration(String groupName, String key, Object value)
if (origin == null)
{
setConfiguration(groupName, key, objectToString(value));
broadcast(configChanged);
}
}
public void unsetConfiguration(String groupName, String key)
{
unsetConfiguration(groupName, key, null);
}
public void unsetConfiguration(String groupName, String key, String origin)
{
String oldValue = (String) properties.remove(groupName + "." + key);
@@ -577,8 +607,15 @@ public class ConfigManager
configChanged.setGroup(groupName);
configChanged.setKey(key);
configChanged.setOldValue(oldValue);
configChanged.setOrigin(origin == null ? RuneLite.uuid : origin);
configChanged.setPath(settingsFileInput.getAbsolutePath());
eventBus.post(ConfigChanged.class, configChanged);
if (origin == null)
{
broadcast(configChanged);
}
}
public ConfigDescriptor getConfigDescriptor(Object configurationProxy)
@@ -842,4 +879,40 @@ public class ConfigManager
syncPropertiesFromFile(newestFile);
}
private void broadcast(ConfigChanged configChanged)
{
groups.sendConfig(null, configChanged);
}
public void receive(Message message)
{
if (message.getObject() instanceof String)
{
return;
}
try
{
ConfigChanged configChanged = Util.objectFromByteBuffer(message.getBuffer());
if (!configChanged.getPath().equals(settingsFileInput.getAbsolutePath()))
{
return;
}
if (configChanged.getNewValue() == null)
{
unsetConfiguration(configChanged.getGroup(), configChanged.getKey(), configChanged.getOrigin());
}
else
{
setConfiguration(configChanged.getGroup(), configChanged.getKey(), configChanged.getNewValue(), configChanged.getOrigin());
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
}

View File

@@ -342,11 +342,23 @@ public interface OpenOSRSConfig extends Config
return new Title();
}
@ConfigItem(
keyName = "localSync",
name = "Sync local instances",
description = "Enables multiple local instances of OpenOSRS to communicate (this enables syncing plugin state and config options)",
position = 21,
titleSection = "miscTitle"
)
default boolean localSync()
{
return true;
}
@ConfigItem(
keyName = "keyboardPin",
name = "Keyboard bank pin",
description = "Enables you to type your bank pin",
position = 21,
position = 22,
titleSection = "miscTitle"
)
default boolean keyboardPin()
@@ -358,7 +370,7 @@ public interface OpenOSRSConfig extends Config
keyName = "detachHotkey",
name = "Detach Cam",
description = "Detach Camera hotkey, press this and it will activate detatched camera.",
position = 22,
position = 23,
titleSection = "miscTitle"
)
default Keybind detachHotkey()

View File

@@ -24,6 +24,7 @@
*/
package net.runelite.client.events;
import java.io.Serializable;
import lombok.Data;
import net.runelite.api.events.Event;
@@ -31,7 +32,7 @@ import net.runelite.api.events.Event;
* An event where a configuration entry has been modified.
*/
@Data
public class ConfigChanged implements Event
public class ConfigChanged implements Event, Serializable
{
/**
* The parent group for the key.
@@ -52,4 +53,12 @@ public class ConfigChanged implements Event
* The new value of the entry, null if the entry has been unset.
*/
private String newValue;
/**
* The client where the config value was changed from
*/
private String origin;
/**
* Path of the current config file
*/
private String path;
}

View File

@@ -1,13 +1,36 @@
/*
* Copyright (c) 2020, Owain van Brakel <https://github.com/Owain94>
* 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.common.collect.ImmutableList;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.MutableGraph;
import com.google.inject.Binder;
import com.google.inject.CreationException;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
@@ -30,13 +53,13 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@@ -48,12 +71,16 @@ import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.config.OpenOSRSConfig;
import net.runelite.client.eventbus.EventBus;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.events.ExternalPluginChanged;
import net.runelite.client.events.ExternalRepositoryChanged;
import net.runelite.client.ui.ClientUI;
import net.runelite.client.ui.RuneLiteSplashScreen;
import net.runelite.client.util.Groups;
import net.runelite.client.util.MiscUtils;
import net.runelite.client.util.SwingUtil;
import org.jgroups.Message;
import org.jgroups.ReceiverAdapter;
import org.pf4j.DefaultPluginManager;
import org.pf4j.DependencyResolver;
import org.pf4j.JarPluginLoader;
@@ -65,6 +92,8 @@ import org.pf4j.PluginDescriptorFinder;
import org.pf4j.PluginLoader;
import org.pf4j.PluginRepository;
import org.pf4j.PluginRuntimeException;
import org.pf4j.PluginState;
import org.pf4j.PluginStateEvent;
import org.pf4j.PluginWrapper;
import org.pf4j.RuntimeMode;
import org.pf4j.update.DefaultUpdateRepository;
@@ -75,8 +104,7 @@ import org.pf4j.update.VerifyException;
@Slf4j
@Singleton
public
class ExternalPluginManager
public class ExternalPluginManager extends ReceiverAdapter
{
public static ArrayList<ClassLoader> pluginClassLoaders = new ArrayList<>();
private final PluginManager runelitePluginManager;
@@ -86,35 +114,41 @@ class ExternalPluginManager
private final OpenOSRSConfig openOSRSConfig;
private final EventBus eventBus;
private final ConfigManager configManager;
private final List<Plugin> plugins = new CopyOnWriteArrayList<>();
private final Map<String, String> pluginsMap = new HashMap<>();
@Getter(AccessLevel.PUBLIC)
private final Map<String, Map<String, String>> pluginsInfoMap = new HashMap<>();
private final Groups groups;
@Getter(AccessLevel.PUBLIC)
private UpdateManager updateManager;
private Map<String, PluginInfo.PluginRelease> lastPluginRelease = new HashMap<>();
@Inject
public ExternalPluginManager(
PluginManager pluginManager,
OpenOSRSConfig openOSRSConfig,
EventBus eventBus,
ConfigManager configManager)
ConfigManager configManager,
Groups groups)
{
this.runelitePluginManager = pluginManager;
this.openOSRSConfig = openOSRSConfig;
this.eventBus = eventBus;
this.configManager = configManager;
this.groups = groups;
//noinspection ResultOfMethodCallIgnored
EXTERNALPLUGIN_DIR.mkdirs();
initPluginManager();
groups.getMessageStringSubject()
.subscribe(this::receive);
}
private void initPluginManager()
{
boolean debug = RuneLiteProperties.getLauncherVersion() == null && RuneLiteProperties.getPluginPath() != null;
this.externalPluginManager = new DefaultPluginManager(
debug ? Paths.get(RuneLiteProperties.getPluginPath() + File.separator + "release")
externalPluginManager = new DefaultPluginManager(
RuneLiteProperties.getPluginPath() != null ? Paths.get(RuneLiteProperties.getPluginPath())
: EXTERNALPLUGIN_DIR.toPath())
{
@Override
@@ -158,7 +192,70 @@ class ExternalPluginManager
@Override
public RuntimeMode getRuntimeMode()
{
return debug ? RuntimeMode.DEVELOPMENT : RuntimeMode.DEPLOYMENT;
return RuneLiteProperties.getLauncherVersion() == null ? RuntimeMode.DEVELOPMENT : RuntimeMode.DEPLOYMENT;
}
@Override
protected void resolvePlugins()
{
// retrieves the plugins descriptors
List<org.pf4j.PluginDescriptor> descriptors = new ArrayList<>();
for (PluginWrapper plugin : plugins.values())
{
descriptors.add(plugin.getDescriptor());
}
// retrieves the plugins descriptors from the resolvedPlugins list. This allows to load plugins that have already loaded dependencies.
for (PluginWrapper plugin : resolvedPlugins)
{
descriptors.add(plugin.getDescriptor());
}
DependencyResolver.Result result = dependencyResolver.resolve(descriptors);
if (result.hasCyclicDependency())
{
throw new DependencyResolver.CyclicDependencyException();
}
List<String> notFoundDependencies = result.getNotFoundDependencies();
if (!notFoundDependencies.isEmpty())
{
throw new DependencyResolver.DependenciesNotFoundException(notFoundDependencies);
}
List<DependencyResolver.WrongDependencyVersion> wrongVersionDependencies = result.getWrongVersionDependencies();
if (!wrongVersionDependencies.isEmpty())
{
throw new DependencyResolver.DependenciesWrongVersionException(wrongVersionDependencies);
}
List<String> sortedPlugins = result.getSortedPlugins();
// move plugins from "unresolved" to "resolved"
for (String pluginId : sortedPlugins)
{
PluginWrapper pluginWrapper = plugins.get(pluginId);
//The plugin is already resolved. Don't put a copy in the resolvedPlugins.
if (resolvedPlugins.contains(pluginWrapper))
{
continue;
}
if (unresolvedPlugins.remove(pluginWrapper))
{
PluginState pluginState = pluginWrapper.getPluginState();
if (pluginState != PluginState.DISABLED)
{
pluginWrapper.setPluginState(PluginState.RESOLVED);
}
resolvedPlugins.add(pluginWrapper);
firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
}
}
}
@Override
@@ -209,8 +306,129 @@ class ExternalPluginManager
log.error(e.getMessage(), e);
}
}
@Override
public PluginState stopPlugin(String pluginId)
{
if (!plugins.containsKey(pluginId))
{
throw new IllegalArgumentException(String.format("Unknown pluginId %s", pluginId));
}
PluginWrapper pluginWrapper = getPlugin(pluginId);
org.pf4j.PluginDescriptor pluginDescriptor = pluginWrapper.getDescriptor();
PluginState pluginState = pluginWrapper.getPluginState();
if (PluginState.STOPPED == pluginState)
{
log.debug("Already stopped plugin '{}'", getPluginLabel(pluginDescriptor));
return PluginState.STOPPED;
}
// test for disabled plugin
if (PluginState.DISABLED == pluginState)
{
// do nothing
return pluginState;
}
pluginWrapper.getPlugin().stop();
pluginWrapper.setPluginState(PluginState.STOPPED);
startedPlugins.remove(pluginWrapper);
firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
return pluginWrapper.getPluginState();
}
@Override
public boolean unloadPlugin(String pluginId)
{
try
{
PluginState pluginState = stopPlugin(pluginId);
if (PluginState.STARTED == pluginState)
{
return false;
}
PluginWrapper pluginWrapper = getPlugin(pluginId);
// remove the plugin
plugins.remove(pluginId);
getResolvedPlugins().remove(pluginWrapper);
firePluginStateEvent(new PluginStateEvent(this, pluginWrapper, pluginState));
// remove the classloader
Map<String, ClassLoader> pluginClassLoaders = getPluginClassLoaders();
if (pluginClassLoaders.containsKey(pluginId))
{
ClassLoader classLoader = pluginClassLoaders.remove(pluginId);
if (classLoader instanceof Closeable)
{
try
{
((Closeable) classLoader).close();
}
catch (IOException e)
{
throw new PluginRuntimeException(e, "Cannot close classloader");
}
}
}
return true;
}
catch (IllegalArgumentException e)
{
// ignore not found exceptions because this method is recursive
}
return false;
}
@Override
public boolean deletePlugin(String pluginId)
{
if (!plugins.containsKey(pluginId))
{
throw new IllegalArgumentException(String.format("Unknown pluginId %s", pluginId));
}
PluginWrapper pluginWrapper = getPlugin(pluginId);
// stop the plugin if it's started
PluginState pluginState = stopPlugin(pluginId);
if (PluginState.STARTED == pluginState)
{
log.error("Failed to stop plugin '{}' on delete", pluginId);
return false;
}
// get an instance of plugin before the plugin is unloaded
// for reason see https://github.com/pf4j/pf4j/issues/309
org.pf4j.Plugin plugin = pluginWrapper.getPlugin();
if (!unloadPlugin(pluginId))
{
log.error("Failed to unload plugin '{}' on delete", pluginId);
return false;
}
// notify the plugin as it's deleted
plugin.delete();
Path pluginPath = pluginWrapper.getPluginPath();
return pluginRepository.deletePluginPath(pluginPath);
}
};
this.externalPluginManager.setSystemVersion(SYSTEM_VERSION);
externalPluginManager.setSystemVersion(SYSTEM_VERSION);
}
public boolean developmentMode()
{
return externalPluginManager.isDevelopment();
}
public boolean doesGhRepoExist(String owner, String name)
@@ -263,7 +481,7 @@ class ExternalPluginManager
{
try
{
this.externalPluginManager.loadPlugins();
externalPluginManager.loadPlugins();
}
catch (Exception ex)
{
@@ -275,7 +493,7 @@ class ExternalPluginManager
for (String dep : deps)
{
install(dep);
updateManager.installPlugin(dep, null);
}
startExternalPluginManager();
@@ -292,7 +510,7 @@ class ExternalPluginManager
loadOldFormat();
}
this.updateManager = new UpdateManager(this.externalPluginManager, repositories);
updateManager = new UpdateManager(externalPluginManager, repositories);
saveConfig();
}
@@ -361,7 +579,7 @@ class ExternalPluginManager
openOSRSConfig.setExternalRepositories("OpenOSRS:https://raw.githubusercontent.com/open-osrs/plugin-hosting/master/");
}
this.updateManager = new UpdateManager(this.externalPluginManager, repositories);
updateManager = new UpdateManager(externalPluginManager, repositories);
}
public void addGHRepository(String owner, String name)
@@ -407,12 +625,9 @@ class ExternalPluginManager
openOSRSConfig.setExternalRepositories(config.toString());
}
private List<Plugin> scanAndInstantiate(List<Plugin> plugins, boolean init, boolean initConfig) throws IOException
private void scanAndInstantiate(List<Plugin> plugins, boolean init, boolean initConfig)
{
RuneLiteSplashScreen.stage(.66, "Loading external plugins");
MutableGraph<Class<? extends Plugin>> graph = GraphBuilder
.directed()
.build();
AtomicInteger loaded = new AtomicInteger();
List<Plugin> scannedPlugins = new CopyOnWriteArrayList<>();
@@ -451,6 +666,7 @@ class ExternalPluginManager
Plugin plugininst;
try
{
//noinspection unchecked
plugininst = instantiate(scannedPlugins, (Class<Plugin>) plugin.getClass(), init, initConfig);
scannedPlugins.add(plugininst);
}
@@ -477,7 +693,6 @@ class ExternalPluginManager
});
}
return scannedPlugins;
}
@SuppressWarnings("unchecked")
@@ -492,7 +707,7 @@ class ExternalPluginManager
Optional<Plugin> dependency =
Stream.concat(runelitePluginManager.getPlugins().stream(), scannedPlugins.stream())
.filter(p -> p.getClass() == pluginDependency.value()).findFirst();
if (!dependency.isPresent())
if (dependency.isEmpty())
{
throw new PluginInstantiationException(
"Unmet dependency for " + clazz.getSimpleName() + ": " + pluginDependency.value().getSimpleName());
@@ -551,12 +766,12 @@ class ExternalPluginManager
{
try
{
SwingUtilities.invokeAndWait(() ->
SwingUtil.syncExec(() ->
{
try
{
runelitePluginManager.startPlugin(plugin);
runelitePluginManager.add(plugin);
runelitePluginManager.startPlugin(plugin);
eventBus.post(ExternalPluginChanged.class,
new ExternalPluginChanged(pluginsMap.get(plugin.getClass().getSimpleName()), plugin,
true));
@@ -586,16 +801,10 @@ class ExternalPluginManager
return plugin;
}
public void loadPlugins()
{
this.externalPluginManager.startPlugins();
List<PluginWrapper> startedPlugins = getStartedPlugins();
List<Plugin> scannedPlugins = new ArrayList<>();
for (PluginWrapper plugin : startedPlugins)
private void checkDepsAndStart(List<PluginWrapper> startedPlugins, List<Plugin> scannedPlugins, PluginWrapper pluginWrapper)
{
boolean depsLoaded = true;
for (PluginDependency dependency : plugin.getDescriptor().getDependencies())
for (PluginDependency dependency : pluginWrapper.getDescriptor().getDependencies())
{
if (startedPlugins.stream().noneMatch(pl -> pl.getPluginId().equals(dependency.getPluginId())))
{
@@ -606,23 +815,39 @@ class ExternalPluginManager
if (!depsLoaded)
{
// This should never happen but can crash the client
continue;
return;
}
scannedPlugins.addAll(loadPlugin(plugin.getPluginId()));
scannedPlugins.addAll(loadPlugin(pluginWrapper.getPluginId()));
}
startPlugins(scannedPlugins, false, false);
public void loadPlugins()
{
externalPluginManager.startPlugins();
List<PluginWrapper> startedPlugins = getStartedPlugins();
List<Plugin> scannedPlugins = new ArrayList<>();
for (PluginWrapper plugin : startedPlugins)
{
checkDepsAndStart(startedPlugins, scannedPlugins, plugin);
}
private void startPlugins(List<Plugin> scannedPlugins, boolean init, boolean initConfig)
scanAndInstantiate(scannedPlugins, false, false);
if (groups.getInstanceCount() > 1)
{
try
for (String pluginId : getDisabledPlugins())
{
plugins.addAll(scanAndInstantiate(scannedPlugins, init, initConfig));
groups.sendString("STOPEXTERNAL;" + pluginId);
}
catch (IOException ignored)
}
else
{
for (String pluginId : getDisabledPlugins())
{
externalPluginManager.enablePlugin(pluginId);
externalPluginManager.deletePlugin(pluginId);
}
}
}
@@ -633,55 +858,57 @@ class ExternalPluginManager
for (Plugin plugin : extensions)
{
pluginClassLoaders.add(plugin.getClass().getClassLoader());
pluginsMap.remove(plugin.getClass().getSimpleName());
pluginsMap.put(plugin.getClass().getSimpleName(), pluginId);
pluginsInfoMap.remove(plugin.getClass().getSimpleName());
AtomicReference<String> support = new AtomicReference<>("");
AtomicReference<String> version = new AtomicReference<>("");
updateManager.getRepositories().forEach(repository ->
repository.getPlugins().forEach((key, value) -> {
if (key.equals(pluginId))
{
support.set(value.projectUrl);
for (PluginInfo.PluginRelease release : value.releases)
{
if (externalPluginManager.getSystemVersion().equals("0.0.0") || externalPluginManager.getVersionManager().checkVersionConstraint(externalPluginManager.getSystemVersion(), release.requires))
{
if (lastPluginRelease.get(pluginId) == null)
{
lastPluginRelease.put(pluginId, release);
}
else if (externalPluginManager.getVersionManager().compareVersions(release.version, lastPluginRelease.get(pluginId).version) > 0)
{
lastPluginRelease.put(pluginId, release);
}
}
}
version.set(lastPluginRelease.get(pluginId).version);
}
}));
pluginsInfoMap.put(
plugin.getClass().getSimpleName(),
new HashMap<>()
{{
put("version", version.get());
put("id", externalPluginManager.getPlugin(pluginId).getDescriptor().getPluginId());
put("provider", externalPluginManager.getPlugin(pluginId).getDescriptor().getProvider());
put("support", support.get());
}}
);
scannedPlugins.add(plugin);
}
return scannedPlugins;
}
private void stopPlugins()
{
List<PluginWrapper> startedPlugins = ImmutableList.copyOf(getStartedPlugins());
for (PluginWrapper pluginWrapper : startedPlugins)
{
String pluginId = pluginWrapper.getDescriptor().getPluginId();
List<Plugin> extensions = externalPluginManager.getExtensions(Plugin.class, pluginId);
for (Plugin plugin : runelitePluginManager.getPlugins())
{
if (!extensions.get(0).getClass().getName().equals(plugin.getClass().getName()))
{
continue;
}
try
{
SwingUtilities.invokeAndWait(() ->
{
try
{
runelitePluginManager.stopPlugin(plugin);
}
catch (Exception e2)
{
throw new RuntimeException(e2);
}
});
runelitePluginManager.remove(plugin);
eventBus.post(ExternalPluginChanged.class, new ExternalPluginChanged(pluginId, plugin, false));
}
catch (Exception ex)
{
log.warn("unable to stop plugin", ex);
return;
}
}
}
}
private Path stopPlugin(String pluginId)
{
List<PluginWrapper> startedPlugins = ImmutableList.copyOf(getStartedPlugins());
@@ -704,7 +931,7 @@ class ExternalPluginManager
try
{
SwingUtilities.invokeAndWait(() ->
SwingUtil.syncExec(() ->
{
try
{
@@ -716,6 +943,7 @@ class ExternalPluginManager
}
});
runelitePluginManager.remove(plugin);
pluginClassLoaders.remove(plugin.getClass().getClassLoader());
eventBus.post(ExternalPluginChanged.class, new ExternalPluginChanged(pluginId, plugin, false));
@@ -734,13 +962,13 @@ class ExternalPluginManager
public boolean install(String pluginId) throws VerifyException
{
if (getDisabledPlugins().contains(pluginId))
{
this.externalPluginManager.enablePlugin(pluginId);
this.externalPluginManager.startPlugin(pluginId);
externalPluginManager.enablePlugin(pluginId);
externalPluginManager.startPlugin(pluginId);
startPlugins(loadPlugin(pluginId), true, false);
groups.broadcastSring("STARTEXTERNAL;" + pluginId);
scanAndInstantiate(loadPlugin(pluginId), true, false);
return true;
}
@@ -775,7 +1003,9 @@ class ExternalPluginManager
updateManager.installPlugin(pluginId, null);
startPlugins(loadPlugin(pluginId), true, true);
scanAndInstantiate(loadPlugin(pluginId), true, true);
groups.broadcastSring("STARTEXTERNAL;" + pluginId);
}
catch (DependencyResolver.DependenciesNotFoundException ex)
{
@@ -792,6 +1022,11 @@ class ExternalPluginManager
}
public boolean uninstall(String pluginId)
{
return uninstall(pluginId, false);
}
public boolean uninstall(String pluginId, boolean skip)
{
Path pluginPath = stopPlugin(pluginId);
@@ -801,13 +1036,35 @@ class ExternalPluginManager
}
externalPluginManager.stopPlugin(pluginId);
externalPluginManager.disablePlugin(pluginId);
if (skip)
{
return true;
}
if (groups.getInstanceCount() > 1)
{
groups.sendString("STOPEXTERNAL;" + pluginId);
}
else
{
externalPluginManager.deletePlugin(pluginId);
}
return true;
}
public void update()
{
if (groups.getInstanceCount() > 1)
{
// Do not update when there is more than one client open -> api might contain changes
log.info("Not updating external plugins since there is more than 1 client open");
return;
}
RuneLiteSplashScreen.stage(.59, "Updating external plugins");
boolean error = false;
if (updateManager.hasUpdates())
{
@@ -828,6 +1085,7 @@ class ExternalPluginManager
}
catch (PluginRuntimeException ex)
{
// This should never happen but can crash the client
log.warn("Cannot update plugin '{}', the user probably has another client open", plugin.id);
error = true;
break;
@@ -861,15 +1119,99 @@ class ExternalPluginManager
public List<String> getDisabledPlugins()
{
return this.externalPluginManager.getResolvedPlugins()
return externalPluginManager.getResolvedPlugins()
.stream()
.filter(not(this.externalPluginManager.getStartedPlugins()::contains))
.filter(not(externalPluginManager.getStartedPlugins()::contains))
.map(PluginWrapper::getPluginId)
.collect(Collectors.toList());
}
public List<PluginWrapper> getStartedPlugins()
{
return this.externalPluginManager.getStartedPlugins();
return externalPluginManager.getStartedPlugins();
}
public Boolean reloadStart(String pluginId)
{
externalPluginManager.loadPlugins();
externalPluginManager.startPlugin(pluginId);
List<PluginWrapper> startedPlugins = ImmutableList.copyOf(getStartedPlugins());
List<Plugin> scannedPlugins = new ArrayList<>();
for (PluginWrapper pluginWrapper : startedPlugins)
{
if (!pluginId.equals(pluginWrapper.getDescriptor().getPluginId()))
{
continue;
}
checkDepsAndStart(startedPlugins, scannedPlugins, pluginWrapper);
}
scanAndInstantiate(scannedPlugins, true, false);
groups.broadcastSring("STARTEXTERNAL;" + pluginId);
return true;
}
public void receive(Message message)
{
if (message.getObject() instanceof ConfigChanged)
{
return;
}
String[] messageObject = ((String) message.getObject()).split(";");
if (messageObject.length < 2)
{
return;
}
String command = messageObject[0];
String pluginId = messageObject[1];
switch (command)
{
case "STARTEXTERNAL":
externalPluginManager.loadPlugins();
externalPluginManager.startPlugin(pluginId);
List<PluginWrapper> startedPlugins = ImmutableList.copyOf(getStartedPlugins());
List<Plugin> scannedPlugins = new ArrayList<>();
for (PluginWrapper pluginWrapper : startedPlugins)
{
if (!pluginId.equals(pluginWrapper.getDescriptor().getPluginId()))
{
continue;
}
checkDepsAndStart(startedPlugins, scannedPlugins, pluginWrapper);
}
scanAndInstantiate(scannedPlugins, true, false);
break;
case "STOPEXTERNAL":
uninstall(pluginId, true);
externalPluginManager.unloadPlugin(pluginId);
groups.send(message.getSrc(), "STOPPEDEXTERNAL;" + pluginId);
break;
case "STOPPEDEXTERNAL":
groups.getMessageMap().get(pluginId).remove(message.getSrc());
if (groups.getMessageMap().get(pluginId).size() == 0)
{
groups.getMessageMap().remove(pluginId);
externalPluginManager.deletePlugin(pluginId);
}
break;
}
}
}

View File

@@ -63,5 +63,9 @@ public class PluginClassLoader extends URLClassLoader
// fall back to main class loader
return parent.loadClass(name);
}
catch (NoClassDefFoundError ex)
{
return null;
}
}
}

View File

@@ -39,6 +39,8 @@ import com.google.inject.CreationException;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import io.reactivex.rxjava3.schedulers.Schedulers;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
@@ -56,6 +58,7 @@ import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import javax.swing.SwingUtilities;
@@ -68,6 +71,7 @@ import net.runelite.client.config.ConfigGroup;
import net.runelite.client.config.ConfigManager;
import net.runelite.client.config.RuneLiteConfig;
import net.runelite.client.eventbus.EventBus;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.events.PluginChanged;
import net.runelite.client.events.SessionClose;
import net.runelite.client.events.SessionOpen;
@@ -76,6 +80,8 @@ import net.runelite.client.task.ScheduledMethod;
import net.runelite.client.task.Scheduler;
import net.runelite.client.ui.RuneLiteSplashScreen;
import net.runelite.client.util.GameEventManager;
import net.runelite.client.util.Groups;
import org.jgroups.Message;
@Singleton
@Slf4j
@@ -96,6 +102,8 @@ public class PluginManager
private final List<PluginConfigurationDescriptor> fakePlugins = new ArrayList<>();
private final String runeliteGroupName = RuneLiteConfig.class
.getAnnotation(ConfigGroup.class).value();
private final Groups groups;
private final File settingsFileInput;
@Inject
ExternalPluginLoader externalPluginLoader;
@@ -109,18 +117,23 @@ public class PluginManager
final EventBus eventBus,
final Scheduler scheduler,
final ConfigManager configManager,
final Provider<GameEventManager> sceneTileManager)
final Provider<GameEventManager> sceneTileManager,
final Groups groups,
final @Named("config") File config)
{
this.eventBus = eventBus;
this.scheduler = scheduler;
this.configManager = configManager;
this.sceneTileManager = sceneTileManager;
this.groups = groups;
this.settingsFileInput = config;
if (eventBus != null)
{
eventBus.subscribe(SessionOpen.class, this, this::onSessionOpen);
eventBus.subscribe(SessionClose.class, this, this::onSessionClose);
}
groups.getMessageStringSubject()
.subscribeOn(Schedulers.from(SwingUtilities::invokeLater))
.subscribe(this::receive);
}
private void onSessionOpen(SessionOpen event)
@@ -321,6 +334,12 @@ public class PluginManager
for (ClassInfo classInfo : classes)
{
Class<?> clazz = classInfo.load();
if (clazz == null)
{
continue;
}
PluginDescriptor pluginDescriptor = clazz.getAnnotation(PluginDescriptor.class);
if (pluginDescriptor == null)
@@ -462,6 +481,8 @@ public class PluginManager
throw new PluginInstantiationException(ex);
}
groups.broadcastSring("STARTPLUGIN;" + plugin.getClass().getSimpleName() + ";" + settingsFileInput.getAbsolutePath());
return true;
}
@@ -491,6 +512,8 @@ public class PluginManager
throw new PluginInstantiationException(ex);
}
groups.broadcastSring("STOPPLUGIN;" + plugin.getClass().getSimpleName() + ";" + settingsFileInput.getAbsolutePath());
return true;
}
@@ -661,4 +684,77 @@ public class PluginManager
incrementChildren(graph, dependencyCount, m, val + 1));
}
}
public void receive(Message message)
{
if (message.getObject() instanceof ConfigChanged)
{
return;
}
String[] messageObject = ((String) message.getObject()).split(";");
if (messageObject.length < 3)
{
return;
}
String command = messageObject[0];
String pluginName = messageObject[1];
String path = messageObject[2];
Plugin plugin = null;
if (!path.equals(settingsFileInput.getAbsolutePath()))
{
return;
}
for (Plugin pl : getPlugins())
{
if (pl.getClass().getSimpleName().equals(pluginName))
{
plugin = pl;
break;
}
}
if (plugin == null)
{
return;
}
Plugin finalPlugin = plugin;
switch (command)
{
case "STARTPLUGIN":
try
{
startPlugin(finalPlugin);
}
catch (PluginInstantiationException e)
{
log.warn("unable to start plugin", e);
throw new RuntimeException(e);
}
break;
case "STOPPLUGIN":
try
{
stopPlugin(finalPlugin);
}
catch (PluginInstantiationException e)
{
log.warn("unable to stop plugin", e);
throw new RuntimeException(e);
}
break;
}
}
}

View File

@@ -30,6 +30,7 @@ import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.ActionListener;
@@ -78,6 +79,7 @@ import javax.swing.plaf.basic.BasicSpinnerUI;
import javax.swing.text.JTextComponent;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.util.Text;
import net.runelite.client.RuneLite;
import net.runelite.client.config.Button;
import net.runelite.client.config.ConfigDescriptor;
import net.runelite.client.config.ConfigItem;
@@ -90,7 +92,9 @@ import net.runelite.client.config.ModifierlessKeybind;
import net.runelite.client.config.Range;
import net.runelite.client.config.Units;
import net.runelite.client.eventbus.EventBus;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.events.PluginChanged;
import net.runelite.client.plugins.ExternalPluginManager;
import net.runelite.client.plugins.PluginManager;
import net.runelite.client.ui.ColorScheme;
import net.runelite.client.ui.DynamicGridLayout;
@@ -102,6 +106,7 @@ import net.runelite.client.ui.components.colorpicker.ColorPickerManager;
import net.runelite.client.ui.components.colorpicker.RuneliteColorPicker;
import net.runelite.client.util.ColorUtil;
import net.runelite.client.util.ImageUtil;
import net.runelite.client.util.LinkBrowser;
import net.runelite.client.util.MiscUtils;
import net.runelite.client.util.SwingUtil;
@@ -132,6 +137,9 @@ class ConfigPanel extends PluginPanel
@Inject
private PluginManager pluginManager;
@Inject
private ExternalPluginManager externalPluginManager;
@Inject
private ColorPickerManager colorPickerManager;
@@ -233,6 +241,7 @@ class ConfigPanel extends PluginPanel
rebuild(false);
eventBus.subscribe(PluginChanged.class, this, this::onPluginChanged);
eventBus.subscribe(ConfigChanged.class, this, this::onConfigChanged);
}
private void getSections(ConfigDescriptor cd)
@@ -425,11 +434,44 @@ class ConfigPanel extends PluginPanel
ConfigDescriptor cd = pluginConfig.getConfigDescriptor();
assert cd != null;
List<JButton> buttons = new ArrayList<>();
Map<String, Map<String, String>> pluginsInfoMap = externalPluginManager.getPluginsInfoMap();
if (pluginConfig.getPlugin() != null && pluginsInfoMap.containsKey(pluginConfig.getPlugin().getClass().getSimpleName()))
{
JPanel infoPanel = new JPanel();
infoPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR);
infoPanel.setBorder(new EmptyBorder(10, 10, 10, 10));
infoPanel.setLayout(new GridLayout(0, 1));
final Font smallFont = FontManager.getRunescapeSmallFont();
Map<String, String> pluginInfo = pluginsInfoMap.get(pluginConfig.getPlugin().getClass().getSimpleName());
JLabel idLabel = new JLabel(htmlLabel("id", pluginInfo.get("id")));
idLabel.setFont(smallFont);
infoPanel.add(idLabel);
JLabel versionLabel = new JLabel(htmlLabel("version", pluginInfo.get("version")));
versionLabel.setFont(smallFont);
infoPanel.add(versionLabel);
JLabel providerLabel = new JLabel(htmlLabel("provider", pluginInfo.get("provider")));
providerLabel.setFont(smallFont);
infoPanel.add(providerLabel);
JButton button = new JButton("Support");
button.addActionListener(e -> LinkBrowser.browse(pluginInfo.get("support")));
buttons.add(button);
mainPanel.add(infoPanel);
}
getSections(cd);
getTitleSections(cd);
List<JButton> buttons = new ArrayList<>();
for (ConfigItemDescriptor cid : cd.getItems())
{
if (cid == null)
@@ -1168,4 +1210,25 @@ class ConfigPanel extends PluginPanel
pluginToggle.setSelected(event.isLoaded()));
}
}
public void onConfigChanged(ConfigChanged event)
{
if (event.getOrigin().equals(RuneLite.uuid))
{
return;
}
try
{
SwingUtilities.invokeAndWait(() -> rebuild(true));
}
catch (InterruptedException | InvocationTargetException e)
{
}
}
private static String htmlLabel(String key, String value)
{
return "<html><body style = 'color:#a5a5a5'>" + key + ": <span style = 'color:white'>" + value + "</span></body></html>";
}
}

View File

@@ -34,13 +34,19 @@ import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JToggleButton;
import javax.swing.SwingWorker;
import lombok.Getter;
import net.runelite.client.RuneLiteProperties;
import net.runelite.client.plugins.ExternalPluginManager;
import net.runelite.client.plugins.PluginType;
import net.runelite.client.ui.ClientUI;
import net.runelite.client.ui.ColorScheme;
import net.runelite.client.ui.PluginPanel;
import net.runelite.client.util.ImageUtil;
@@ -50,6 +56,8 @@ public class PluginListItem extends JPanel
{
private static final ImageIcon CONFIG_ICON;
private static final ImageIcon CONFIG_ICON_HOVER;
private static final ImageIcon REFRESH_ICON;
private static final ImageIcon REFRESH_ICON_HOVER;
private static final ImageIcon ON_STAR;
private static final ImageIcon OFF_STAR;
@@ -70,10 +78,13 @@ public class PluginListItem extends JPanel
static
{
BufferedImage configIcon = ImageUtil.getResourceStreamFromClass(ConfigPanel.class, "config_edit_icon.png");
BufferedImage refreshIcon = ImageUtil.getResourceStreamFromClass(ConfigPanel.class, "refresh.png");
BufferedImage onStar = ImageUtil.getResourceStreamFromClass(ConfigPanel.class, "star_on.png");
CONFIG_ICON = new ImageIcon(configIcon);
REFRESH_ICON = new ImageIcon(refreshIcon);
ON_STAR = new ImageIcon(ImageUtil.recolorImage(onStar, ColorScheme.BRAND_BLUE));
CONFIG_ICON_HOVER = new ImageIcon(ImageUtil.luminanceOffset(configIcon, -100));
REFRESH_ICON_HOVER = new ImageIcon(ImageUtil.luminanceOffset(refreshIcon, -100));
BufferedImage offStar = ImageUtil.luminanceScale(
ImageUtil.grayscaleImage(onStar),
@@ -82,7 +93,7 @@ public class PluginListItem extends JPanel
OFF_STAR = new ImageIcon(offStar);
}
PluginListItem(PluginListPanel pluginListPanel, PluginConfigurationDescriptor pluginConfig)
PluginListItem(PluginListPanel pluginListPanel, PluginConfigurationDescriptor pluginConfig, ExternalPluginManager externalPluginManager)
{
this.pluginListPanel = pluginListPanel;
this.pluginConfig = pluginConfig;
@@ -119,6 +130,55 @@ public class PluginListItem extends JPanel
buttonPanel.setLayout(new GridLayout(1, 2));
add(buttonPanel, BorderLayout.LINE_END);
Map<String, Map<String, String>> pluginsInfoMap = externalPluginManager.getPluginsInfoMap();
if (RuneLiteProperties.getLauncherVersion() == null && pluginConfig.getPlugin() != null && pluginsInfoMap.containsKey(pluginConfig.getPlugin().getClass().getSimpleName()))
{
JButton hotSwapButton = new JButton(REFRESH_ICON);
hotSwapButton.setRolloverIcon(REFRESH_ICON_HOVER);
SwingUtil.removeButtonDecorations(hotSwapButton);
hotSwapButton.setPreferredSize(new Dimension(25, 0));
hotSwapButton.setVisible(false);
buttonPanel.add(hotSwapButton);
hotSwapButton.addActionListener(e ->
{
Map<String, String> pluginInfo = pluginsInfoMap.get(pluginConfig.getPlugin().getClass().getSimpleName());
String pluginId = pluginInfo.get("id");
hotSwapButton.setIcon(REFRESH_ICON);
externalPluginManager.uninstall(pluginId);
SwingWorker<Boolean, Void> worker = new SwingWorker<>()
{
@Override
protected Boolean doInBackground()
{
return externalPluginManager.uninstall(pluginId);
}
};
worker.execute();
JOptionPane.showMessageDialog(ClientUI.getFrame(),
pluginId + " is unloaded, put the new jar file in the externalmanager folder and click `ok`",
"Hotswap " + pluginId,
JOptionPane.INFORMATION_MESSAGE);
worker = new SwingWorker<>()
{
@Override
protected Boolean doInBackground()
{
return externalPluginManager.reloadStart(pluginId);
}
};
worker.execute();
});
hotSwapButton.setVisible(true);
hotSwapButton.setToolTipText("Hotswap plugin");
}
if (pluginConfig.hasConfigurables())
{
JButton configButton = new JButton(CONFIG_ICON);
@@ -211,8 +271,6 @@ public class PluginListItem extends JPanel
*/
static void addLabelMouseOver(final JLabel label)
{
final Color labelForeground = label.getForeground();
label.addMouseListener(new MouseAdapter()
{
private Color lastForeground;

View File

@@ -62,6 +62,7 @@ import javax.swing.event.DocumentListener;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.runelite.api.util.Text;
import net.runelite.client.RuneLite;
import net.runelite.client.config.Config;
import net.runelite.client.config.ConfigDescriptor;
import net.runelite.client.config.ConfigGroup;
@@ -73,6 +74,7 @@ import net.runelite.client.events.ConfigChanged;
import net.runelite.client.events.ExternalPluginChanged;
import net.runelite.client.events.ExternalPluginsLoaded;
import net.runelite.client.events.PluginChanged;
import net.runelite.client.plugins.ExternalPluginManager;
import net.runelite.client.plugins.Plugin;
import net.runelite.client.plugins.PluginDescriptor;
import net.runelite.client.plugins.PluginInstantiationException;
@@ -110,6 +112,7 @@ public class PluginListPanel extends PluginPanel
private final PluginManager pluginManager;
private final Provider<ConfigPanel> configPanelProvider;
private final OpenOSRSConfig openOSRSConfig;
private final ExternalPluginManager externalPluginManager;
@Getter
private final MultiplexingPluginPanel muxer;
@@ -140,6 +143,7 @@ public class PluginListPanel extends PluginPanel
PluginManager pluginManager,
Provider<ConfigPanel> configPanelProvider,
OpenOSRSConfig openOSRSConfig,
ExternalPluginManager externalPluginManager,
EventBus eventBus)
{
super(false);
@@ -148,8 +152,14 @@ public class PluginListPanel extends PluginPanel
this.pluginManager = pluginManager;
this.configPanelProvider = configPanelProvider;
this.openOSRSConfig = openOSRSConfig;
this.externalPluginManager = externalPluginManager;
eventBus.subscribe(ConfigChanged.class, this, ev -> {
if (ev.getGroup().equals("runelite") && ev.getKey().equals("pinnedPlugins") && !ev.getOrigin().equals(RuneLite.uuid))
{
SwingUtilities.invokeLater(this::rebuildPluginList);
}
if (!ev.getGroup().equals("openosrs"))
{
return;
@@ -157,7 +167,7 @@ public class PluginListPanel extends PluginPanel
if (ev.getKey().equals("enableCategories") || ev.getKey().equals("pluginSortMode"))
{
rebuildPluginList();
SwingUtilities.invokeLater(this::rebuildPluginList);
}
if (ev.getKey().equals("pluginSortMode"))
@@ -264,7 +274,7 @@ public class PluginListPanel extends PluginPanel
})
).map(desc ->
{
PluginListItem listItem = new PluginListItem(this, desc);
PluginListItem listItem = new PluginListItem(this, desc, externalPluginManager);
listItem.setPinned(pinnedPlugins.contains(desc.getName()));
listItem.setColor(getColorByCategory(listItem.getPluginType()));

View File

@@ -67,6 +67,11 @@ public class RuneLiteSplashScreen extends JFrame
this.setVisible(true);
}
public static boolean showing()
{
return INSTANCE != null;
}
public static void setError(String title, String content)
{
if (INSTANCE != null)

View File

@@ -34,9 +34,6 @@ import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
@@ -54,9 +51,6 @@ import net.runelite.client.util.LinkBrowser;
@Slf4j
public class InfoPanel extends JPanel
{
private static final String RUNELITE_VERSION = "runelite.version";
private static final String RUNELITE_PLUS_VERSION = "open.osrs.version";
private static final String RUNELITE_PLUS_DATE = "open.osrs.builddate";
private static final Color DARK_GREY = new Color(10, 10, 10, 255);
private static final BufferedImage TRANSPARENT_LOGO = ImageUtil.getResourceStreamFromClass(InfoPanel.class, "/openosrs.png");
@@ -67,16 +61,6 @@ public class InfoPanel extends JPanel
public InfoPanel()
{
Properties properties = new Properties();
try (InputStream in = getClass().getResourceAsStream("/open.osrs.properties"))
{
properties.load(in);
}
catch (IOException ex)
{
log.warn("unable to load propertries", ex);
}
this.setLayout(new GridBagLayout());
this.setPreferredSize(PANEL_SIZE);
this.setBackground(new Color(38, 38, 38));
@@ -104,11 +88,11 @@ public class InfoPanel extends JPanel
c.weighty = 0;
// OpenOSRS version
this.add(createPanelTextButton("OpenOSRS Version: " + properties.getProperty(RUNELITE_PLUS_VERSION)), c);
this.add(createPanelTextButton("OpenOSRS Version: " + RuneLiteProperties.getPlusVersion()), c);
c.gridy++;
// Build date
this.add(createPanelTextButton("Build date: " + properties.getProperty(RUNELITE_PLUS_DATE)), c);
this.add(createPanelTextButton("Build date: " + RuneLiteProperties.getPlusDate()), c);
c.gridy++;
final JLabel logsFolder = createPanelButton("Open logs folder", null, () -> LinkBrowser.openLocalFile(LOGS_DIR));

View File

@@ -1,64 +0,0 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 by rumatoest at github.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.runelite.client.util;
/**
* The Class AppLock.
*
* @author Vladislav Zablotsky
*/
public class AppLock
{
private static CrossLock lockInstance;
/**
* Set lock for application instance.
* Method must be run only one time at application start.
*
* @param lockId Unique lock identifiers
* @return true if succeeded
*/
public synchronized boolean lock(String lockId)
{
if (lockInstance == null)
{
lockInstance = new CrossLock("application_" + lockId);
}
return lockInstance.lock();
}
/**
* Trying to release application lock.
* Thus another application instances will be able to use lock with current ID.
*/
public synchronized void release()
{
if (lockInstance != null)
{
lockInstance.clear();
}
lockInstance = null;
}
}

View File

@@ -1,228 +0,0 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2018 by rumatoest at github.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package net.runelite.client.util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import static net.runelite.client.RuneLite.RUNELITE_DIR;
/**
* Universal cross application instances locker.
* Allow you to create simple lock like object which can be used for
* different application instances. Basic idea is simple - just a simple file lock.
* <br />
* All you need is to define unique key for each lock type.
*
* @author Vladislav Zablotsky
*/
public class CrossLock
{
private static final HashMap<String, CrossLock> locks = new HashMap<>();
private final String id;
private final File fileToLock;
private FileOutputStream fileStream;
private FileChannel fileStreamChannel;
private FileLock lockOnFile;
/**
* Will create or retrieve lock instance.
* Each lock id is unique among all you instances,
* thus only one instance can acquire lock for this id.
*
* @param lockId Unique lock identifier
* @return Not null
*/
public static CrossLock get(String lockId)
{
if (locks.containsKey(lockId))
{
return locks.get(lockId);
}
else
{
synchronized (CrossLock.class)
{
if (locks.containsKey(lockId))
{
return locks.get(lockId);
}
else
{
CrossLock cl = new CrossLock(lockId);
locks.put(lockId, cl);
return cl;
}
}
}
}
/**
* Will remove lock object for specific id and release lock if any.
*
* @param lockId Unique lock identifier
*/
public static void remove(String lockId)
{
if (locks.containsKey(lockId))
{
CrossLock lock = null;
synchronized (CrossLock.class)
{
if (locks.containsKey(lockId))
{
lock = locks.remove(lockId);
}
}
if (lock != null)
{
lock.release();
}
}
}
CrossLock(String lockId)
{
this.id = lockId;
fileToLock = new File(RUNELITE_DIR, lockId + ".app_lock");
}
/**
* Return lock instance identifier.
*/
public String id()
{
return this.id;
}
/**
* Activate lock.
* Note! This is only cross application (cross instances) lock. It will not work
* as lock inside single application instance.
*
* @return true if lock was acquire or false
*/
public synchronized boolean lock()
{
if (lockOnFile != null && lockOnFile.isValid())
{
return true;
}
else
{
release();
}
String lockContent = "#Java AppLock Object\n#Locked by key: " + id() + "\r\n";
try
{
if (fileToLock.exists())
{
fileToLock.createNewFile();
}
fileStream = new FileOutputStream(fileToLock);
fileStreamChannel = fileStream.getChannel();
lockOnFile = fileStreamChannel.tryLock();
if (lockOnFile != null)
{
fileStream.write(lockContent.getBytes());
}
}
catch (Exception ex)
{
if (!(ex instanceof OverlappingFileLockException))
{
Logger.getLogger(AppLock.class.getName()).log(Level.WARNING,
"Can not get application lock for id=" + id() + "\n" + ex.getMessage(), ex);
}
return false;
}
return lockOnFile != null;
}
/**
* Release lock associated with this object.
*/
public synchronized void release()
{
try
{
if (lockOnFile != null && lockOnFile.isValid())
{
lockOnFile.release();
}
lockOnFile = null;
if (fileStream != null)
{
fileStream.close();
fileStream = null;
}
if (fileStreamChannel != null && fileStreamChannel.isOpen())
{
fileStreamChannel.close();
}
fileStreamChannel = null;
}
catch (IOException ex)
{
Logger.getLogger(AppLock.class.getName()).log(Level.WARNING,
"Can not get application lock for id=" + id() + "\n" + ex.getMessage(), ex);
}
}
/**
* Release lock and remove lock file.
*/
public synchronized void clear()
{
release();
if (fileToLock.exists())
{
fileToLock.delete();
}
}
@Override
protected void finalize() throws Throwable
{
this.clear();
super.finalize();
}
}

View File

@@ -0,0 +1,143 @@
package net.runelite.client.util;
import io.reactivex.rxjava3.subjects.PublishSubject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Singleton;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.runelite.client.RuneLite;
import net.runelite.client.config.OpenOSRSConfig;
import net.runelite.client.events.ConfigChanged;
import net.runelite.client.ui.RuneLiteSplashScreen;
import org.jgroups.Address;
import org.jgroups.JChannel;
import org.jgroups.Message;
import org.jgroups.ReceiverAdapter;
import org.jgroups.View;
import org.jgroups.util.Util;
@Slf4j
@Singleton
public class Groups extends ReceiverAdapter
{
private final OpenOSRSConfig openOSRSConfig;
private final JChannel channel;
@Getter(AccessLevel.PUBLIC)
private int instanceCount;
@Getter(AccessLevel.PUBLIC)
private Map<String, List<Address>> messageMap = new HashMap<>();
@Getter(AccessLevel.PUBLIC)
private final PublishSubject<Message> messageStringSubject = PublishSubject.create();
@Getter(AccessLevel.PUBLIC)
private final PublishSubject<Message> messageObjectSubject = PublishSubject.create();
@Inject
public Groups(OpenOSRSConfig openOSRSConfig) throws Exception
{
this.openOSRSConfig = openOSRSConfig;
this.channel = new JChannel(RuneLite.class.getResourceAsStream("/udp.xml"))
.setName(RuneLite.uuid)
.setReceiver(this)
.setDiscardOwnMessages(true)
.connect("openosrs");
}
public void broadcastSring(String command)
{
send(null, command);
}
public void sendConfig(Address destination, ConfigChanged configChanged)
{
if (!openOSRSConfig.localSync() || RuneLiteSplashScreen.showing() || instanceCount < 2)
{
return;
}
try
{
byte[] buffer = Util.objectToByteBuffer(configChanged);
Message message = new Message()
.setDest(destination)
.setBuffer(buffer);
channel.send(message);
}
catch (Exception e)
{
e.printStackTrace();
}
}
public void sendString(String command)
{
String[] messageObject = command.split(";");
String pluginId = messageObject[1];
messageMap.put(pluginId, new ArrayList<>());
for (Address member : channel.getView().getMembers())
{
if (member.toString().equals(RuneLite.uuid))
{
continue;
}
messageMap.get(pluginId).add(member);
send(member, command);
}
}
public void send(Address destination, String command)
{
if (!openOSRSConfig.localSync() || RuneLiteSplashScreen.showing() || instanceCount < 2)
{
return;
}
try
{
channel.send(new Message(destination, command));
}
catch (Exception e)
{
e.printStackTrace();
}
}
@Override
public void viewAccepted(View view)
{
instanceCount = view.getMembers().size();
}
@Override
public void receive(Message message)
{
if (RuneLiteSplashScreen.showing())
{
return;
}
if (message.getObject() instanceof String)
{
messageStringSubject.onNext(message);
}
else
{
messageObjectSubject.onNext(message);
}
}
public void close()
{
channel.close();
}
}

View File

@@ -69,4 +69,5 @@
</root>
<logger name="org.pf4j.AbstractPluginManager" level="OFF"/>
<logger name="org.jgroups" level="ERROR"/>
</configuration>

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 B

View File

@@ -0,0 +1,54 @@
<!--
Default stack using IP multicasting. It is similar to the "udp"
stack in stacks.xml, but doesn't use streaming state transfer and flushing
author: Bela Ban
-->
<config xmlns="urn:org:jgroups"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:org:jgroups http://www.jgroups.org/schema/jgroups.xsd">
<UDP
mcast_port="${jgroups.udp.mcast_port:45588}"
ip_ttl="4"
tos="8"
ucast_recv_buf_size="5M"
ucast_send_buf_size="5M"
mcast_recv_buf_size="5M"
mcast_send_buf_size="5M"
max_bundle_size="64K"
enable_diagnostics="true"
thread_naming_pattern="cl"
thread_pool.min_threads="0"
thread_pool.max_threads="20"
thread_pool.keep_alive_time="30000"/>
<PING/>
<MERGE3 max_interval="30000"
min_interval="10000"/>
<FD_SOCK/>
<FD_ALL/>
<VERIFY_SUSPECT timeout="1500"/>
<BARRIER/>
<pbcast.NAKACK2 xmit_interval="500"
xmit_table_num_rows="100"
xmit_table_msgs_per_row="2000"
xmit_table_max_compaction_time="30000"
use_mcast_xmit="false"
discard_delivered_msgs="true"/>
<UNICAST3 xmit_interval="500"
xmit_table_num_rows="100"
xmit_table_msgs_per_row="2000"
xmit_table_max_compaction_time="60000"
conn_expiry_timeout="0"/>
<pbcast.STABLE desired_avg_gossip="50000"
max_bytes="4M"/>
<pbcast.GMS print_local_addr="false" join_timeout="2000"/>
<UFC max_credits="2M"
min_threshold="0.4"/>
<MFC max_credits="2M"
min_threshold="0.4"/>
<FRAG2 frag_size="60K"/>
<RSVP resend_interval="2000" timeout="10000"/>
<pbcast.STATE_TRANSFER/>
</config>

View File

@@ -116,7 +116,7 @@ public class PluginManagerTest
@Test
public void testLoadPlugins() throws Exception
{
PluginManager pluginManager = new PluginManager(null, null, null, null);
PluginManager pluginManager = new PluginManager(null, null, null, null, null, null);
pluginManager.setOutdated(true);
pluginManager.loadCorePlugins();
Collection<Plugin> plugins = pluginManager.getPlugins();
@@ -127,7 +127,7 @@ public class PluginManagerTest
.count();
assertEquals(expected, plugins.size());
pluginManager = new PluginManager(null, null, null, null);
pluginManager = new PluginManager(null, null, null, null, null, null);
pluginManager.loadCorePlugins();
plugins = pluginManager.getPlugins();
@@ -145,7 +145,7 @@ public class PluginManagerTest
modules.add(new GraphvizModule());
modules.add(new RuneLiteModule(() -> null, RuneLite.DEFAULT_CONFIG_FILE));
PluginManager pluginManager = new PluginManager(null, null, null, null);
PluginManager pluginManager = new PluginManager(null, null, null, null, null, null);
pluginManager.loadCorePlugins();
modules.addAll(pluginManager.getPlugins());
@@ -194,10 +194,10 @@ public class PluginManagerTest
}
@Test
public void testEventbusAnnotations() throws PluginInstantiationException
public void testEventbusAnnotations() throws Exception
{
EventBus eventbus = new EventBus();
PluginManager pluginManager = new PluginManager(eventbus, null, null, null)
PluginManager pluginManager = new PluginManager(eventbus, null, null, null, null, null)
{
@Override
public boolean isPluginEnabled(Plugin plugin)