/* * Copyright (c) 2020, Owain van Brakel * 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 pluginClassLoaders = new ArrayList<>(); private final net.runelite.client.plugins.PluginManager runelitePluginManager; private org.pf4j.PluginManager externalPluginManager; @Getter(AccessLevel.PUBLIC) private final List repositories = new ArrayList<>(); private final OpenOSRSConfig openOSRSConfig; private final EventBus eventBus; private final ExecutorService executorService; private final ConfigManager configManager; private final Map pluginsMap = new HashMap<>(); @Getter(AccessLevel.PUBLIC) private final Map> 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 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 Predicate not(Predicate t) { return t.negate(); } public void startExternalPluginManager() { try { externalPluginManager.loadPlugins(); } catch (Exception ex) { if (ex instanceof DependencyResolver.DependenciesNotFoundException) { List 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. *

* 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 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 plugins, boolean init, boolean initConfig) { OpenOSRSSplashScreen.stage(.66, "Loading external plugins"); MutableGraph> graph = GraphBuilder .directed() .build(); for (Plugin plugin : plugins) { Class 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 pluginClass = (Class) clazz; graph.addNode(pluginClass); } // Build plugin graph for (Class 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) pluginDependency.value()); } } } if (Graphs.hasCycle(graph)) { throw new RuntimeException("Plugin dependency graph contains a cycle!"); } List>> sortedPlugins = PluginManager.topologicalGroupSort(graph); sortedPlugins = Lists.reverse(sortedPlugins); AtomicInteger loaded = new AtomicInteger(); final long start = System.currentTimeMillis(); List scannedPlugins = new CopyOnWriteArrayList<>(); sortedPlugins.forEach(group -> { List> curGroup = new ArrayList<>(); group.forEach(pluginClazz -> curGroup.add(executorService.submit(() -> { Plugin plugininst; try { //noinspection unchecked plugininst = instantiate(scannedPlugins, (Class) 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 scannedPlugins, Class clazz, boolean init, boolean initConfig) throws PluginInstantiationException { net.runelite.client.plugins.PluginDependency[] pluginDependencies = clazz.getAnnotationsByType(net.runelite.client.plugins.PluginDependency.class); List deps = new ArrayList<>(); for (net.runelite.client.plugins.PluginDependency pluginDependency : pluginDependencies) { Optional 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 modules = new ArrayList<>(deps.size()); for (Plugin p : deps) { // Create a module for each dependency Module module = (Binder binder) -> { binder.bind((Class) 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 startedPlugins, List 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 startedPlugins = getStartedPlugins(); List 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 loadPlugin(String pluginId) { List scannedPlugins = new ArrayList<>(); try { List 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 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 startedPlugins = List.copyOf(getStartedPlugins()); for (PluginWrapper pluginWrapper : startedPlugins) { if (!pluginId.equals(pluginWrapper.getDescriptor().getPluginId())) { continue; } List 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 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 getDependencies() { Set deps = new HashSet<>(); List startedPlugins = getStartedPlugins(); for (PluginWrapper pluginWrapper : startedPlugins) { for (PluginDependency pluginDependency : pluginWrapper.getDescriptor().getDependencies()) { deps.add(pluginDependency.getPluginId()); } } return deps; } public List getDisabledPlugins() { return externalPluginManager.getResolvedPlugins() .stream() .filter(not(externalPluginManager.getStartedPlugins()::contains)) .map(PluginWrapper::getPluginId) .collect(Collectors.toList()); } public List getStartedPlugins() { return externalPluginManager.getStartedPlugins(); } public Boolean reloadStart(String pluginId) { externalPluginManager.loadPlugins(); externalPluginManager.startPlugin(pluginId); List startedPlugins = List.copyOf(getStartedPlugins()); List 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 startedPlugins = List.copyOf(getStartedPlugins()); List 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; } } }