diff --git a/build.gradle b/build.gradle index d872618cd..7ad511486 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,10 @@ allprojects { mavenCentral() maven { url 'https://libraries.minecraft.net' } maven { url 'https://jitpack.io' } + maven { + name 'sponge' + url 'http://repo.spongepowered.org/maven' + } } } @@ -109,6 +113,15 @@ dependencies { api 'org.projectlombok:lombok:1.18.12' annotationProcessor 'org.projectlombok:lombok:1.18.12' + // Code modification + api "org.ow2.asm:asm:${asmVersion}" + api "org.ow2.asm:asm-tree:${asmVersion}" + api "org.ow2.asm:asm-analysis:${asmVersion}" + api "org.ow2.asm:asm-util:${asmVersion}" + api "org.ow2.asm:asm-commons:${asmVersion}" + implementation 'com.google.guava:guava:21.0' + api "org.spongepowered:mixin:${mixinVersion}" + // Path finding api 'com.github.MadMartian:hydrazine-path-finding:1.4.2' diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..13d2e376f --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +asmVersion=8.0.1 +mixinVersion=0.8 \ No newline at end of file diff --git a/src/main/java/fr/themode/demo/Main.java b/src/main/java/fr/themode/demo/Main.java index 4f44f6414..af8cda8fc 100644 --- a/src/main/java/fr/themode/demo/Main.java +++ b/src/main/java/fr/themode/demo/Main.java @@ -36,6 +36,7 @@ public class Main { commandManager.register(new ShutdownCommand()); commandManager.register(new TeleportCommand()); + StorageManager storageManager = MinecraftServer.getStorageManager(); storageManager.defineDefaultStorageSystem(FileStorageSystem::new); diff --git a/src/main/java/net/minestom/server/Bootstrap.java b/src/main/java/net/minestom/server/Bootstrap.java new file mode 100644 index 000000000..4fa25f61a --- /dev/null +++ b/src/main/java/net/minestom/server/Bootstrap.java @@ -0,0 +1,57 @@ +package net.minestom.server; + +import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; +import net.minestom.server.extras.selfmodification.mixins.MixinCodeModifier; +import net.minestom.server.extras.selfmodification.mixins.MixinServiceMinestom; +import org.spongepowered.asm.launch.MixinBootstrap; +import org.spongepowered.asm.launch.platform.CommandLineOptions; +import org.spongepowered.asm.mixin.Mixins; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; + +/** + * Used to launch Minestom with the {@link MinestomOverwriteClassLoader} to allow for self-modifications + */ +public final class Bootstrap { + + public static void bootstrap(String mainClassFullName, String[] args) { + try { + ClassLoader classLoader = MinestomOverwriteClassLoader.getInstance(); + startMixin(args); + MinestomOverwriteClassLoader.getInstance().addCodeModifier(new MixinCodeModifier()); + + MixinServiceMinestom.gotoPreinitPhase(); + // ensure extensions are loaded when starting the server + Class serverClass = classLoader.loadClass("net.minestom.server.MinecraftServer"); + Method init = serverClass.getMethod("init"); + init.invoke(null); + MixinServiceMinestom.gotoInitPhase(); + + MixinServiceMinestom.gotoDefaultPhase(); + + Class mainClass = classLoader.loadClass(mainClassFullName); + Method main = mainClass.getDeclaredMethod("main", String[].class); + main.invoke(null, new Object[] { args }); + } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException | ClassNotFoundException e) { + e.printStackTrace(); + } + } + + private static void startMixin(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + // hacks required to pass custom arguments + Method start = MixinBootstrap.class.getDeclaredMethod("start"); + start.setAccessible(true); + if (! ((boolean)start.invoke(null)) ) { + return; + } + + Method doInit = MixinBootstrap.class.getDeclaredMethod("doInit", CommandLineOptions.class); + doInit.setAccessible(true); + doInit.invoke(null, CommandLineOptions.ofArgs(Arrays.asList(args))); + + MixinBootstrap.getPlatform().inject(); + Mixins.getConfigs().forEach(c -> MinestomOverwriteClassLoader.getInstance().protectedPackages.add(c.getConfig().getMixinPackage())); + } +} diff --git a/src/main/java/net/minestom/server/MinecraftServer.java b/src/main/java/net/minestom/server/MinecraftServer.java index b108abff2..74dfc2f1e 100644 --- a/src/main/java/net/minestom/server/MinecraftServer.java +++ b/src/main/java/net/minestom/server/MinecraftServer.java @@ -147,6 +147,9 @@ public class MinecraftServer { public static MinecraftServer init() { if (minecraftServer != null) // don't init twice return minecraftServer; + extensionManager = new ExtensionManager(); + extensionManager.loadExtensions(); + // warmup/force-init registries // without this line, registry types that are not loaded explicitly will have an internal empty registry in Registries // That can happen with PotionType for instance, if no code tries to access a PotionType field @@ -182,8 +185,6 @@ public class MinecraftServer { updateManager = new UpdateManager(); - extensionManager = new ExtensionManager(); - lootTableManager = new LootTableManager(); tagManager = new TagManager(); @@ -454,7 +455,6 @@ public class MinecraftServer { updateManager.start(); nettyServer.start(address, port); long t1 = -System.nanoTime(); - extensionManager.loadExtensionJARs(); // Init extensions // TODO: Extensions should handle depending on each other and have a load-order. extensionManager.getExtensions().forEach(Extension::preInitialize); diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index efc0b2c38..80dd06717 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -1,36 +1,35 @@ package net.minestom.server.extensions; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import com.google.gson.*; import lombok.extern.slf4j.Slf4j; +import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.LoggerFactory; +import org.spongepowered.asm.mixin.Mixins; -import java.io.File; -import java.io.InputStream; -import java.io.InputStreamReader; +import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.zip.ZipFile; @Slf4j public class ExtensionManager { - private final Map extensionLoaders = new HashMap<>(); + private final Map extensionLoaders = new HashMap<>(); private final Map extensions = new HashMap<>(); private final File extensionFolder = new File("extensions"); + private final static String INDEV_CLASSES_FOLDER = "minestom.extension.indevfolder.classes"; + private final static String INDEV_RESOURCES_FOLDER = "minestom.extension.indevfolder.resources"; public ExtensionManager() { } - public void loadExtensionJARs() { + public void loadExtensions() { if (!extensionFolder.exists()) { if (!extensionFolder.mkdirs()) { log.error("Could not find or create the extension folder, extensions will not be loaded!"); @@ -38,32 +37,41 @@ public class ExtensionManager { } } - for (File file : extensionFolder.listFiles()) { - if (file.isDirectory()) { - continue; - } - if (!file.getName().endsWith(".jar")) { - continue; - } + List discoveredExtensions = discoverExtensions(); + setupCodeModifiers(discoveredExtensions); + + for (DiscoveredExtension discoveredExtension : discoveredExtensions) { URLClassLoader loader; + URL[] urls = new URL[discoveredExtension.files.length]; try { - URL url = file.toURI().toURL(); - loader = loadJar(url); - extensionLoaders.put(url, loader); + for (int i = 0; i < urls.length; i++) { + urls[i] = discoveredExtension.files[i].toURI().toURL(); + } + loader = newClassLoader(urls); } catch (MalformedURLException e) { - log.error(String.format("Failed to get URL for file %s.", file.getPath())); + log.error("Failed to get URL.", e); return; } InputStream extensionInputStream = loader.getResourceAsStream("extension.json"); if (extensionInputStream == null) { - log.error(String.format("Failed to find extension.json in the file '%s'.", file.getPath())); + StringBuilder urlsString = new StringBuilder(); + for (int i = 0; i < urls.length; i++) { + URL url = urls[i]; + if(i != 0) { + urlsString.append(" ; "); + } + urlsString.append("'").append(url.toString()).append("'"); + } + log.error(String.format("Failed to find extension.json in the urls '%s'.", urlsString)); return; } JsonObject extensionDescription = JsonParser.parseReader(new InputStreamReader(extensionInputStream)).getAsJsonObject(); String mainClass = extensionDescription.get("entrypoint").getAsString(); String extensionName = extensionDescription.get("name").getAsString(); - + + extensionLoaders.put(extensionName, loader); + if (extensions.containsKey(extensionName.toLowerCase())) { log.error(String.format("An extension called '%s' has already been registered.", extensionName)); return; @@ -137,14 +145,53 @@ public class ExtensionManager { } } + private List discoverExtensions() { + Gson gson = new Gson(); + List extensions = new LinkedList<>(); + for (File file : extensionFolder.listFiles()) { + if (file.isDirectory()) { + continue; + } + if (!file.getName().endsWith(".jar")) { + continue; + } + try(ZipFile f = new ZipFile(file); + InputStreamReader reader = new InputStreamReader(f.getInputStream(f.getEntry("extension.json")))) { + + DiscoveredExtension extension = new DiscoveredExtension(); + extension.files = new File[]{file}; + extension.description = gson.fromJson(reader, JsonObject.class); + extensions.add(extension); + } catch (IOException e) { + e.printStackTrace(); + } + } + + // this allows developers to have their extension discovered while working on it, without having to build a jar and put in the extension folder + if(System.getProperty(INDEV_CLASSES_FOLDER) != null && System.getProperty(INDEV_RESOURCES_FOLDER) != null) { + log.info("Found indev folders for extension. Adding to list of discovered extensions."); + String extensionClasses = System.getProperty(INDEV_CLASSES_FOLDER); + String extensionResources = System.getProperty(INDEV_RESOURCES_FOLDER); + try(InputStreamReader reader = new InputStreamReader(new FileInputStream(new File(extensionResources, "extension.json")))) { + DiscoveredExtension extension = new DiscoveredExtension(); + extension.files = new File[] { new File(extensionClasses), new File(extensionResources) }; + extension.description = gson.fromJson(reader, JsonObject.class); + extensions.add(extension); + } catch (IOException e) { + e.printStackTrace(); + } + } + return extensions; + } + /** * Loads a URL into the classpath. * - * @param url {@link URL} (usually a JAR) that should be loaded. + * @param urls {@link URL} (usually a JAR) that should be loaded. */ @NotNull - public URLClassLoader loadJar(@NotNull URL url) { - return URLClassLoader.newInstance(new URL[]{url}, ExtensionManager.class.getClassLoader()); + public URLClassLoader newClassLoader(@NotNull URL[] urls) { + return URLClassLoader.newInstance(urls, ExtensionManager.class.getClassLoader()); } @NotNull @@ -163,7 +210,44 @@ public class ExtensionManager { } @NotNull - public Map getExtensionLoaders() { + public Map getExtensionLoaders() { return new HashMap<>(extensionLoaders); } + + /** + * Extensions are allowed to apply Mixin transformers, the magic happens here + */ + private void setupCodeModifiers(List extensions) { + ClassLoader cl = getClass().getClassLoader(); + if(!(cl instanceof MinestomOverwriteClassLoader)) { + log.warn("Current class loader is not a MinestomOverwriteClassLoader, but "+cl+". This disables code modifiers (Mixin support is therefore disabled)"); + return; + } + MinestomOverwriteClassLoader modifiableClassLoader = (MinestomOverwriteClassLoader)cl; + log.info("Start loading code modifiers..."); + for(DiscoveredExtension extension : extensions) { + try { + if(extension.description.has("codeModifiers")) { + JsonArray codeModifierClasses = extension.description.getAsJsonArray("codeModifiers"); + for(JsonElement elem : codeModifierClasses) { + modifiableClassLoader.loadModifier(extension.files, elem.getAsString()); + } + } + if(extension.description.has("mixinConfig")) { + String mixinConfigFile = extension.description.get("mixinConfig").getAsString(); + Mixins.addConfiguration(mixinConfigFile); + log.info("Found mixin in extension "+extension.description.get("name").getAsString()+": "+mixinConfigFile); + } + } catch (Exception e) { + e.printStackTrace(); + log.error("Failed to load code modifier for extension in files: "+Arrays.toString(extension.files), e); + } + } + log.info("Done loading code modifiers."); + } + + private class DiscoveredExtension { + private File[] files; + private JsonObject description; + } } diff --git a/src/main/java/net/minestom/server/extras/selfmodification/CodeModifier.java b/src/main/java/net/minestom/server/extras/selfmodification/CodeModifier.java new file mode 100644 index 000000000..faa739c0a --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/CodeModifier.java @@ -0,0 +1,22 @@ +package net.minestom.server.extras.selfmodification; + +import org.objectweb.asm.tree.ClassNode; + +/** + * Will be called by {@link MinestomOverwriteClassLoader} to transform classes at load-time + */ +public abstract class CodeModifier { + /** + * Must return true iif the class node has been modified + * @param source + * @return + */ + public abstract boolean transform(ClassNode source); + + /** + * Beginning of the class names to transform. + * 'null' is allowed to transform any class, but not recommended + * @return + */ + public abstract String getNamespace(); +} diff --git a/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java b/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java new file mode 100644 index 000000000..721f7c30c --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java @@ -0,0 +1,228 @@ +package net.minestom.server.extras.selfmodification; + +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.tree.ClassNode; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * Class Loader that can modify class bytecode when they are loaded + */ +@Slf4j +public class MinestomOverwriteClassLoader extends URLClassLoader { + + private static MinestomOverwriteClassLoader INSTANCE; + + /** + * Classes that cannot be loaded/modified by this classloader. + * Will go through parent class loader + */ + public final Set protectedClasses = new HashSet<>() { + { + add("net.minestom.server.extras.selfmodification.CodeModifier"); + add("net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader"); + } + }; + public final Set protectedPackages = new HashSet<>() { + { + add("com.google"); + add("com.mojang"); + add("org.objectweb.asm"); + add("org.slf4j"); + add("org.apache"); + add("org.spongepowered"); + add("net.minestom.server.extras.selfmodification"); + } + }; + /** + * Used to let ASM find out common super types, without actually commiting to loading them + * Otherwise ASM would accidentally load classes we might want to modify + */ + private final URLClassLoader asmClassLoader; + + // TODO: replace by tree to optimize lookup times. We can use the fact that package names are split with '.' to allow for fast lookup + // TODO: eg. Node("java", Node("lang"), Node("io")). Loading "java.nio.Channel" would apply modifiers from "java", but not "java.io" or "java.lang". + // TODO: that's an example, please don't modify standard library classes. And this classloader should not let you do it because it first asks the platform classloader + + // TODO: priorities? + private List modifiers = new LinkedList<>(); + + private MinestomOverwriteClassLoader(ClassLoader parent) { + super("Minestom ClassLoader", extractURLsFromClasspath(), parent); + asmClassLoader = newChild(new URL[0]); + } + + public static MinestomOverwriteClassLoader getInstance() { + if(INSTANCE == null) { + synchronized (MinestomOverwriteClassLoader.class) { + if(INSTANCE == null) { + INSTANCE = new MinestomOverwriteClassLoader(MinestomOverwriteClassLoader.class.getClassLoader()); + } + } + } + return INSTANCE; + } + + private static URL[] extractURLsFromClasspath() { + String classpath = System.getProperty("java.class.path"); + String[] parts = classpath.split(";"); + URL[] urls = new URL[parts.length]; + for (int i = 0; i < urls.length; i++) { + try { + String part = parts[i]; + String protocol; + if(part.contains("!")) { + protocol = "jar://"; + } else { + protocol = "file://"; + } + urls[i] = new URL(protocol+part); + } catch (MalformedURLException e) { + throw new Error(e); + } + } + return urls; + } + + @Override + public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + Class loadedClass = findLoadedClass(name); + if(loadedClass != null) + return loadedClass; + + try { + // we do not load system classes by ourselves + Class systemClass = ClassLoader.getPlatformClassLoader().loadClass(name); + log.trace("System class: "+systemClass); + return systemClass; + } catch (ClassNotFoundException e) { + try { + if(isProtected(name)) { + log.trace("Protected: "+name); + return super.loadClass(name, resolve); + } + + return define(name, loadBytes(name, true), resolve); + } catch (Exception ex) { + log.trace("Fail to load class, resorting to parent loader: "+name, ex); + // fail to load class, let parent load + // this forbids code modification, but at least it will load + return super.loadClass(name, resolve); + } + } + } + + private boolean isProtected(String name) { + if(!protectedClasses.contains(name)) { + for(String start : protectedPackages) { + if(name.startsWith(start)) + return true; + } + return false; + } + return true; + } + + private Class define(String name, byte[] bytes, boolean resolve) throws ClassNotFoundException { + Class defined = defineClass(name, bytes, 0, bytes.length); + log.trace("Loaded with code modifiers: "+name); + if(resolve) { + resolveClass(defined); + } + return defined; + } + + /** + * Loads and possibly transforms class bytecode corresponding to the given binary name. + * @param name + * @param transform + * @return + * @throws IOException + * @throws ClassNotFoundException + */ + public byte[] loadBytes(String name, boolean transform) throws IOException, ClassNotFoundException { + if(name == null) + throw new ClassNotFoundException(); + String path = name.replace(".", "/") + ".class"; + byte[] bytes = getResourceAsStream(path).readAllBytes(); + if(transform && !isProtected(name)) { + ClassReader reader = new ClassReader(bytes); + ClassNode node = new ClassNode(); + reader.accept(node, 0); + boolean modified = false; + synchronized (modifiers) { + for(CodeModifier modifier : modifiers) { + boolean shouldModify = modifier.getNamespace() == null || name.startsWith(modifier.getNamespace()); + if(shouldModify) { + modified |= modifier.transform(node); + } + } + } + if(modified) { + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES) { + @Override + protected ClassLoader getClassLoader() { + return asmClassLoader; + } + }; + node.accept(writer); + bytes = writer.toByteArray(); + log.trace("Modified "+name); + } + } + return bytes; + } + + // overriden to increase access (from protected to public) + @Override + public Class findClass(String name) throws ClassNotFoundException { + return super.findClass(name); + } + + @NotNull + public URLClassLoader newChild(@NotNull URL[] urls) { + return URLClassLoader.newInstance(urls, this); + } + + public void loadModifier(File[] originFiles, String codeModifierClass) { + URL[] urls = new URL[originFiles.length]; + try { + for (int i = 0; i < originFiles.length; i++) { + urls[i] = originFiles[i].toURI().toURL(); + } + URLClassLoader loader = newChild(urls); + Class modifierClass = loader.loadClass(codeModifierClass); + if(CodeModifier.class.isAssignableFrom(modifierClass)) { + CodeModifier modifier = (CodeModifier) modifierClass.getDeclaredConstructor().newInstance(); + synchronized (modifiers) { + log.warn("Added Code modifier: "+modifier); + addCodeModifier(modifier); + } + } + } catch (MalformedURLException | ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) { + e.printStackTrace(); + } + } + + public void addCodeModifier(CodeModifier modifier) { + synchronized (modifiers) { + modifiers.add(modifier); + } + } + + public List getModifiers() { + return modifiers; + } +} diff --git a/src/main/java/net/minestom/server/extras/selfmodification/mixins/GlobalPropertyServiceMinestom.java b/src/main/java/net/minestom/server/extras/selfmodification/mixins/GlobalPropertyServiceMinestom.java new file mode 100644 index 000000000..e7f5df0f3 --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/GlobalPropertyServiceMinestom.java @@ -0,0 +1,71 @@ +package net.minestom.server.extras.selfmodification.mixins; + +import org.spongepowered.asm.service.IGlobalPropertyService; +import org.spongepowered.asm.service.IPropertyKey; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Global properties service for Mixin + */ +public class GlobalPropertyServiceMinestom implements IGlobalPropertyService { + + private class BasicProperty implements IPropertyKey { + + private final String name; + + public BasicProperty(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BasicProperty that = (BasicProperty) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public String toString() { + return "BasicProperty{" + + "name='" + name + '\'' + + '}'; + } + } + + private final Map keys = new HashMap<>(); + private final Map values = new HashMap<>(); + + @Override + public IPropertyKey resolveKey(String name) { + return keys.computeIfAbsent(name, k -> new BasicProperty(k)); + } + + @Override + public T getProperty(IPropertyKey key) { + return (T) values.get(key); + } + + @Override + public void setProperty(IPropertyKey key, Object value) { + values.put(key, value); + } + + @Override + public T getProperty(IPropertyKey key, T defaultValue) { + return (T) values.getOrDefault(key, defaultValue); + } + + @Override + public String getPropertyString(IPropertyKey key, String defaultValue) { + return (String) values.getOrDefault(key, defaultValue); + } +} diff --git a/src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomBytecodeProvider.java b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomBytecodeProvider.java new file mode 100644 index 000000000..c8b3f20cb --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomBytecodeProvider.java @@ -0,0 +1,41 @@ +package net.minestom.server.extras.selfmodification.mixins; + +import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.service.IClassBytecodeProvider; + +import java.io.IOException; + +/** + * Provides class bytecode for Mixin + */ +public class MinestomBytecodeProvider implements IClassBytecodeProvider { + private final MinestomOverwriteClassLoader classLoader; + + public MinestomBytecodeProvider(MinestomOverwriteClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public ClassNode getClassNode(String name) throws ClassNotFoundException, IOException { + return getClassNode(name, false); + } + + private ClassNode loadNode(String name, boolean transform) throws ClassNotFoundException { + ClassNode node = new ClassNode(); + ClassReader reader; + try { + reader = new ClassReader(classLoader.loadBytes(name, transform)); + } catch (IOException e) { + throw new ClassNotFoundException("Could not load ClassNode with name "+name, e); + } + reader.accept(node, 0); + return node; + } + + @Override + public ClassNode getClassNode(String name, boolean runTransformers) throws ClassNotFoundException, IOException { + return loadNode(name, runTransformers); + } +} diff --git a/src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomClassProvider.java b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomClassProvider.java new file mode 100644 index 000000000..ae3b98b4f --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomClassProvider.java @@ -0,0 +1,37 @@ +package net.minestom.server.extras.selfmodification.mixins; + +import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; +import org.spongepowered.asm.service.IClassProvider; + +import java.net.URL; + +/** + * Provides classes for Mixin + */ +public class MinestomClassProvider implements IClassProvider { + private final MinestomOverwriteClassLoader classLoader; + + public MinestomClassProvider(MinestomOverwriteClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public URL[] getClassPath() { + return classLoader.getURLs(); + } + + @Override + public Class findClass(String name) throws ClassNotFoundException { + return classLoader.findClass(name); + } + + @Override + public Class findClass(String name, boolean initialize) throws ClassNotFoundException { + return Class.forName(name, initialize, Thread.currentThread().getContextClassLoader()); + } + + @Override + public Class findAgentClass(String name, boolean initialize) throws ClassNotFoundException { + return Class.forName(name, initialize, classLoader); + } +} diff --git a/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinAuditTrailMinestom.java b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinAuditTrailMinestom.java new file mode 100644 index 000000000..d8a2784af --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinAuditTrailMinestom.java @@ -0,0 +1,25 @@ +package net.minestom.server.extras.selfmodification.mixins; + +import lombok.extern.slf4j.Slf4j; +import org.spongepowered.asm.service.IMixinAuditTrail; + +/** + * Takes care of logging mixin operations + */ +@Slf4j +public class MixinAuditTrailMinestom implements IMixinAuditTrail { + @Override + public void onApply(String className, String mixinName) { + log.trace("Applied mixin "+mixinName+" to class "+className); + } + + @Override + public void onPostProcess(String className) { + log.trace("Post processing "+className); + } + + @Override + public void onGenerate(String className, String generatorName) { + log.trace("Generating class "+className+" via generator "+generatorName); + } +} diff --git a/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinCodeModifier.java b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinCodeModifier.java new file mode 100644 index 000000000..c52e51713 --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinCodeModifier.java @@ -0,0 +1,53 @@ +package net.minestom.server.extras.selfmodification.mixins; + +import net.minestom.server.extras.selfmodification.CodeModifier; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.MixinEnvironment; +import org.spongepowered.asm.transformers.TreeTransformer; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * CodeModifier responsible for applying Mixins during class load + */ +public class MixinCodeModifier extends CodeModifier { + + /** + * Call MixinTransformer's transformClass + */ + private final Method transformClassMethod; + private final TreeTransformer transformer; + + public MixinCodeModifier() { + try { + // MixinTransformer is package-protected, so we have to force to gain access + Class mixinTransformerClass = Class.forName("org.spongepowered.asm.mixin.transformer.MixinTransformer"); + Constructor ctor = mixinTransformerClass.getDeclaredConstructor(); + ctor.setAccessible(true); + this.transformer = (TreeTransformer) ctor.newInstance(); + + // we can't access the MixinTransformer type here, so we use reflection to access the method + transformClassMethod = mixinTransformerClass.getDeclaredMethod("transformClass", MixinEnvironment.class, String.class, ClassNode.class); + transformClassMethod.setAccessible(true); + } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException | ClassNotFoundException e) { + throw new RuntimeException("Failed to initialize MixinCodeModifier", e); + } + } + + @Override + public boolean transform(ClassNode source) { + try { + return (boolean) transformClassMethod.invoke(transformer, MixinEnvironment.getEnvironment(MixinEnvironment.Phase.DEFAULT), source.name.replace("/", "."), source); + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + return false; + } + + @Override + public String getNamespace() { + return null; + } +} diff --git a/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinPlatformAgentMinestom.java b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinPlatformAgentMinestom.java new file mode 100644 index 000000000..c35bf2534 --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinPlatformAgentMinestom.java @@ -0,0 +1,29 @@ +package net.minestom.server.extras.selfmodification.mixins; + +import org.spongepowered.asm.launch.platform.IMixinPlatformServiceAgent; +import org.spongepowered.asm.launch.platform.MixinPlatformAgentAbstract; +import org.spongepowered.asm.launch.platform.MixinPlatformManager; +import org.spongepowered.asm.launch.platform.container.IContainerHandle; +import org.spongepowered.asm.util.Constants; + +import java.util.Collection; + +public class MixinPlatformAgentMinestom extends MixinPlatformAgentAbstract implements IMixinPlatformServiceAgent { + @Override + public void init() { } + + @Override + public String getSideName() { + return Constants.SIDE_SERVER; + } + + @Override + public AcceptResult accept(MixinPlatformManager manager, IContainerHandle handle) { + return AcceptResult.ACCEPTED; + } + + @Override + public Collection getMixinContainers() { + return null; + } +} diff --git a/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinServiceMinestom.java b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinServiceMinestom.java new file mode 100644 index 000000000..ef4c4c760 --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinServiceMinestom.java @@ -0,0 +1,108 @@ +package net.minestom.server.extras.selfmodification.mixins; + +import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; +import org.spongepowered.asm.launch.platform.container.ContainerHandleVirtual; +import org.spongepowered.asm.launch.platform.container.IContainerHandle; +import org.spongepowered.asm.mixin.MixinEnvironment; +import org.spongepowered.asm.service.*; +import org.spongepowered.asm.util.IConsumer; + +import java.io.InputStream; +import java.util.Collection; +import java.util.Collections; + +public class MixinServiceMinestom extends MixinServiceAbstract { + + private final MinestomOverwriteClassLoader classLoader; + private final MinestomClassProvider classProvider; + private final MinestomBytecodeProvider bytecodeProvider; + private final MixinAuditTrailMinestom auditTrail; + private static MixinServiceMinestom INSTANCE = null; + private IConsumer phaseConsumer; + + public MixinServiceMinestom() { + INSTANCE = this; + this.classLoader = MinestomOverwriteClassLoader.getInstance(); + classProvider = new MinestomClassProvider(classLoader); + bytecodeProvider = new MinestomBytecodeProvider(classLoader); + auditTrail = new MixinAuditTrailMinestom(); + } + + @Override + public String getName() { + return "Minestom"; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public IClassProvider getClassProvider() { + return classProvider; + } + + @Override + public IClassBytecodeProvider getBytecodeProvider() { + return bytecodeProvider; + } + + @Override + public ITransformerProvider getTransformerProvider() { + return null; + } + + @Override + public Collection getPlatformAgents() { + return Collections.singletonList("net.minestom.server.extras.selfmodification.mixins.MixinPlatformAgentMinestom"); + } + + @Override + public IContainerHandle getPrimaryContainer() { + return new ContainerHandleVirtual("Minestom"); + } + + @Override + public InputStream getResourceAsStream(String name) { + return classLoader.getResourceAsStream(name); + } + + @Override + public IClassTracker getClassTracker() { + return null; + } + + @Override + public IMixinAuditTrail getAuditTrail() { + return auditTrail; + } + + @Override + public void wire(MixinEnvironment.Phase phase, IConsumer phaseConsumer) { + super.wire(phase, phaseConsumer); + this.phaseConsumer = phaseConsumer; + } + + private void gotoPhase(MixinEnvironment.Phase phase) { + phaseConsumer.accept(phase); + } + + public static void gotoPreinitPhase() { + if(INSTANCE != null) { + INSTANCE.gotoPhase(MixinEnvironment.Phase.PREINIT); + } + } + + public static void gotoInitPhase() { + if(INSTANCE != null) { + INSTANCE.gotoPhase(MixinEnvironment.Phase.INIT); + } + } + + public static void gotoDefaultPhase() { + if(INSTANCE != null) { + INSTANCE.gotoPhase(MixinEnvironment.Phase.DEFAULT); + } + } +} diff --git a/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinServiceMinestomBootstrap.java b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinServiceMinestomBootstrap.java new file mode 100644 index 000000000..8d514d417 --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinServiceMinestomBootstrap.java @@ -0,0 +1,20 @@ +package net.minestom.server.extras.selfmodification.mixins; + +import org.spongepowered.asm.service.IMixinServiceBootstrap; + +public class MixinServiceMinestomBootstrap implements IMixinServiceBootstrap { + @Override + public String getName() { + return "MinestomBootstrap"; + } + + @Override + public String getServiceClassName() { + return "net.minestom.server.extras.selfmodification.mixins.MixinServiceMinestom"; + } + + @Override + public void bootstrap() { + + } +} diff --git a/src/main/resources/META-INF/services/org.spongepowered.asm.service.IGlobalPropertyService b/src/main/resources/META-INF/services/org.spongepowered.asm.service.IGlobalPropertyService new file mode 100644 index 000000000..74a63c27d --- /dev/null +++ b/src/main/resources/META-INF/services/org.spongepowered.asm.service.IGlobalPropertyService @@ -0,0 +1 @@ +net.minestom.server.extras.selfmodification.mixins.GlobalPropertyServiceMinestom \ No newline at end of file diff --git a/src/main/resources/META-INF/services/org.spongepowered.asm.service.IMixinService b/src/main/resources/META-INF/services/org.spongepowered.asm.service.IMixinService new file mode 100644 index 000000000..113a8c7ca --- /dev/null +++ b/src/main/resources/META-INF/services/org.spongepowered.asm.service.IMixinService @@ -0,0 +1 @@ +net.minestom.server.extras.selfmodification.mixins.MixinServiceMinestom \ No newline at end of file diff --git a/src/main/resources/META-INF/services/org.spongepowered.asm.service.IMixinServiceBootstrap b/src/main/resources/META-INF/services/org.spongepowered.asm.service.IMixinServiceBootstrap new file mode 100644 index 000000000..d357615c7 --- /dev/null +++ b/src/main/resources/META-INF/services/org.spongepowered.asm.service.IMixinServiceBootstrap @@ -0,0 +1 @@ +net.minestom.server.extras.selfmodification.mixins.MixinServiceMinestomBootstrap \ No newline at end of file diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index f8c195373..ea0bfe2c8 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -2,13 +2,16 @@ - + + + + - + \ No newline at end of file diff --git a/src/test/java/testextension/TestExtension.java b/src/test/java/testextension/TestExtension.java new file mode 100644 index 000000000..3d2d72685 --- /dev/null +++ b/src/test/java/testextension/TestExtension.java @@ -0,0 +1,15 @@ +package testextension; + +import net.minestom.server.extensions.Extension; + +public class TestExtension extends Extension { + @Override + public void initialize() { + System.out.println("Hello from extension!"); + } + + @Override + public void terminate() { + + } +} diff --git a/src/test/java/testextension/TestExtensionLauncherArgs.java b/src/test/java/testextension/TestExtensionLauncherArgs.java new file mode 100644 index 000000000..f9130f23f --- /dev/null +++ b/src/test/java/testextension/TestExtensionLauncherArgs.java @@ -0,0 +1,17 @@ +package testextension; + +import net.minestom.server.Bootstrap; + +// To launch with VM arguments: +// -Dminestom.extension.indevfolder.classes=build/classes/java/test/ -Dminestom.extension.indevfolder.resources=build/resources/test/ +public class TestExtensionLauncherArgs { + + public static void main(String[] args) { + String[] argsWithMixins = new String[args.length+2]; + System.arraycopy(args, 0, argsWithMixins, 0, args.length); + argsWithMixins[argsWithMixins.length-2] = "--mixin"; + argsWithMixins[argsWithMixins.length-1] = "mixins.testextension.json"; + Bootstrap.bootstrap("fr.themode.demo.MainDemo", argsWithMixins); + } + +} diff --git a/src/test/java/testextension/TestExtensionLauncherNoSetup.java b/src/test/java/testextension/TestExtensionLauncherNoSetup.java new file mode 100644 index 000000000..a3e358579 --- /dev/null +++ b/src/test/java/testextension/TestExtensionLauncherNoSetup.java @@ -0,0 +1,15 @@ +package testextension; + +import net.minestom.server.Bootstrap; +import org.spongepowered.asm.launch.MixinBootstrap; +import org.spongepowered.asm.mixin.Mixins; + +// To launch with VM arguments: +// -Dminestom.extension.indevfolder.classes=build/classes/java/test/ -Dminestom.extension.indevfolder.resources=build/resources/test/ +public class TestExtensionLauncherNoSetup { + + public static void main(String[] args) { + Bootstrap.bootstrap("fr.themode.demo.MainDemo", args); + } + +} diff --git a/src/test/java/testextension/TestModifier.java b/src/test/java/testextension/TestModifier.java new file mode 100644 index 000000000..602ba13f6 --- /dev/null +++ b/src/test/java/testextension/TestModifier.java @@ -0,0 +1,37 @@ +package testextension; + +import net.minestom.server.extras.selfmodification.CodeModifier; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.*; + +import java.util.List; + +public class TestModifier extends CodeModifier implements Opcodes { + @Override + public boolean transform(ClassNode source) { + if(source.name.equals("net/minestom/server/instance/InstanceContainer")) { + System.out.println("Modifying code of "+source.name); + MethodNode constructor = findConstructor(source.methods); + constructor.instructions.insert(constructor.instructions.getFirst(), buildInjectionCode()); + return true; + } + return false; + } + + private InsnList buildInjectionCode() { + InsnList list = new InsnList(); + list.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;")); + list.add(new LdcInsnNode("Hello from modified code!!")); + list.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false)); + return list; + } + + private MethodNode findConstructor(List methods) { + return methods.stream().filter(m -> m.name.equals("")).findFirst().orElseThrow(); + } + + @Override + public String getNamespace() { + return "net.minestom.server"; + } +} diff --git a/src/test/java/testextension/mixins/DynamicChunkMixin.java b/src/test/java/testextension/mixins/DynamicChunkMixin.java new file mode 100644 index 000000000..a71075469 --- /dev/null +++ b/src/test/java/testextension/mixins/DynamicChunkMixin.java @@ -0,0 +1,18 @@ +package testextension.mixins; + +import net.minestom.server.instance.DynamicChunk; +import net.minestom.server.instance.block.Block; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +@Mixin(DynamicChunk.class) +public class DynamicChunkMixin { + + @ModifyVariable(method = "setBlock", at = @At("HEAD"), index = 4, require = 1, argsOnly = true, remap = false) + public short oopsAllTnt(short blockStateId) { + if(blockStateId != 0) + return Block.TNT.getBlockId(); + return 0; + } +} diff --git a/src/test/java/testextension/mixins/InstanceContainerMixin.java b/src/test/java/testextension/mixins/InstanceContainerMixin.java new file mode 100644 index 000000000..b3299afbd --- /dev/null +++ b/src/test/java/testextension/mixins/InstanceContainerMixin.java @@ -0,0 +1,21 @@ +package testextension.mixins; + +import net.minestom.server.data.Data; +import net.minestom.server.instance.InstanceContainer; +import net.minestom.server.instance.block.Block; +import net.minestom.server.instance.block.CustomBlock; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyVariable; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(InstanceContainer.class) +public class InstanceContainerMixin { + + @Inject(method = "", at = @At("RETURN")) + private void onRunHead(CallbackInfo ci) { + System.out.println("Hello from Mixin!!!"); + } + +} diff --git a/src/test/resources/extension.json b/src/test/resources/extension.json new file mode 100644 index 000000000..c7d404173 --- /dev/null +++ b/src/test/resources/extension.json @@ -0,0 +1,8 @@ +{ + "entrypoint": "testextension.TestExtension", + "name": "Test extension", + "codeModifiers": [ + "testextension.TestModifier" + ], + "mixinConfig": "mixins.testextension.json" +} \ No newline at end of file diff --git a/src/test/resources/mixins.testextension.json b/src/test/resources/mixins.testextension.json new file mode 100644 index 000000000..6fd90870c --- /dev/null +++ b/src/test/resources/mixins.testextension.json @@ -0,0 +1,11 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "testextension.mixins", + "target": "@env(DEFAULT)", + "compatibilityLevel": "JAVA_11", + "mixins": [ + "InstanceContainerMixin", + "DynamicChunkMixin" + ] +} \ No newline at end of file