diff --git a/src/bungeepluginmanager/ModifiedPluginEventBus.java b/src/bungeepluginmanager/ModifiedPluginEventBus.java new file mode 100644 index 0000000..830ad1a --- /dev/null +++ b/src/bungeepluginmanager/ModifiedPluginEventBus.java @@ -0,0 +1,38 @@ +package bungeepluginmanager; + +import net.md_5.bungee.api.event.AsyncEvent; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.event.EventBus; + +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; + +public final class ModifiedPluginEventBus extends EventBus { + + private static final Set> UNCOMPLETED_EVENTS = Collections.newSetFromMap(new WeakHashMap<>()); + private static final Object LOCK = new Object(); + + static void completeIntents(Plugin plugin) { + synchronized (LOCK) { + UNCOMPLETED_EVENTS.forEach(event -> { + try { + event.completeIntent(plugin); + } catch (Exception error) { + // Ignored + } + }); + } + } + + @Override + public void post(Object event) { + if (event instanceof AsyncEvent) { + synchronized (LOCK) { + UNCOMPLETED_EVENTS.add((AsyncEvent) event); + } + } + super.post(event); + } + +} diff --git a/src/bungeepluginmanager/PluginUtils.java b/src/bungeepluginmanager/PluginUtils.java new file mode 100644 index 0000000..d574faa --- /dev/null +++ b/src/bungeepluginmanager/PluginUtils.java @@ -0,0 +1,175 @@ +package bungeepluginmanager; + +import com.google.common.collect.Multimap; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.plugin.*; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.*; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.logging.Handler; +import java.util.logging.Level; + +public final class PluginUtils { + private PluginUtils() { + throw new IllegalStateException("Utility class"); + } + + @SuppressWarnings("deprecation") + public static void unloadPlugin(Plugin plugin) { + + PluginManager pluginManager = ProxyServer.getInstance().getPluginManager(); + ClassLoader pluginClassLoader = plugin.getClass().getClassLoader(); + + try { + //call onDisable + plugin.onDisable(); + //close all log handlers + for (Handler handler : plugin.getLogger().getHandlers()) { + handler.close(); + } + } catch (Exception t) { + severe("Exception disabling plugin", t, plugin.getDescription().getName()); + } + + //unregister event handlers + pluginManager.unregisterListeners(plugin); + //unregister commands + pluginManager.unregisterCommands(plugin); + //cancel tasks + ProxyServer.getInstance().getScheduler().cancel(plugin); + //shutdown internal executor + plugin.getExecutorService().shutdownNow(); + //stop all still active threads that belong to a plugin + Thread.getAllStackTraces().keySet().stream() + .filter(thread -> (thread.getClass().getClassLoader() == pluginClassLoader)) + .forEach(thread -> { + try { + thread.interrupt(); + thread.join(2000); + if (thread.isAlive()) { + throw new IllegalStateException("Thread " + thread.getName() + " still running"); + } + } catch (Exception t) { + severe("Failed to stop thread that belong to plugin", t, plugin.getDescription().getName()); + } + }); + + //finish uncompleted intents + ModifiedPluginEventBus.completeIntents(plugin); + //remove commands that were registered by plugin not through normal means + try { + Map commandMap = ReflectionUtils.getFieldValue(pluginManager, "commandMap"); + if (checkReflectionNotNull(commandMap, "commandMap")) { + commandMap.entrySet().removeIf(entry -> entry.getValue().getClass().getClassLoader() == pluginClassLoader); + } + } catch (Exception t) { + severe("Failed to cleanup commandMap", t, plugin.getDescription().getName()); + } + //cleanup internal listener and command maps from plugin refs + try { + Map pluginsMap = ReflectionUtils.getFieldValue(pluginManager, "plugins"); + Multimap commands = ReflectionUtils.getFieldValue(pluginManager, "commandsByPlugin"); + Multimap listeners = ReflectionUtils.getFieldValue(pluginManager, "listenersByPlugin"); + + if (checkReflectionNotNull(pluginsMap, "plugin") + && checkReflectionNotNull(commands, "commandByPlugin") + && checkReflectionNotNull(listeners, "listenersByPlugin")) { + pluginsMap.values().remove(plugin); + commands.removeAll(plugin); + listeners.removeAll(plugin); + } + } catch (Exception t) { + severe("Failed to cleanup bungee internal maps from plugin refs", t, plugin.getDescription().getName()); + } + //close classloader + if (pluginClassLoader instanceof URLClassLoader) { + try { + ((URLClassLoader) pluginClassLoader).close(); + } catch (Exception t) { + severe("Failed to close the classloader for plugin", t, plugin.getDescription().getName()); + } + } + //remove classloader + Set allLoaders = ReflectionUtils.getStaticFieldValue(PluginClassloader.class, "allLoaders"); + if (checkReflectionNotNull(allLoaders, "allLoaders")) { + allLoaders.remove(pluginClassLoader); + } + + } + + private static boolean checkReflectionNotNull(Object obj, String field) { + if (obj == null) { + ProxyServer.getInstance().getLogger().log(Level.SEVERE, "Could not get field {} by reflections: are you using the latest version of BungeePluginManager?", field); + return false; + } + return true; + } + + @SuppressWarnings("resource") + public static boolean loadPlugin(File pluginFile) { + + try (JarFile jar = new JarFile(pluginFile)) { + + JarEntry pdf = jar.getJarEntry("bungee.yml"); + + if (pdf == null) { + pdf = jar.getJarEntry("plugin.yml"); + } + + try (InputStream in = jar.getInputStream(pdf)) { + //load description + PluginDescription desc = new Yaml().loadAs(in, PluginDescription.class); + desc.setFile(pluginFile); + //check depends + HashSet plugins = new HashSet<>(); + ProxyServer.getInstance().getPluginManager().getPlugins().forEach(plugin -> plugins.add(plugin.getDescription().getName())); + for (String dependency : desc.getDepends()) { + if (!plugins.contains(dependency)) { + ProxyServer.getInstance().getLogger().log(Level.WARNING, "{0} (required by {1}) is unavailable", new Object[]{dependency, desc.getName()}); + return false; + } + } + + // do actual loading + Class main; + try (URLClassLoader loader = new PluginClassloader( + new URL[]{pluginFile.toURI().toURL()} + )) { + main = loader.loadClass(desc.getMain()); + Enumeration entries = jar.entries(); + while(entries.hasMoreElements()){ + JarEntry entry = entries.nextElement(); + if(!entry.isDirectory() && entry.getName().endsWith(".class")){ + loader.loadClass(entry.getName().replace('/', '.').replace(".class", "")); + } + } + } + Plugin clazz = (Plugin) main.getDeclaredConstructor().newInstance(); + + // reflection + Map pluginsMap = ReflectionUtils.getFieldValue(ProxyServer.getInstance().getPluginManager(), "plugins"); + ReflectionUtils.invokeMethod(clazz, "init", ProxyServer.getInstance(), desc); + + pluginsMap.put(desc.getName(), clazz); + clazz.onLoad(); + clazz.onEnable(); + return true; + } + } catch (Exception t) { + severe("Failed to load plugin", t, pluginFile.getName()); + return false; + } + + } + + private static void severe(String message, Exception t, String pluginName) { + ProxyServer.getInstance().getLogger().log(Level.SEVERE, message + " " + pluginName, t); + } + +} \ No newline at end of file diff --git a/src/de/steamwar/bungeecore/ReflectionUtils.java b/src/bungeepluginmanager/ReflectionUtils.java similarity index 94% rename from src/de/steamwar/bungeecore/ReflectionUtils.java rename to src/bungeepluginmanager/ReflectionUtils.java index c451db5..333e8b5 100644 --- a/src/de/steamwar/bungeecore/ReflectionUtils.java +++ b/src/bungeepluginmanager/ReflectionUtils.java @@ -1,10 +1,10 @@ -package de.steamwar.bungeecore; +package bungeepluginmanager; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -final class ReflectionUtils { +public final class ReflectionUtils { private ReflectionUtils() { throw new IllegalStateException("Utility class"); } @@ -24,7 +24,7 @@ final class ReflectionUtils { return null; } - static void setFieldValue(Object obj, String fieldName, Object value) { + public static void setFieldValue(Object obj, String fieldName, Object value) { Class clazz = obj.getClass(); do { try { diff --git a/src/de/steamwar/bungeecore/Persistent.java b/src/de/steamwar/bungeecore/Persistent.java index 1f45561..faca785 100644 --- a/src/de/steamwar/bungeecore/Persistent.java +++ b/src/de/steamwar/bungeecore/Persistent.java @@ -1,21 +1,14 @@ package de.steamwar.bungeecore; -import com.google.common.collect.Multimap; +import bungeepluginmanager.ModifiedPluginEventBus; +import bungeepluginmanager.PluginUtils; +import bungeepluginmanager.ReflectionUtils; import net.md_5.bungee.api.CommandSender; -import net.md_5.bungee.api.plugin.*; -import org.yaml.snakeyaml.Yaml; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.api.plugin.PluginManager; import java.io.File; -import java.io.InputStream; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.logging.Handler; -import java.util.logging.Level; public class Persistent extends Plugin { @@ -26,110 +19,15 @@ public class Persistent extends Plugin { @Override public void onEnable(){ instance = this; + ReflectionUtils.setFieldValue(getProxy().getPluginManager(), "eventBus", new ModifiedPluginEventBus()); getProxy().getPluginManager().registerCommand(this, new Command("softreload", "bungeecore.softreload"){ @Override public void execute(CommandSender sender, String[] args) { // Copied from https://www.spigotmc.org/resources/bungeepluginmanager-manage-your-bungee-plugin-at-runtime.63861/ PluginManager pluginManager = getProxy().getPluginManager(); Plugin bungeecore = pluginManager.getPlugin("BungeeCore"); - if(bungeecore != null) { - ClassLoader pluginClassLoader = bungeecore.getClass().getClassLoader(); - - bungeecore.onDisable(); - for (Handler h : bungeecore.getLogger().getHandlers()) { - h.close(); - } - - pluginManager.unregisterCommands(bungeecore); - pluginManager.unregisterListeners(bungeecore); - getProxy().getScheduler().cancel(bungeecore); - bungeecore.getExecutorService().shutdownNow(); - Thread.getAllStackTraces().keySet().stream() - .filter(thread -> (thread.getClass().getClassLoader() == pluginClassLoader)) - .forEach(thread -> { - try { - thread.interrupt(); - thread.join(2000); - if (thread.isAlive()) { - throw new IllegalStateException("Thread " + thread.getName() + " still running"); - } - } catch (Exception t) { - getProxy().getLogger().log(Level.SEVERE, "Failed to stop thread that belong to plugin", t); - } - }); - //remove commands that were registered by plugin not through normal means - try { - Map commandMap = ReflectionUtils.getFieldValue(pluginManager, "commandMap"); - if (commandMap != null) { - commandMap.entrySet().removeIf(entry -> entry.getValue().getClass().getClassLoader() == pluginClassLoader); - } - } catch (Exception t) { - getLogger().log(Level.SEVERE, "Failed to cleanup commandMap", t); - } - try { - Map pluginsMap = ReflectionUtils.getFieldValue(pluginManager, "plugins"); - Multimap commands = ReflectionUtils.getFieldValue(pluginManager, "commandsByPlugin"); - Multimap listeners = ReflectionUtils.getFieldValue(pluginManager, "listenersByPlugin"); - - if (pluginsMap != null && commands != null && listeners != null) { - pluginsMap.values().remove(bungeecore); - commands.removeAll(bungeecore); - listeners.removeAll(bungeecore); - } - } catch (Exception t) { - getLogger().log(Level.SEVERE, "Failed to cleanup bungee internal maps from plugin refs", t); - } - if (pluginClassLoader instanceof URLClassLoader) { - try { - ((URLClassLoader) pluginClassLoader).close(); - } catch (Exception t) { - getLogger().log(Level.SEVERE, "Failed to close classloader", t); - } - } - Set allLoaders = ReflectionUtils.getStaticFieldValue(PluginClassloader.class, "allLoaders"); - if (allLoaders != null) { - allLoaders.remove(pluginClassLoader); - } - } - - File pluginFile = new File(getProxy().getPluginsFolder(), "BungeeCore.jar"); - try (JarFile jar = new JarFile(pluginFile)) { - JarEntry pdf = jar.getJarEntry("plugin.yml"); - - try (InputStream in = jar.getInputStream(pdf)) { - //load description - PluginDescription desc = new Yaml().loadAs(in, PluginDescription.class); - desc.setFile(pluginFile); - //check depends - HashSet plugins = new HashSet<>(); - getProxy().getPluginManager().getPlugins().forEach(plugin -> plugins.add(plugin.getDescription().getName())); - for (String dependency : desc.getDepends()) { - if (!plugins.contains(dependency)) { - getLogger().log(Level.WARNING, "{0} (required by {1}) is unavailable", new Object[]{dependency, desc.getName()}); - return; - } - } - - // do actual loading - Class main; - try (URLClassLoader loader = new PluginClassloader(new URL[]{ - pluginFile.toURI().toURL() - })) { - main = loader.loadClass(desc.getMain()); - } - Plugin clazz = (Plugin) main.getDeclaredConstructor().newInstance(); - - // reflection - Map pluginsMap = ReflectionUtils.getFieldValue(getProxy().getPluginManager(), "plugins"); - ReflectionUtils.invokeMethod(clazz, "init", getProxy(), desc); - - pluginsMap.put(desc.getName(), clazz); - clazz.onLoad(); - clazz.onEnable(); - } - } catch (Exception t) { - getLogger().log(Level.SEVERE, "Failed to load plugin", t); - } + PluginUtils.unloadPlugin(bungeecore); + PluginUtils.loadPlugin(new File(getProxy().getPluginsFolder(), "BungeeCore.jar")); } }); } @@ -145,13 +43,13 @@ public class Persistent extends Plugin { public static void setChatPrefix(String prefix){ chatPrefix = prefix; } - public static String getLobbyServer() { + static String getLobbyServer() { return lobbyServer; } - public static String getPrefix(){ + static String getPrefix(){ return chatPrefix; } - public static Persistent getInstance() { + static Persistent getInstance() { return instance; } }