From 9ae3858a740541d1cc165df9db1941b503be1252 Mon Sep 17 00:00:00 2001 From: Lixfel Date: Thu, 31 Aug 2023 10:37:49 +0200 Subject: [PATCH] More secure softreload implementation --- src/de/steamwar/bungeecore/Persistent.java | 68 ++-- src/de/steamwar/bungeecore/Subserver.java | 6 +- .../persistent}/ModifiedPluginEventBus.java | 40 +- src/de/steamwar/persistent/PluginUtils.java | 183 ++++++++++ src/de/steamwar/persistent/Reflection.java | 342 ++++++++++++++++++ .../api/bungeepluginmanager/PluginUtils.java | 203 ----------- .../bungeepluginmanager/ReflectionUtils.java | 90 ----- 7 files changed, 585 insertions(+), 347 deletions(-) rename src/{net/md_5/bungee/api/bungeepluginmanager => de/steamwar/persistent}/ModifiedPluginEventBus.java (54%) create mode 100644 src/de/steamwar/persistent/PluginUtils.java create mode 100644 src/de/steamwar/persistent/Reflection.java delete mode 100644 src/net/md_5/bungee/api/bungeepluginmanager/PluginUtils.java delete mode 100644 src/net/md_5/bungee/api/bungeepluginmanager/ReflectionUtils.java diff --git a/src/de/steamwar/bungeecore/Persistent.java b/src/de/steamwar/bungeecore/Persistent.java index c5d9476..9022e04 100644 --- a/src/de/steamwar/bungeecore/Persistent.java +++ b/src/de/steamwar/bungeecore/Persistent.java @@ -19,39 +19,38 @@ package de.steamwar.bungeecore; +import de.steamwar.persistent.Reflection; import net.md_5.bungee.api.CommandSender; -import net.md_5.bungee.api.bungeepluginmanager.ModifiedPluginEventBus; -import net.md_5.bungee.api.bungeepluginmanager.PluginUtils; -import net.md_5.bungee.api.bungeepluginmanager.ReflectionUtils; +import de.steamwar.persistent.ModifiedPluginEventBus; +import de.steamwar.persistent.PluginUtils; +import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.plugin.Command; import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.api.plugin.PluginDescription; import net.md_5.bungee.api.plugin.PluginManager; +import net.md_5.bungee.event.EventBus; import java.io.File; +import java.util.Map; +import java.util.logging.Level; public class Persistent extends Plugin { - private static Persistent instance; - private static String chatPrefix = ""; - private static String lobbyServer = ""; + public static final String PREFIX = "§eSteam§8War» "; + + private static final Reflection.FieldAccessor eventBus = Reflection.getField(PluginManager.class, "eventBus", EventBus.class); @Override public void onLoad() { - ReflectionUtils.setFieldValue(getProxy().getPluginManager(), "eventBus", new ModifiedPluginEventBus()); + eventBus.set(getProxy().getPluginManager(), new ModifiedPluginEventBus()); } @Override public void onEnable(){ - instance = this; - - getProxy().getPluginManager().registerCommand(this, new Command("softreload", "bungeecore.softreload"){ + 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"); - PluginUtils.unloadPlugin(bungeecore); - PluginUtils.loadPlugin(new File(getProxy().getPluginsFolder(), "BungeeCore.jar")); + softreload(); } }); } @@ -61,23 +60,30 @@ public class Persistent extends Plugin { Subserver.shutdown(); } - public static void setLobbyServer(String lobbyServer) { - Persistent.lobbyServer = lobbyServer; + public void softreload() { + Map commandMap = PluginUtils.CommandMap.get(getProxy().getPluginManager()); + Map plugins = PluginUtils.Plugins.get(getProxy().getPluginManager()); + Object libraryLoader = PluginUtils.LibraryLoader.get(getProxy().getPluginManager()); + PluginDescription desc = PluginUtils.loadDescription(getProxy().getPluginManager(), new File(getProxy().getPluginsFolder(), "BungeeCore.jar")); + + getProxy().getScheduler().runAsync(this, () -> { + getProxy().broadcast(TextComponent.fromLegacyText(PREFIX + "§eNetwork update is starting§8.")); + try { + PluginUtils.unloadPlugin(getProxy(), commandMap, plugins, getProxy().getPluginManager().getPlugin("BungeeCore")); + PluginUtils.loadPlugin(plugins, libraryLoader, desc); + } catch (Throwable t) { + getLogger().log(Level.SEVERE, "Errors during softreload", t); + getProxy().broadcast(TextComponent.fromLegacyText(PREFIX + "§cNetwork update failed§8, §cexpect network restart soon§8.")); + return; + } + + getProxy().broadcast(TextComponent.fromLegacyText(PREFIX + "§eNetwork update complete§8.")); + }); } - public static void setChatPrefix(String prefix){ - chatPrefix = prefix; - } + @Deprecated + public static void setLobbyServer(String lobbyServer) {} - static String getLobbyServer() { - return lobbyServer; - } - - static String getPrefix(){ - return chatPrefix; - } - - static Persistent getInstance() { - return instance; - } + @Deprecated + public static void setChatPrefix(String prefix) {} } diff --git a/src/de/steamwar/bungeecore/Subserver.java b/src/de/steamwar/bungeecore/Subserver.java index c916f82..3eabf41 100644 --- a/src/de/steamwar/bungeecore/Subserver.java +++ b/src/de/steamwar/bungeecore/Subserver.java @@ -123,7 +123,7 @@ public class Subserver implements Runnable { public void sendPlayer(ProxiedPlayer p){ if(!started){ - p.sendMessage(Persistent.getPrefix() + "§7Der Server wird gestartet, einen Moment bitte..."); + p.sendMessage(Persistent.PREFIX + "§7Starting server, please wait..."); cachedPlayers.add(p); }else{ p.connect(server); @@ -171,9 +171,9 @@ public class Subserver implements Runnable { private void fatalError(){ for(ProxiedPlayer cached : cachedPlayers) - cached.sendMessage(Persistent.getPrefix() + "§cUnerwarteter Fehler beim Serverstart."); + cached.sendMessage(Persistent.PREFIX + "§cUnexpected error during server startup."); for(ProxiedPlayer player : server.getPlayers()) - player.sendMessage(Persistent.getPrefix() + "§cUnerwarteter Fehler im Server."); + player.sendMessage(Persistent.PREFIX + "§cLost connection to server."); } private void sendProgress(int progress){ diff --git a/src/net/md_5/bungee/api/bungeepluginmanager/ModifiedPluginEventBus.java b/src/de/steamwar/persistent/ModifiedPluginEventBus.java similarity index 54% rename from src/net/md_5/bungee/api/bungeepluginmanager/ModifiedPluginEventBus.java rename to src/de/steamwar/persistent/ModifiedPluginEventBus.java index 60c33ac..a34321f 100644 --- a/src/net/md_5/bungee/api/bungeepluginmanager/ModifiedPluginEventBus.java +++ b/src/de/steamwar/persistent/ModifiedPluginEventBus.java @@ -1,23 +1,23 @@ -/* - This file is a part of the SteamWar software. - - Copyright (C) 2020 SteamWar.de-Serverteam +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2023 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -package net.md_5.bungee.api.bungeepluginmanager; +package de.steamwar.persistent; import net.md_5.bungee.api.event.AsyncEvent; import net.md_5.bungee.api.plugin.Plugin; @@ -33,7 +33,7 @@ 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) { + public static void completeIntents(Plugin plugin) { synchronized (LOCK) { UNCOMPLETED_EVENTS.forEach(event -> { try { diff --git a/src/de/steamwar/persistent/PluginUtils.java b/src/de/steamwar/persistent/PluginUtils.java new file mode 100644 index 0000000..d86e472 --- /dev/null +++ b/src/de/steamwar/persistent/PluginUtils.java @@ -0,0 +1,183 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2023 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.persistent; + +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.plugin.Command; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.api.plugin.PluginDescription; +import net.md_5.bungee.api.plugin.PluginManager; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLClassLoader; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.logging.Handler; +import java.util.logging.Level; + +public final class PluginUtils { + //Adapted from https://github.com/Shevchik/BungeePluginManager/blob/master/src/bungeepluginmanager/PluginUtils.java + + private static final Class LibraryLoaderClass = Reflection.getClass("net.md_5.bungee.api.plugin.LibraryLoader"); + private static final Reflection.MethodInvoker createLoader = Reflection.getTypedMethod(LibraryLoaderClass, "createLoader", ClassLoader.class, PluginDescription.class); + + public static final Reflection.FieldAccessor CommandMap = Reflection.getField(PluginManager.class, "commandMap", Map.class); + public static final Reflection.FieldAccessor Plugins = Reflection.getField(PluginManager.class, "plugins", Map.class); + public static final Reflection.FieldAccessor Yaml = Reflection.getField(PluginManager.class, "yaml", Yaml.class); + public static final Reflection.FieldAccessor LibraryLoader = Reflection.getField(PluginManager.class, "libraryLoader", LibraryLoaderClass); + public static final Reflection.FieldAccessor Service = Reflection.getField(Plugin.class, "service", ExecutorService.class); + + private static final Class PluginClassLoaderClass = Reflection.getClass("net.md_5.bungee.api.plugin.PluginClassloader"); + private static final Reflection.ConstructorInvoker NewPluginClassLoader = Reflection.getConstructor(PluginClassLoaderClass, ProxyServer.class, PluginDescription.class, File.class, ClassLoader.class); + + private static final Reflection.MethodInvoker init = Reflection.getMethod(Plugin.class, "init", ProxyServer.class, PluginDescription.class); + + private static final Set allLoaders = Reflection.getField(PluginClassLoaderClass, "allLoaders", Set.class).get(null); + + private PluginUtils() {} + + public static void unloadPlugin(ProxyServer proxy, Map commandMap, Map plugins, Plugin plugin) { + IllegalStateException error = new IllegalStateException(); + + PluginManager pluginManager = proxy.getPluginManager(); + ClassLoader pluginClassLoader = plugin.getClass().getClassLoader(); + + try { + plugin.onDisable(); + } catch (Exception e) { + proxy.getLogger().log(Level.SEVERE, "Exception on disable", e); + } + + //unregister event handlers + pluginManager.unregisterListeners(plugin); + + //unregister commands + pluginManager.unregisterCommands(plugin); + //remove incorrectly registered plugins + commandMap.entrySet().removeIf(entry -> entry.getValue().getClass().getClassLoader() == pluginClassLoader); + + //cancel scheduled tasks + proxy.getScheduler().cancel(plugin); + + //finish uncompleted intents + ModifiedPluginEventBus.completeIntents(plugin); + + //shutdown internal executor + ExecutorService service = Service.get(plugin); + if(service != null) + service.shutdownNow(); + + //stop all still active threads that belong to a plugin + for(Thread thread : Thread.getAllStackTraces().keySet()) { + if(thread.getClass().getClassLoader() != pluginClassLoader) + continue; + + thread.interrupt(); + try { + thread.join(100); + } catch (InterruptedException t) { + Thread.currentThread().interrupt(); + } + + if (thread.isAlive()) + proxy.getLogger().log(Level.WARNING, () -> "Could not stop thread " + thread.getName() + " of plugin " + plugin.getDescription().getName() + ". Still running"); + } + + //Clear resource bundle cache + ResourceBundle.clearCache(pluginClassLoader); + + //close all log handlers + for (Handler handler : plugin.getLogger().getHandlers()) { + handler.close(); + } + + //cleanup internal listener and command maps from plugin refs + plugins.values().remove(plugin); + + // Remove classloader + allLoaders.remove(pluginClassLoader); + + //close classloader + if (pluginClassLoader instanceof URLClassLoader) { + try { + ((URLClassLoader) pluginClassLoader).close(); + } catch (Exception e) { + proxy.getLogger().log(Level.SEVERE, "Exception while closing the classloader", e); + } + } + + if(error.getSuppressed().length > 0) { + throw error; + } + } + + public static PluginDescription loadDescription(PluginManager pluginManager, File pluginFile) { + PluginDescription desc; + 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 + desc = PluginUtils.Yaml.get(pluginManager).loadAs(in, PluginDescription.class); + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + desc.setFile(pluginFile); + + //check depends + Map pluginsMap = PluginUtils.Plugins.get(pluginManager); + for (String dependency : desc.getDepends()) { + if (!pluginsMap.containsKey(dependency)) { + throw new IllegalStateException(dependency +" (required by " + desc.getName() + ") is unavailable"); + } + } + + return desc; + } + + public static void loadPlugin(Map plugins, Object libraryLoader, PluginDescription desc) { + URLClassLoader loader = (URLClassLoader)NewPluginClassLoader.invoke(ProxyServer.getInstance(), desc, desc.getFile(), libraryLoader != null ? createLoader.invoke(libraryLoader, desc) : null); + + Plugin plugin; + try { + Class mainclazz = loader.loadClass(desc.getMain()); + plugin = (Plugin) Reflection.newInstance(mainclazz); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } + init.invoke(plugin, ProxyServer.getInstance(), desc); + + plugins.put(desc.getName(), plugin); + plugin.onLoad(); + plugin.onEnable(); + } +} diff --git a/src/de/steamwar/persistent/Reflection.java b/src/de/steamwar/persistent/Reflection.java new file mode 100644 index 0000000..94f68d9 --- /dev/null +++ b/src/de/steamwar/persistent/Reflection.java @@ -0,0 +1,342 @@ +package de.steamwar.persistent; + +import java.lang.reflect.*; +import java.util.Arrays; + +/** + * An utility class that simplifies reflection in Bukkit plugins. + * + * @author Kristian + */ +public final class Reflection { + /** + * An interface for invoking a specific constructor. + */ + public interface ConstructorInvoker { + /** + * Invoke a constructor for a specific class. + * + * @param arguments - the arguments to pass to the constructor. + * @return The constructed object. + */ + Object invoke(Object... arguments); + } + + /** + * An interface for invoking a specific method. + */ + public interface MethodInvoker { + /** + * Invoke a method on a specific target object. + * + * @param target - the target object, or NULL for a static method. + * @param arguments - the arguments to pass to the method. + * @return The return value, or NULL if is void. + */ + Object invoke(Object target, Object... arguments); + } + + /** + * An interface for retrieving the field content. + * + * @param - field type. + */ + public interface FieldAccessor { + /** + * Retrieve the content of a field. + * + * @param target - the target object, or NULL for a static field. + * @return The value of the field. + */ + T get(Object target); + + /** + * Set the content of a field. + * + * @param target - the target object, or NULL for a static field. + * @param value - the new value of the field. + */ + void set(Object target, Object value); + + /** + * Determine if the given object has this field. + * + * @param target - the object to test. + * @return TRUE if it does, FALSE otherwise. + */ + boolean hasField(Object target); + } + + private Reflection() { + // Seal class + } + + /** + * Retrieve a field accessor for a specific field type and name. + * + * @param target - the target type. + * @param name - the name of the field, or NULL to ignore. + * @param fieldType - a compatible field type. + * @return The field accessor. + */ + public static FieldAccessor getField(Class target, String name, Class fieldType) { + return getField(target, name, fieldType, 0); + } + + /** + * Retrieve a field accessor for a specific field type and name. + * + * @param className - lookup name of the class, see {@link #getClass(String)}. + * @param name - the name of the field, or NULL to ignore. + * @param fieldType - a compatible field type. + * @return The field accessor. + */ + public static FieldAccessor getField(String className, String name, Class fieldType) { + return getField(getClass(className), name, fieldType, 0); + } + + /** + * Retrieve a field accessor for a specific field type and name. + * + * @param target - the target type. + * @param fieldType - a compatible field type. + * @param index - the number of compatible fields to skip. + * @return The field accessor. + */ + public static FieldAccessor getField(Class target, Class fieldType, int index) { + return getField(target, null, fieldType, index); + } + + /** + * Retrieve a field accessor for a specific field type and name. + * + * @param className - lookup name of the class, see {@link #getClass(String)}. + * @param fieldType - a compatible field type. + * @param index - the number of compatible fields to skip. + * @return The field accessor. + */ + public static FieldAccessor getField(String className, Class fieldType, int index) { + return getField(getClass(className), fieldType, index); + } + + public static FieldAccessor getField(Class target, Class fieldType, int index, Class... parameters) { + return getField(target, null, fieldType, index, parameters); + } + + private static FieldAccessor getField(Class target, String name, Class fieldType, int index, Class... parameters) { + for (final Field field : target.getDeclaredFields()) { + if(matching(field, name, fieldType, parameters) && index-- <= 0) { + field.setAccessible(true); + + return new FieldAccessor() { + @Override + @SuppressWarnings("unchecked") + public T get(Object target) { + try { + return (T) field.get(target); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Cannot access reflection.", e); + } + } + + @Override + public void set(Object target, Object value) { + try { + field.set(target, value); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Cannot access reflection.", e); + } + } + + @Override + public boolean hasField(Object target) { + // target instanceof DeclaringClass + return field.getDeclaringClass().isAssignableFrom(target.getClass()); + } + }; + } + } + + // Search in parent classes + if (target.getSuperclass() != null) + return getField(target.getSuperclass(), name, fieldType, index); + + throw new IllegalArgumentException("Cannot find field with type " + fieldType); + } + + private static boolean matching(Field field, String name, Class fieldType, Class... parameters) { + if(name != null && !field.getName().equals(name)) + return false; + + if(!fieldType.isAssignableFrom(field.getType())) + return false; + + if(parameters.length > 0) { + Type[] arguments = ((ParameterizedType)field.getGenericType()).getActualTypeArguments(); + + for(int i = 0; i < parameters.length; i++) { + if(arguments[i] instanceof ParameterizedType ? ((ParameterizedType) arguments[i]).getRawType() != parameters[i] : arguments[i] != parameters[i]) + return false; + } + } + return true; + } + + /** + * Search for the first publicly and privately defined method of the given name and parameter count. + * + * @param className - lookup name of the class, see {@link #getClass(String)}. + * @param methodName - the method name, or NULL to skip. + * @param params - the expected parameters. + * @return An object that invokes this specific method. + * @throws IllegalStateException If we cannot find this method. + */ + public static MethodInvoker getMethod(String className, String methodName, Class... params) { + return getTypedMethod(getClass(className), methodName, null, params); + } + + /** + * Search for the first publicly and privately defined method of the given name and parameter count. + * + * @param clazz - a class to start with. + * @param methodName - the method name, or NULL to skip. + * @param params - the expected parameters. + * @return An object that invokes this specific method. + * @throws IllegalStateException If we cannot find this method. + */ + public static MethodInvoker getMethod(Class clazz, String methodName, Class... params) { + return getTypedMethod(clazz, methodName, null, params); + } + + /** + * Search for the first publicly and privately defined method of the given name and parameter count. + * + * @param clazz - a class to start with. + * @param methodName - the method name, or NULL to skip. + * @param returnType - the expected return type, or NULL to ignore. + * @param params - the expected parameters. + * @return An object that invokes this specific method. + * @throws IllegalStateException If we cannot find this method. + */ + public static MethodInvoker getTypedMethod(Class clazz, String methodName, Class returnType, Class... params) { + for (final Method method : clazz.getDeclaredMethods()) { + if ((methodName == null || method.getName().equals(methodName)) + && (returnType == null || method.getReturnType().equals(returnType)) + && Arrays.equals(method.getParameterTypes(), params)) { + method.setAccessible(true); + + return (target, arguments) -> { + try { + return method.invoke(target, arguments); + } catch (Exception e) { + throw new IllegalArgumentException("Cannot invoke method " + method, e); + } + }; + } + } + + // Search in every superclass + if (clazz.getSuperclass() != null) + return getTypedMethod(clazz.getSuperclass(), methodName, returnType, params); + + throw new IllegalStateException(String.format("Unable to find method %s (%s).", methodName, Arrays.asList(params))); + } + + /** + * Search for the first publically and privately defined constructor of the given name and parameter count. + * + * @param className - lookup name of the class, see {@link #getClass(String)}. + * @param params - the expected parameters. + * @return An object that invokes this constructor. + * @throws IllegalStateException If we cannot find this method. + */ + public static ConstructorInvoker getConstructor(String className, Class... params) { + return getConstructor(getClass(className), params); + } + + /** + * Search for the first publically and privately defined constructor of the given name and parameter count. + * + * @param clazz - a class to start with. + * @param params - the expected parameters. + * @return An object that invokes this constructor. + * @throws IllegalStateException If we cannot find this method. + */ + public static ConstructorInvoker getConstructor(Class clazz, Class... params) { + for (final Constructor constructor : clazz.getDeclaredConstructors()) { + if (Arrays.equals(constructor.getParameterTypes(), params)) { + constructor.setAccessible(true); + + return arguments -> { + try { + return constructor.newInstance(arguments); + } catch (Exception e) { + throw new IllegalArgumentException("Cannot invoke constructor " + constructor, e); + } + }; + } + } + + throw new IllegalStateException(String.format("Unable to find constructor for %s (%s).", clazz, Arrays.asList(params))); + } + + /** + * Retrieve a class from its full name, without knowing its type on compile time. + *

+ * This is useful when looking up fields by a NMS or OBC type. + *

+ * + * @param lookupName - the class name with variables. + * @return The class. + */ + public static Class getUntypedClass(String lookupName) { + @SuppressWarnings({ "rawtypes", "unchecked" }) + Class clazz = (Class) getClass(lookupName); + return clazz; + } + + /** + * Retrieve a class from its full name. + *

+ * Strings enclosed with curly brackets - such as {TEXT} - will be replaced according to the following table: + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
VariableContent
{nms}Actual package name of net.minecraft.server.VERSION
{obc}Actual pacakge name of org.bukkit.craftbukkit.VERSION
{version}The current Minecraft package VERSION, if any.
+ * + * @param lookupName - the class name with variables. + * @return The looked up class. + * @throws IllegalArgumentException If a variable or class could not be found. + */ + public static Class getClass(String lookupName) { + try { + return Class.forName(lookupName); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Cannot find " + lookupName, e); + } + } + + @SuppressWarnings("deprecation") + public static Object newInstance(Class clazz) { + try { + return clazz.newInstance(); + } catch (InstantiationException | IllegalAccessException e) { + throw new SecurityException("Could not create object", e); + } + } +} diff --git a/src/net/md_5/bungee/api/bungeepluginmanager/PluginUtils.java b/src/net/md_5/bungee/api/bungeepluginmanager/PluginUtils.java deleted file mode 100644 index 5fcc82f..0000000 --- a/src/net/md_5/bungee/api/bungeepluginmanager/PluginUtils.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - This file is a part of the SteamWar software. - - Copyright (C) 2020 SteamWar.de-Serverteam - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -package net.md_5.bungee.api.bungeepluginmanager; - -import com.google.common.base.Preconditions; -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 org.yaml.snakeyaml.constructor.Constructor; -import org.yaml.snakeyaml.representer.Representer; - -import java.io.File; -import java.io.InputStream; -import java.net.URLClassLoader; -import java.util.Map; -import java.util.ResourceBundle; -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 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(100); - if (thread.isAlive()) { - ProxyServer.getInstance().getLogger().log(Level.SEVERE, "Could not stop thread " + thread.getName() + " of plugin " + plugin.getDescription().getName() + ". Still running"); - } - } catch (InterruptedException t) { - Thread.currentThread().interrupt(); - } - }); - - //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()); - } - - ResourceBundle.clearCache(pluginClassLoader); - - //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 - try { - Class PluginClassLoader = Class.forName("net.md_5.bungee.api.plugin.PluginClassloader"); - Set allLoaders = ReflectionUtils.getStaticFieldValue(PluginClassLoader, "allLoaders"); - Preconditions.checkNotNull(allLoaders); - allLoaders.remove(pluginClassLoader); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - - } - - 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 - - Representer representer = new Representer(); - representer.getPropertyUtils().setSkipMissingProperties(true); - PluginDescription desc = new Yaml(new Constructor(PluginDescription.class), representer).loadAs(in, PluginDescription.class); - desc.setFile(pluginFile); - - - //check depends - Map pluginsMap = ReflectionUtils.getFieldValue(ProxyServer.getInstance().getPluginManager(), "plugins"); - Preconditions.checkNotNull(pluginsMap); - - for (String dependency : desc.getDepends()) { - if (!pluginsMap.containsKey(dependency)) { - ProxyServer.getInstance().getLogger().log(Level.WARNING, "{0} (required by {1}) is unavailable", new Object[]{dependency, desc.getName()}); - return false; - } - } - - Object libraryLoader = ReflectionUtils.getFieldValue(ProxyServer.getInstance().getPluginManager(), "libraryLoader"); - - //load plugin - Class pluginClassLoader = Class.forName("net.md_5.bungee.api.plugin.PluginClassloader"); - java.lang.reflect.Constructor constructor = pluginClassLoader.getConstructor(ProxyServer.class, PluginDescription.class, File.class, ClassLoader.class); - constructor.setAccessible(true); - URLClassLoader loader = (URLClassLoader) constructor.newInstance(ProxyServer.getInstance(), desc, pluginFile, libraryLoader != null ? ReflectionUtils.invokeMethod(libraryLoader, "createLoader", desc) : null); - Class mainclazz = loader.loadClass(desc.getMain()); - Plugin plugin = (Plugin) mainclazz.getDeclaredConstructor().newInstance(); - ReflectionUtils.invokeMethod(plugin, "init", ProxyServer.getInstance(), desc); - - pluginsMap.put(desc.getName(), plugin); - plugin.onLoad(); - plugin.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); - } - -} diff --git a/src/net/md_5/bungee/api/bungeepluginmanager/ReflectionUtils.java b/src/net/md_5/bungee/api/bungeepluginmanager/ReflectionUtils.java deleted file mode 100644 index 4ef2e86..0000000 --- a/src/net/md_5/bungee/api/bungeepluginmanager/ReflectionUtils.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - This file is a part of the SteamWar software. - - Copyright (C) 2020 SteamWar.de-Serverteam - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . -*/ - -package net.md_5.bungee.api.bungeepluginmanager; - -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -public final class ReflectionUtils { - private ReflectionUtils() { - throw new IllegalStateException("Utility class"); - } - - @SuppressWarnings("unchecked") - static T getFieldValue(Object obj, String fieldName) { - Class clazz = obj.getClass(); - do { - try { - Field field = clazz.getDeclaredField(fieldName); - field.setAccessible(true); - return (T) field.get(obj); - } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException error) { - // Ignored - } - } while ((clazz = clazz.getSuperclass()) != null); - return null; - } - - public static void setFieldValue(Object obj, String fieldName, Object value) { - Class clazz = obj.getClass(); - do { - try { - Field field = clazz.getDeclaredField(fieldName); - field.setAccessible(true); - field.set(obj, value); - } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException error) { - // Ignored - } - } while ((clazz = clazz.getSuperclass()) != null); - } - - @SuppressWarnings("unchecked") - static T getStaticFieldValue(Class clazz, String fieldName) { - do { - try { - Field field = clazz.getDeclaredField(fieldName); - field.setAccessible(true); - return (T) field.get(null); - } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException error) { - // Ignored - } - } while ((clazz = clazz.getSuperclass()) != null); - return null; - } - - static Object invokeMethod(Object obj, String methodName, Object... args) { - Class clazz = obj.getClass(); - do { - try { - for (Method method : clazz.getDeclaredMethods()) { - if (method.getName().equals(methodName) && method.getParameterTypes().length == args.length) { - method.setAccessible(true); - return method.invoke(obj, args); - } - } - } catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException error) { - // Ignore - } - } while ((clazz = clazz.getSuperclass()) != null); - throw new SecurityException("Method could not be found!"); - } - -} -- 2.39.2