1074 lines
28 KiB
Java
1074 lines
28 KiB
Java
/*
|
|
* 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 com.openosrs.client.plugins;
|
|
|
|
import com.google.common.collect.Lists;
|
|
import com.google.common.graph.GraphBuilder;
|
|
import com.google.common.graph.Graphs;
|
|
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.lang.reflect.InvocationTargetException;
|
|
import java.net.MalformedURLException;
|
|
import java.net.URL;
|
|
import java.nio.file.Path;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.Set;
|
|
import java.util.concurrent.CopyOnWriteArrayList;
|
|
import java.util.concurrent.ExecutionException;
|
|
import java.util.concurrent.ExecutorService;
|
|
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.Named;
|
|
import javax.inject.Singleton;
|
|
import javax.swing.JOptionPane;
|
|
import lombok.AccessLevel;
|
|
import lombok.Getter;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import net.runelite.client.RuneLite;
|
|
import static com.openosrs.client.OpenOSRS.EXTERNALPLUGIN_DIR;
|
|
import static com.openosrs.client.OpenOSRS.SYSTEM_VERSION;
|
|
import net.runelite.client.config.Config;
|
|
import net.runelite.client.config.ConfigManager;
|
|
import com.openosrs.client.config.OpenOSRSConfig;
|
|
import net.runelite.client.config.RuneLiteConfig;
|
|
import net.runelite.client.eventbus.EventBus;
|
|
import net.runelite.client.events.ConfigChanged;
|
|
import com.openosrs.client.events.ExternalPluginChanged;
|
|
import com.openosrs.client.events.ExternalRepositoryChanged;
|
|
import net.runelite.client.plugins.PluginDescriptor;
|
|
import net.runelite.client.plugins.PluginInstantiationException;
|
|
import net.runelite.client.plugins.PluginManager;
|
|
import net.runelite.client.ui.ClientUI;
|
|
import com.openosrs.client.ui.OpenOSRSSplashScreen;
|
|
import com.openosrs.client.util.Groups;
|
|
import com.openosrs.client.util.MiscUtils;
|
|
import com.openosrs.client.util.SwingUtil;
|
|
import org.jgroups.Message;
|
|
import org.pf4j.DefaultPluginManager;
|
|
import org.pf4j.DependencyResolver;
|
|
import org.pf4j.PluginDependency;
|
|
import org.pf4j.PluginRuntimeException;
|
|
import org.pf4j.PluginWrapper;
|
|
import org.pf4j.update.DefaultUpdateRepository;
|
|
import org.pf4j.update.PluginInfo;
|
|
import org.pf4j.update.UpdateManager;
|
|
import org.pf4j.update.UpdateRepository;
|
|
import org.pf4j.update.VerifyException;
|
|
|
|
@Slf4j
|
|
@Singleton
|
|
public class ExternalPluginManager
|
|
{
|
|
public static final String DEFAULT_PLUGIN_REPOS = "OpenOSRS:https://raw.githubusercontent.com/open-osrs/plugin-hosting/master/";
|
|
static final String DEVELOPMENT_MANIFEST_PATH = "build/tmp/jar/MANIFEST.MF";
|
|
|
|
public static ArrayList<ClassLoader> pluginClassLoaders = new ArrayList<>();
|
|
private final net.runelite.client.plugins.PluginManager runelitePluginManager;
|
|
private org.pf4j.PluginManager externalPluginManager;
|
|
@Getter(AccessLevel.PUBLIC)
|
|
private final List<UpdateRepository> repositories = new ArrayList<>();
|
|
private final OpenOSRSConfig openOSRSConfig;
|
|
private final EventBus eventBus;
|
|
private final ExecutorService executorService;
|
|
private final ConfigManager configManager;
|
|
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 final boolean safeMode;
|
|
|
|
@Inject
|
|
public ExternalPluginManager(
|
|
@Named("safeMode") final boolean safeMode,
|
|
net.runelite.client.plugins.PluginManager pluginManager,
|
|
OpenOSRSConfig openOSRSConfig,
|
|
EventBus eventBus,
|
|
ExecutorService executorService,
|
|
ConfigManager configManager,
|
|
Groups groups)
|
|
{
|
|
this.safeMode = safeMode;
|
|
this.runelitePluginManager = pluginManager;
|
|
this.openOSRSConfig = openOSRSConfig;
|
|
this.eventBus = eventBus;
|
|
this.executorService = executorService;
|
|
this.configManager = configManager;
|
|
this.groups = groups;
|
|
|
|
//noinspection ResultOfMethodCallIgnored
|
|
EXTERNALPLUGIN_DIR.mkdirs();
|
|
|
|
initPluginManager();
|
|
|
|
groups.getMessageStringSubject()
|
|
.subscribe(this::receive);
|
|
}
|
|
|
|
private void initPluginManager()
|
|
{
|
|
externalPluginManager = new ExternalPf4jPluginManager(this);
|
|
externalPluginManager.setSystemVersion(SYSTEM_VERSION);
|
|
}
|
|
|
|
public boolean doesGhRepoExist(String owner, String name)
|
|
{
|
|
return doesRepoExist("gh:" + owner + "/" + name);
|
|
}
|
|
|
|
/**
|
|
* Note that {@link UpdateManager#addRepository} checks if the repo exists, however it throws an exception which is bad
|
|
*/
|
|
public boolean doesRepoExist(String id)
|
|
{
|
|
return repositories.stream().anyMatch((repo) -> repo.getId().equals(id));
|
|
}
|
|
|
|
private static URL toRepositoryUrl(String owner, String name) throws MalformedURLException
|
|
{
|
|
return new URL("https://raw.githubusercontent.com/" + owner + "/" + name + "/master/");
|
|
}
|
|
|
|
public static boolean testGHRepository(String owner, String name)
|
|
{
|
|
try
|
|
{
|
|
return testRepository(toRepositoryUrl(owner, name));
|
|
}
|
|
catch (MalformedURLException e)
|
|
{
|
|
e.printStackTrace();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public static boolean testRepository(URL url)
|
|
{
|
|
final List<UpdateRepository> repositories = new ArrayList<>();
|
|
repositories.add(new DefaultUpdateRepository("repository-testing", url));
|
|
DefaultPluginManager testPluginManager = new DefaultPluginManager(EXTERNALPLUGIN_DIR.toPath());
|
|
UpdateManager updateManager = new UpdateManager(testPluginManager, repositories);
|
|
|
|
return updateManager.getPlugins().size() <= 0;
|
|
}
|
|
|
|
public static <T> Predicate<T> not(Predicate<T> t)
|
|
{
|
|
return t.negate();
|
|
}
|
|
|
|
public void startExternalPluginManager()
|
|
{
|
|
try
|
|
{
|
|
externalPluginManager.loadPlugins();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex instanceof DependencyResolver.DependenciesNotFoundException)
|
|
{
|
|
List<String> deps = ((DependencyResolver.DependenciesNotFoundException) ex).getDependencies();
|
|
|
|
log.error("The following dependencies are missing: {}", deps);
|
|
|
|
for (String dep : deps)
|
|
{
|
|
updateManager.installPlugin(dep, null);
|
|
}
|
|
|
|
startExternalPluginManager();
|
|
}
|
|
else
|
|
{
|
|
log.error("Could not load plugins", ex);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void startExternalUpdateManager()
|
|
{
|
|
if (!tryLoadNewFormat())
|
|
{
|
|
log.debug("Load new format failed.");
|
|
loadOldFormat();
|
|
}
|
|
|
|
updateManager = new UpdateManager(externalPluginManager, repositories);
|
|
saveConfig();
|
|
}
|
|
|
|
public boolean tryLoadNewFormat()
|
|
{
|
|
try
|
|
{
|
|
duplicateCheck();
|
|
log.debug("Trying to load new format: {}", openOSRSConfig.getExternalRepositories());
|
|
repositories.clear();
|
|
|
|
for (String keyval : openOSRSConfig.getExternalRepositories().split(";"))
|
|
{
|
|
String[] split = keyval.split("\\|");
|
|
if (split.length != 2)
|
|
{
|
|
log.debug("Split length invalid: {}", keyval);
|
|
repositories.clear();
|
|
return false;
|
|
}
|
|
String id = split[0];
|
|
String url = split[1];
|
|
if (!url.endsWith("/"))
|
|
{
|
|
url = url.concat("/");
|
|
}
|
|
|
|
if (id.contains("https://raw.githubusercontent.com/"))
|
|
{
|
|
id = "gh:" + id.substring(id.indexOf("https://raw.githubusercontent.com/")).replace("/master", "")
|
|
.replace("https://raw.githubusercontent.com/", "");
|
|
|
|
if (id.endsWith("/"))
|
|
{
|
|
id = id.substring(0, id.lastIndexOf("/"));
|
|
}
|
|
}
|
|
|
|
repositories.add(new DefaultUpdateRepository(id, new URL(url)));
|
|
}
|
|
}
|
|
catch (ArrayIndexOutOfBoundsException | MalformedURLException e)
|
|
{
|
|
log.error("Error in new format", e);
|
|
repositories.clear();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public void loadOldFormat()
|
|
{
|
|
try
|
|
{
|
|
log.debug("Loading old format.");
|
|
repositories.clear();
|
|
|
|
for (String keyval : openOSRSConfig.getExternalRepositories().split(";"))
|
|
{
|
|
log.debug("KeyVal: {}", keyval);
|
|
String id = keyval.substring(0, keyval.lastIndexOf(":https"));
|
|
String url = keyval.substring(keyval.lastIndexOf("https"));
|
|
|
|
DefaultUpdateRepository defaultRepo = new DefaultUpdateRepository(id, new URL(url));
|
|
repositories.add(defaultRepo);
|
|
log.debug("Added Repo: {}", defaultRepo.getUrl());
|
|
}
|
|
}
|
|
catch (MalformedURLException e)
|
|
{
|
|
log.error("Old repository format contained malformed url", e);
|
|
}
|
|
catch (StringIndexOutOfBoundsException e)
|
|
{
|
|
log.error("Error loading external repositories. They have been reset.");
|
|
openOSRSConfig.setExternalRepositories(DEFAULT_PLUGIN_REPOS);
|
|
}
|
|
|
|
updateManager = new UpdateManager(externalPluginManager, repositories);
|
|
}
|
|
|
|
public void addGHRepository(String owner, String name)
|
|
{
|
|
try
|
|
{
|
|
addRepository("gh:" + owner + "/" + name, toRepositoryUrl(owner, name));
|
|
}
|
|
catch (MalformedURLException e)
|
|
{
|
|
log.error("GitHub repostitory could not be added (owner={}, name={})", owner, name, e);
|
|
}
|
|
}
|
|
|
|
public void addRepository(String key, URL url)
|
|
{
|
|
DefaultUpdateRepository respository = new DefaultUpdateRepository(key, url);
|
|
updateManager.addRepository(respository);
|
|
eventBus.post(new ExternalRepositoryChanged(key, true));
|
|
saveConfig();
|
|
}
|
|
|
|
public void removeRepository(String owner)
|
|
{
|
|
updateManager.removeRepository(owner);
|
|
eventBus.post(new ExternalRepositoryChanged(owner, false));
|
|
saveConfig();
|
|
}
|
|
|
|
private void saveConfig()
|
|
{
|
|
StringBuilder config = new StringBuilder();
|
|
|
|
for (UpdateRepository repository : updateManager.getRepositories())
|
|
{
|
|
config.append(repository.getId());
|
|
config.append("|");
|
|
config.append(MiscUtils.urlToStringEncoded(repository.getUrl()));
|
|
config.append(";");
|
|
}
|
|
config.deleteCharAt(config.lastIndexOf(";"));
|
|
openOSRSConfig.setExternalRepositories(config.toString());
|
|
}
|
|
|
|
public void setWarning(boolean val)
|
|
{
|
|
configManager.setConfiguration("openosrs", "warning", val);
|
|
}
|
|
|
|
public boolean getWarning()
|
|
{
|
|
return openOSRSConfig.warning();
|
|
}
|
|
|
|
/**
|
|
* This method is a fail safe to ensure that no duplicate
|
|
* repositories end up getting saved to the config.
|
|
* <p>
|
|
* Configs that had duplicate repos prior to this should
|
|
* be updated and set correctly.
|
|
*/
|
|
private void duplicateCheck()
|
|
{
|
|
String[] split = openOSRSConfig.getExternalRepositories().split(";");
|
|
|
|
if (split.length <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Set<String> strings = new HashSet<>();
|
|
boolean duplicates = false;
|
|
|
|
for (String s : split)
|
|
{
|
|
if (strings.contains(s))
|
|
{
|
|
log.error("Duplicate Repo: {}", s);
|
|
duplicates = true;
|
|
continue;
|
|
}
|
|
strings.add(s);
|
|
}
|
|
|
|
if (!duplicates)
|
|
{
|
|
log.info("No duplicates found.");
|
|
return;
|
|
}
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
for (String string : strings)
|
|
{
|
|
sb.append(string);
|
|
sb.append(";");
|
|
}
|
|
|
|
sb.deleteCharAt(sb.lastIndexOf(";"));
|
|
String duplicateFix = sb.toString();
|
|
|
|
log.info("Duplicate Repos detected, setting them to: {}", duplicateFix);
|
|
openOSRSConfig.setExternalRepositories(duplicateFix);
|
|
}
|
|
|
|
private void scanAndInstantiate(List<Plugin> plugins, boolean init, boolean initConfig)
|
|
{
|
|
OpenOSRSSplashScreen.stage(.66, "Loading external plugins");
|
|
MutableGraph<Class<? extends Plugin>> graph = GraphBuilder
|
|
.directed()
|
|
.build();
|
|
|
|
for (Plugin plugin : plugins)
|
|
{
|
|
Class<? extends Plugin> clazz = plugin.getClass();
|
|
PluginDescriptor pluginDescriptor = clazz.getAnnotation(PluginDescriptor.class);
|
|
|
|
try
|
|
{
|
|
if (pluginDescriptor == null)
|
|
{
|
|
if (Plugin.class.isAssignableFrom(clazz))
|
|
{
|
|
log.warn("Class {} is a plugin, but has no plugin descriptor", clazz);
|
|
}
|
|
continue;
|
|
}
|
|
else if (!Plugin.class.isAssignableFrom(clazz))
|
|
{
|
|
log.warn("Class {} has plugin descriptor, but is not a plugin", clazz);
|
|
continue;
|
|
}
|
|
}
|
|
catch (EnumConstantNotPresentException e)
|
|
{
|
|
log.warn("{} has an invalid plugin type of {}", clazz, e.getMessage());
|
|
continue;
|
|
}
|
|
|
|
if (safeMode && !pluginDescriptor.loadInSafeMode())
|
|
{
|
|
log.debug("Disabling {} due to safe mode", clazz);
|
|
// also disable the plugin from autostarting later
|
|
configManager.unsetConfiguration(RuneLiteConfig.GROUP_NAME, clazz.getSimpleName().toLowerCase());
|
|
continue;
|
|
}
|
|
|
|
@SuppressWarnings("unchecked") Class<Plugin> pluginClass = (Class<Plugin>) clazz;
|
|
graph.addNode(pluginClass);
|
|
}
|
|
|
|
// Build plugin graph
|
|
for (Class<? extends Plugin> pluginClazz : graph.nodes())
|
|
{
|
|
net.runelite.client.plugins.PluginDependency[] pluginDependencies = pluginClazz.getAnnotationsByType(net.runelite.client.plugins.PluginDependency.class);
|
|
|
|
for (net.runelite.client.plugins.PluginDependency pluginDependency : pluginDependencies)
|
|
{
|
|
if (graph.nodes().contains(pluginDependency.value()))
|
|
{
|
|
graph.putEdge(pluginClazz, (Class<? extends Plugin>) pluginDependency.value());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Graphs.hasCycle(graph))
|
|
{
|
|
throw new RuntimeException("Plugin dependency graph contains a cycle!");
|
|
}
|
|
|
|
List<List<Class<? extends Plugin>>> sortedPlugins = PluginManager.topologicalGroupSort(graph);
|
|
sortedPlugins = Lists.reverse(sortedPlugins);
|
|
AtomicInteger loaded = new AtomicInteger();
|
|
|
|
final long start = System.currentTimeMillis();
|
|
|
|
List<Plugin> scannedPlugins = new CopyOnWriteArrayList<>();
|
|
sortedPlugins.forEach(group ->
|
|
{
|
|
List<Future<?>> curGroup = new ArrayList<>();
|
|
group.forEach(pluginClazz ->
|
|
curGroup.add(executorService.submit(() ->
|
|
{
|
|
Plugin plugininst;
|
|
try
|
|
{
|
|
//noinspection unchecked
|
|
plugininst = instantiate(scannedPlugins, (Class<Plugin>) pluginClazz, init, initConfig);
|
|
if (plugininst == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
scannedPlugins.add(plugininst);
|
|
}
|
|
catch (PluginInstantiationException e)
|
|
{
|
|
log.warn("Error instantiating plugin!", e);
|
|
return;
|
|
}
|
|
|
|
loaded.getAndIncrement();
|
|
|
|
OpenOSRSSplashScreen.stage(.67, .75, "Loading external plugins", loaded.get(), scannedPlugins.size());
|
|
})));
|
|
curGroup.forEach(future ->
|
|
{
|
|
try
|
|
{
|
|
future.get();
|
|
}
|
|
catch (InterruptedException | ExecutionException e)
|
|
{
|
|
log.warn("Could not instantiate external plugin", e);
|
|
}
|
|
});
|
|
});
|
|
|
|
log.info("External plugin instantiation took {}ms", System.currentTimeMillis() - start);
|
|
}
|
|
|
|
@SuppressWarnings("unchecked")
|
|
private Plugin instantiate(List<Plugin> scannedPlugins, Class<Plugin> clazz, boolean init, boolean initConfig)
|
|
throws PluginInstantiationException
|
|
{
|
|
net.runelite.client.plugins.PluginDependency[] pluginDependencies =
|
|
clazz.getAnnotationsByType(net.runelite.client.plugins.PluginDependency.class);
|
|
List<Plugin> deps = new ArrayList<>();
|
|
for (net.runelite.client.plugins.PluginDependency pluginDependency : pluginDependencies)
|
|
{
|
|
Optional<Plugin> dependency =
|
|
Stream.concat(runelitePluginManager.getOprsPlugins().stream(), scannedPlugins.stream())
|
|
.filter(p -> p.getClass() == pluginDependency.value()).findFirst();
|
|
if (dependency.isEmpty())
|
|
{
|
|
throw new PluginInstantiationException(
|
|
"Unmet dependency for " + clazz.getSimpleName() + ": " + pluginDependency.value().getSimpleName());
|
|
}
|
|
deps.add(dependency.get());
|
|
}
|
|
|
|
log.info("Loading plugin {}", clazz.getSimpleName());
|
|
Plugin plugin;
|
|
try
|
|
{
|
|
plugin = clazz.getDeclaredConstructor().newInstance();
|
|
}
|
|
catch (ThreadDeath e)
|
|
{
|
|
throw e;
|
|
}
|
|
catch (Throwable ex)
|
|
{
|
|
throw new PluginInstantiationException(ex);
|
|
}
|
|
|
|
try
|
|
{
|
|
Injector parent = RuneLite.getInjector();
|
|
|
|
if (deps.size() > 1)
|
|
{
|
|
List<Module> modules = new ArrayList<>(deps.size());
|
|
for (Plugin p : deps)
|
|
{
|
|
// Create a module for each dependency
|
|
Module module = (Binder binder) ->
|
|
{
|
|
binder.bind((Class<Plugin>) p.getClass()).toInstance(p);
|
|
binder.install(p);
|
|
};
|
|
modules.add(module);
|
|
}
|
|
|
|
// Create a parent injector containing all of the dependencies
|
|
parent = parent.createChildInjector(modules);
|
|
}
|
|
else if (!deps.isEmpty())
|
|
{
|
|
// With only one dependency we can simply use its injector
|
|
parent = deps.get(0).injector;
|
|
}
|
|
|
|
// Create injector for the module
|
|
Module pluginModule = (Binder binder) ->
|
|
{
|
|
// Since the plugin itself is a module, it won't bind itself, so we'll bind it here
|
|
binder.bind(clazz).toInstance(plugin);
|
|
binder.install(plugin);
|
|
};
|
|
Injector pluginInjector = parent.createChildInjector(pluginModule);
|
|
pluginInjector.injectMembers(plugin);
|
|
plugin.injector = pluginInjector;
|
|
|
|
if (initConfig)
|
|
{
|
|
for (Key<?> key : pluginInjector.getBindings().keySet())
|
|
{
|
|
Class<?> type = key.getTypeLiteral().getRawType();
|
|
if (Config.class.isAssignableFrom(type))
|
|
{
|
|
if (type.getPackageName().startsWith(plugin.getClass().getPackageName()))
|
|
{
|
|
Config config = (Config) pluginInjector.getInstance(key);
|
|
configManager.setDefaultConfiguration(config, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (init)
|
|
{
|
|
try
|
|
{
|
|
SwingUtil.syncExec(() ->
|
|
{
|
|
try
|
|
{
|
|
runelitePluginManager.add(plugin);
|
|
runelitePluginManager.startPlugin(plugin);
|
|
eventBus.post(new ExternalPluginChanged(pluginsMap.get(plugin.getClass().getSimpleName()),
|
|
plugin, true));
|
|
}
|
|
catch (PluginInstantiationException e)
|
|
{
|
|
throw new RuntimeException(e);
|
|
}
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.warn("unable to start plugin", ex);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
runelitePluginManager.add(plugin);
|
|
}
|
|
}
|
|
catch (CreationException ex)
|
|
{
|
|
throw new PluginInstantiationException(ex);
|
|
}
|
|
catch (NoClassDefFoundError ex)
|
|
{
|
|
log.error("Plugin {} is outdated", clazz.getSimpleName());
|
|
return null;
|
|
}
|
|
|
|
log.debug("Loaded plugin {}", clazz.getSimpleName());
|
|
return plugin;
|
|
}
|
|
|
|
private void checkDepsAndStart(List<PluginWrapper> startedPlugins, List<Plugin> scannedPlugins, PluginWrapper pluginWrapper)
|
|
{
|
|
boolean depsLoaded = true;
|
|
for (PluginDependency dependency : pluginWrapper.getDescriptor().getDependencies())
|
|
{
|
|
if (startedPlugins.stream().noneMatch(pl -> pl.getPluginId().equals(dependency.getPluginId())))
|
|
{
|
|
depsLoaded = false;
|
|
}
|
|
}
|
|
|
|
if (!depsLoaded)
|
|
{
|
|
// This should never happen but can crash the client
|
|
return;
|
|
}
|
|
|
|
scannedPlugins.addAll(loadPlugin(pluginWrapper.getPluginId()));
|
|
}
|
|
|
|
public void loadPlugins()
|
|
{
|
|
externalPluginManager.startPlugins();
|
|
List<PluginWrapper> startedPlugins = getStartedPlugins();
|
|
List<Plugin> scannedPlugins = new ArrayList<>();
|
|
|
|
for (PluginWrapper plugin : startedPlugins)
|
|
{
|
|
checkDepsAndStart(startedPlugins, scannedPlugins, plugin);
|
|
}
|
|
|
|
scanAndInstantiate(scannedPlugins, false, false);
|
|
|
|
if (groups.getInstanceCount() > 1)
|
|
{
|
|
for (String pluginId : getDisabledPlugins())
|
|
{
|
|
groups.sendString("STOPEXTERNAL;" + pluginId);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (String pluginId : getDisabledPlugins())
|
|
{
|
|
externalPluginManager.enablePlugin(pluginId);
|
|
externalPluginManager.deletePlugin(pluginId);
|
|
}
|
|
}
|
|
}
|
|
|
|
private List<Plugin> loadPlugin(String pluginId)
|
|
{
|
|
List<Plugin> scannedPlugins = new ArrayList<>();
|
|
try
|
|
{
|
|
List<Plugin> extensions = externalPluginManager.getExtensions(Plugin.class, pluginId);
|
|
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<>("");
|
|
|
|
updateManager.getRepositories().forEach(repository ->
|
|
repository.getPlugins().forEach((key, value) ->
|
|
{
|
|
if (key.equals(pluginId))
|
|
{
|
|
support.set(value.projectUrl);
|
|
}
|
|
}));
|
|
|
|
pluginsInfoMap.put(
|
|
plugin.getClass().getSimpleName(),
|
|
new HashMap<>()
|
|
{{
|
|
put("version", externalPluginManager.getPlugin(pluginId).getDescriptor().getVersion());
|
|
put("id", externalPluginManager.getPlugin(pluginId).getDescriptor().getPluginId());
|
|
put("provider", externalPluginManager.getPlugin(pluginId).getDescriptor().getProvider());
|
|
put("support", support.get());
|
|
}}
|
|
);
|
|
|
|
scannedPlugins.add(plugin);
|
|
}
|
|
}
|
|
catch (Throwable ex)
|
|
{
|
|
log.error("Plugin {} could not be loaded.", pluginId, ex);
|
|
}
|
|
|
|
return scannedPlugins;
|
|
}
|
|
|
|
private Path stopPlugin(String pluginId)
|
|
{
|
|
List<PluginWrapper> startedPlugins = List.copyOf(getStartedPlugins());
|
|
|
|
for (PluginWrapper pluginWrapper : startedPlugins)
|
|
{
|
|
if (!pluginId.equals(pluginWrapper.getDescriptor().getPluginId()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
List<Plugin> extensions = externalPluginManager.getExtensions(Plugin.class, pluginId);
|
|
|
|
for (Plugin plugin : runelitePluginManager.getOprsPlugins())
|
|
{
|
|
if (!extensions.get(0).getClass().getName().equals(plugin.getClass().getName()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
try
|
|
{
|
|
SwingUtil.syncExec(() ->
|
|
{
|
|
try
|
|
{
|
|
runelitePluginManager.stopPlugin(plugin);
|
|
}
|
|
catch (Exception e2)
|
|
{
|
|
throw new RuntimeException(e2);
|
|
}
|
|
});
|
|
runelitePluginManager.remove(plugin);
|
|
pluginClassLoaders.remove(plugin.getClass().getClassLoader());
|
|
|
|
eventBus.post(new ExternalPluginChanged(pluginId, plugin, false));
|
|
|
|
return pluginWrapper.getPluginPath();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.warn("unable to stop plugin", ex);
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public boolean install(String pluginId) throws VerifyException
|
|
{
|
|
if (getDisabledPlugins().contains(pluginId))
|
|
{
|
|
externalPluginManager.enablePlugin(pluginId);
|
|
externalPluginManager.startPlugin(pluginId);
|
|
|
|
groups.broadcastSring("STARTEXTERNAL;" + pluginId);
|
|
scanAndInstantiate(loadPlugin(pluginId), true, false);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (getStartedPlugins().stream().anyMatch(ev -> ev.getPluginId().equals(pluginId)))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
try
|
|
{
|
|
PluginInfo.PluginRelease latest = updateManager.getLastPluginRelease(pluginId);
|
|
|
|
// Null version returns the last release version of this plugin for given system version
|
|
if (latest == null)
|
|
{
|
|
try
|
|
{
|
|
SwingUtil.syncExec(() ->
|
|
JOptionPane.showMessageDialog(ClientUI.getFrame(),
|
|
pluginId + " is outdated and cannot be installed",
|
|
"Installation error",
|
|
JOptionPane.ERROR_MESSAGE));
|
|
}
|
|
catch (InvocationTargetException | InterruptedException ignored)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
updateManager.installPlugin(pluginId, null);
|
|
|
|
scanAndInstantiate(loadPlugin(pluginId), true, true);
|
|
|
|
groups.broadcastSring("STARTEXTERNAL;" + pluginId);
|
|
}
|
|
catch (DependencyResolver.DependenciesNotFoundException ex)
|
|
{
|
|
uninstall(pluginId);
|
|
|
|
for (String dep : ex.getDependencies())
|
|
{
|
|
install(dep);
|
|
}
|
|
|
|
install(pluginId);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public boolean uninstall(String pluginId)
|
|
{
|
|
return uninstall(pluginId, false);
|
|
}
|
|
|
|
public boolean uninstall(String pluginId, boolean skip)
|
|
{
|
|
Path pluginPath = stopPlugin(pluginId);
|
|
|
|
if (pluginPath == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
externalPluginManager.stopPlugin(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;
|
|
}
|
|
|
|
OpenOSRSSplashScreen.stage(.59, "Updating external plugins");
|
|
|
|
boolean error = false;
|
|
|
|
if (updateManager.hasUpdates())
|
|
{
|
|
List<PluginInfo> updates = updateManager.getUpdates();
|
|
for (PluginInfo plugin : updates)
|
|
{
|
|
PluginInfo.PluginRelease lastRelease = updateManager.getLastPluginRelease(plugin.id);
|
|
String lastVersion = lastRelease.version;
|
|
try
|
|
{
|
|
|
|
OpenOSRSSplashScreen.stage(.59, "Updating " + plugin.id + " to version " + lastVersion);
|
|
boolean updated = updateManager.updatePlugin(plugin.id, lastVersion);
|
|
|
|
if (!updated)
|
|
{
|
|
log.warn("Cannot update plugin '{}'", plugin.id);
|
|
error = true;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (error)
|
|
{
|
|
initPluginManager();
|
|
startExternalUpdateManager();
|
|
startExternalPluginManager();
|
|
}
|
|
}
|
|
|
|
public Set<String> getDependencies()
|
|
{
|
|
Set<String> deps = new HashSet<>();
|
|
List<PluginWrapper> startedPlugins = getStartedPlugins();
|
|
|
|
for (PluginWrapper pluginWrapper : startedPlugins)
|
|
{
|
|
for (PluginDependency pluginDependency : pluginWrapper.getDescriptor().getDependencies())
|
|
{
|
|
deps.add(pluginDependency.getPluginId());
|
|
}
|
|
}
|
|
|
|
return deps;
|
|
}
|
|
|
|
public List<String> getDisabledPlugins()
|
|
{
|
|
return externalPluginManager.getResolvedPlugins()
|
|
.stream()
|
|
.filter(not(externalPluginManager.getStartedPlugins()::contains))
|
|
.map(PluginWrapper::getPluginId)
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
public List<PluginWrapper> getStartedPlugins()
|
|
{
|
|
return externalPluginManager.getStartedPlugins();
|
|
}
|
|
|
|
public Boolean reloadStart(String pluginId)
|
|
{
|
|
externalPluginManager.loadPlugins();
|
|
externalPluginManager.startPlugin(pluginId);
|
|
|
|
List<PluginWrapper> startedPlugins = List.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 = List.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;
|
|
}
|
|
}
|
|
|
|
}
|