13
0

Merge pull request 'More secure softreload implementation' (#26) from secure-softreload into master
Alle Prüfungen waren erfolgreich
SteamWarCI Build successful

Reviewed-on: #26
Reviewed-by: YoyoNow <jwsteam@nidido.de>
Dieser Commit ist enthalten in:
Lixfel 2023-08-31 10:52:35 +02:00
Commit af89693438
7 geänderte Dateien mit 585 neuen und 347 gelöschten Zeilen

Datei anzeigen

@ -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> 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<String, Command> commandMap = PluginUtils.CommandMap.get(getProxy().getPluginManager());
Map<String, Plugin> 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) {}
}

Datei anzeigen

@ -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){

Datei anzeigen

@ -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 <https://www.gnu.org/licenses/>.
*/
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 <https://www.gnu.org/licenses/>.
*/
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<AsyncEvent<?>> 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 {

Datei anzeigen

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Map> CommandMap = Reflection.getField(PluginManager.class, "commandMap", Map.class);
public static final Reflection.FieldAccessor<Map> Plugins = Reflection.getField(PluginManager.class, "plugins", Map.class);
public static final Reflection.FieldAccessor<Yaml> 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<ExecutorService> 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<String, Command> commandMap, Map<String, Plugin> 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<String, Plugin> 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<String, Plugin> 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();
}
}

Datei anzeigen

@ -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 <T> - field type.
*/
public interface FieldAccessor<T> {
/**
* 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 <T> FieldAccessor<T> getField(Class<?> target, String name, Class<T> 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 <T> FieldAccessor<T> getField(String className, String name, Class<T> 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 <T> FieldAccessor<T> getField(Class<?> target, Class<T> 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 <T> FieldAccessor<T> getField(String className, Class<T> fieldType, int index) {
return getField(getClass(className), fieldType, index);
}
public static <T> FieldAccessor<T> getField(Class<?> target, Class<T> fieldType, int index, Class<?>... parameters) {
return getField(target, null, fieldType, index, parameters);
}
private static <T> FieldAccessor<T> getField(Class<?> target, String name, Class<T> 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<T>() {
@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 <T> boolean matching(Field field, String name, Class<T> 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.
* <p>
* This is useful when looking up fields by a NMS or OBC type.
* <p>
*
* @param lookupName - the class name with variables.
* @return The class.
*/
public static Class<Object> getUntypedClass(String lookupName) {
@SuppressWarnings({ "rawtypes", "unchecked" })
Class<Object> clazz = (Class) getClass(lookupName);
return clazz;
}
/**
* Retrieve a class from its full name.
* <p>
* Strings enclosed with curly brackets - such as {TEXT} - will be replaced according to the following table:
* <p>
* <table border="1">
* <tr>
* <th>Variable</th>
* <th>Content</th>
* </tr>
* <tr>
* <td>{nms}</td>
* <td>Actual package name of net.minecraft.server.VERSION</td>
* </tr>
* <tr>
* <td>{obc}</td>
* <td>Actual pacakge name of org.bukkit.craftbukkit.VERSION</td>
* </tr>
* <tr>
* <td>{version}</td>
* <td>The current Minecraft package VERSION, if any.</td>
* </tr>
* </table>
*
* @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);
}
}
}

Datei anzeigen

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, Command> 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<String, Plugin> pluginsMap = ReflectionUtils.getFieldValue(pluginManager, "plugins");
Multimap<Plugin, Command> commands = ReflectionUtils.getFieldValue(pluginManager, "commandsByPlugin");
Multimap<Plugin, Listener> 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<String, Plugin> 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);
}
}

Datei anzeigen

@ -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 <https://www.gnu.org/licenses/>.
*/
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> 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> 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!");
}
}