From 164719090aabd61a8a9270f8dc4b6cf6e00cb037 Mon Sep 17 00:00:00 2001 From: jglrxavpok Date: Thu, 20 Aug 2020 02:06:58 +0200 Subject: [PATCH 1/5] (WIP) Support for runtime code modification --- .../java/fr/themode/demo/MainWrapper.java | 11 +++ .../java/net/minestom/server/Bootstrap.java | 23 ++++++ .../net/minestom/server/MinecraftServer.java | 6 +- .../server/extensions/ExtensionManager.java | 78 ++++++++++++++++--- .../MinestomOverwriteClassLoader.java | 72 +++++++++++++++++ 5 files changed, 175 insertions(+), 15 deletions(-) create mode 100644 src/main/java/fr/themode/demo/MainWrapper.java create mode 100644 src/main/java/net/minestom/server/Bootstrap.java create mode 100644 src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java diff --git a/src/main/java/fr/themode/demo/MainWrapper.java b/src/main/java/fr/themode/demo/MainWrapper.java new file mode 100644 index 000000000..b77049231 --- /dev/null +++ b/src/main/java/fr/themode/demo/MainWrapper.java @@ -0,0 +1,11 @@ +package fr.themode.demo; + +import net.minestom.server.Bootstrap; + +public class MainWrapper { + + public static void main(String[] args) { + Bootstrap.bootstrap("fr.themode.demo.Main", args); + } + +} 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..a301fb2cf --- /dev/null +++ b/src/main/java/net/minestom/server/Bootstrap.java @@ -0,0 +1,23 @@ +package net.minestom.server; + +import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Used to launch Minestom with the {@link MinestomOverwriteClassLoader} to allow for self-modifications + */ +public class Bootstrap { + + public static void bootstrap(String mainClassFullName, String[] args) { + try { + ClassLoader classLoader = new MinestomOverwriteClassLoader(Bootstrap.class.getClassLoader()); + 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(); + } + } +} diff --git a/src/main/java/net/minestom/server/MinecraftServer.java b/src/main/java/net/minestom/server/MinecraftServer.java index 8cca0a584..3f8abe050 100644 --- a/src/main/java/net/minestom/server/MinecraftServer.java +++ b/src/main/java/net/minestom/server/MinecraftServer.java @@ -145,6 +145,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 @@ -180,8 +183,6 @@ public class MinecraftServer { updateManager = new UpdateManager(); - extensionManager = new ExtensionManager(); - lootTableManager = new LootTableManager(); tagManager = new TagManager(); @@ -452,7 +453,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..19240ef19 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -1,13 +1,16 @@ package net.minestom.server.extensions; +import com.google.gson.Gson; import com.google.gson.JsonObject; import com.google.gson.JsonParser; 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 java.io.File; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Constructor; @@ -16,10 +19,8 @@ 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 { @@ -30,7 +31,7 @@ public class ExtensionManager { 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,14 +39,12 @@ 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 extension : discoveredExtensions) { URLClassLoader loader; + File file = extension.jarFile; try { URL url = file.toURI().toURL(); loader = loadJar(url); @@ -137,6 +136,30 @@ 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.jarFile = file; + extension.description = gson.fromJson(reader, JsonObject.class); + extensions.add(extension); + } catch (IOException e) { + e.printStackTrace(); + } + } + return extensions; + } + /** * Loads a URL into the classpath. * @@ -166,4 +189,35 @@ public class ExtensionManager { 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.warning("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("codeModifier")) { + String codeModifierClass = extension.description.get("codeModifier").getAsString(); + modifiableClassLoader.loadModifier(extension.jarFile, codeModifierClass); + } + // TODO: special support for mixins + } catch (Exception e) { + e.printStackTrace(); + log.error("Failed to load code modifier for extension "+extension.jarFile, e); + } + } + log.info("Done loading code modifiers."); + } + + private class DiscoveredExtension { + private File jarFile; + private JsonObject description; + } } 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..a9c4a1394 --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java @@ -0,0 +1,72 @@ +package net.minestom.server.extras.selfmodification; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; + +// TODO: register code modifiers +public class MinestomOverwriteClassLoader extends URLClassLoader { + + public MinestomOverwriteClassLoader(ClassLoader parent) { + super("Minestom ClassLoader", loadURLs(), parent); + } + + private static URL[] loadURLs() { + 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; + } + + private static URL[] fromParent(ClassLoader parent) { + if(parent instanceof URLClassLoader) { + return ((URLClassLoader) parent).getURLs(); + } + return new URL[0]; + } + + @Override + public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + Class loadedClass = findLoadedClass(name); + if(loadedClass != null) + return loadedClass; + + try { + Class systemClass = ClassLoader.getPlatformClassLoader().loadClass(name); + return systemClass; + } catch (ClassNotFoundException e) { + try { + String path = name.replace(".", "/") + ".class"; + byte[] bytes = getResourceAsStream(path).readAllBytes(); + Class defined = defineClass(name, bytes, 0, bytes.length); + if(resolve) { + resolveClass(defined); + } + return defined; + } catch (Exception ioException) { + // fail to load class, let parent load + // this forbids code modification, but at least it will load + return super.loadClass(name, resolve); + } + } + } + + public void loadModifier(File originFile, String codeModifierClass) { + throw new UnsupportedOperationException("TODO"); + } +} From 223af361d8eba5d1a50429c77db0e0ec9be29e8c Mon Sep 17 00:00:00 2001 From: jglrxavpok Date: Fri, 21 Aug 2020 01:32:59 +0200 Subject: [PATCH 2/5] Code modifiers and test extension --- build.gradle | 4 + src/main/java/fr/themode/demo/Main.java | 1 + .../java/fr/themode/demo/MainWrapper.java | 11 -- .../java/net/minestom/server/Bootstrap.java | 6 + .../server/extensions/ExtensionManager.java | 79 +++++++---- .../extras/selfmodification/CodeModifier.java | 22 +++ .../MinestomOverwriteClassLoader.java | 130 ++++++++++++++++-- .../java/testextension/TestExtension.java | 15 ++ .../testextension/TestExtensionLauncher.java | 13 ++ src/test/java/testextension/TestModifier.java | 37 +++++ src/test/resources/extension.json | 7 + 11 files changed, 278 insertions(+), 47 deletions(-) delete mode 100644 src/main/java/fr/themode/demo/MainWrapper.java create mode 100644 src/main/java/net/minestom/server/extras/selfmodification/CodeModifier.java create mode 100644 src/test/java/testextension/TestExtension.java create mode 100644 src/test/java/testextension/TestExtensionLauncher.java create mode 100644 src/test/java/testextension/TestModifier.java create mode 100644 src/test/resources/extension.json diff --git a/build.gradle b/build.gradle index 452ad0e84..0e5dbc815 100644 --- a/build.gradle +++ b/build.gradle @@ -111,6 +111,10 @@ dependencies { api 'org.projectlombok:lombok:1.18.12' annotationProcessor 'org.projectlombok:lombok:1.18.12' + // Code modification + api "org.ow2.asm:asm:6.2.1" + api "org.ow2.asm:asm-tree:6.2.1" + // Path finding api 'com.github.MadMartian:hydrazine-path-finding:1.3.1' 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/fr/themode/demo/MainWrapper.java b/src/main/java/fr/themode/demo/MainWrapper.java deleted file mode 100644 index b77049231..000000000 --- a/src/main/java/fr/themode/demo/MainWrapper.java +++ /dev/null @@ -1,11 +0,0 @@ -package fr.themode.demo; - -import net.minestom.server.Bootstrap; - -public class MainWrapper { - - public static void main(String[] args) { - Bootstrap.bootstrap("fr.themode.demo.Main", args); - } - -} diff --git a/src/main/java/net/minestom/server/Bootstrap.java b/src/main/java/net/minestom/server/Bootstrap.java index a301fb2cf..ff48514bd 100644 --- a/src/main/java/net/minestom/server/Bootstrap.java +++ b/src/main/java/net/minestom/server/Bootstrap.java @@ -13,6 +13,12 @@ public class Bootstrap { public static void bootstrap(String mainClassFullName, String[] args) { try { ClassLoader classLoader = new MinestomOverwriteClassLoader(Bootstrap.class.getClassLoader()); + + // ensure extensions are loaded when starting the server + Class serverClass = classLoader.loadClass("net.minestom.server.MinecraftServer"); + Method init = serverClass.getMethod("init"); + init.invoke(null); + Class mainClass = classLoader.loadClass(mainClassFullName); Method main = mainClass.getDeclaredMethod("main", String[].class); main.invoke(null, new Object[] { args }); diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index 19240ef19..33572a0c1 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -1,18 +1,13 @@ package net.minestom.server.extensions; -import com.google.gson.Gson; -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 java.io.File; -import java.io.IOException; -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; @@ -24,9 +19,11 @@ 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() { } @@ -42,27 +39,38 @@ public class ExtensionManager { List discoveredExtensions = discoverExtensions(); setupCodeModifiers(discoveredExtensions); - for (DiscoveredExtension extension : discoveredExtensions) { + for (DiscoveredExtension discoveredExtension : discoveredExtensions) { URLClassLoader loader; - File file = extension.jarFile; + 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; @@ -150,7 +158,22 @@ public class ExtensionManager { InputStreamReader reader = new InputStreamReader(f.getInputStream(f.getEntry("extension.json")))) { DiscoveredExtension extension = new DiscoveredExtension(); - extension.jarFile = file; + 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) { @@ -163,11 +186,11 @@ public class ExtensionManager { /** * 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 @@ -186,7 +209,7 @@ public class ExtensionManager { } @NotNull - public Map getExtensionLoaders() { + public Map getExtensionLoaders() { return new HashMap<>(extensionLoaders); } @@ -196,28 +219,30 @@ public class ExtensionManager { private void setupCodeModifiers(List extensions) { ClassLoader cl = getClass().getClassLoader(); if(!(cl instanceof MinestomOverwriteClassLoader)) { - log.warning("Current class loader is not a MinestomOverwriteClassLoader, but "+cl+". This disables code modifiers (Mixin support is therefore disabled)"); + 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("codeModifier")) { - String codeModifierClass = extension.description.get("codeModifier").getAsString(); - modifiableClassLoader.loadModifier(extension.jarFile, codeModifierClass); + if(extension.description.has("codeModifiers")) { + JsonArray codeModifierClasses = extension.description.getAsJsonArray("codeModifiers"); + for(JsonElement elem : codeModifierClasses) { + modifiableClassLoader.loadModifier(extension.files, elem.getAsString()); + } } // TODO: special support for mixins } catch (Exception e) { e.printStackTrace(); - log.error("Failed to load code modifier for extension "+extension.jarFile, e); + 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 jarFile; + 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 index a9c4a1394..80bded808 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java @@ -1,15 +1,61 @@ 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.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; -// TODO: register code modifiers +@Slf4j public class MinestomOverwriteClassLoader extends URLClassLoader { + /** + * Classes that cannot be loaded/modified by this classloader. + * Will go through parent class loader + */ + private static final Set protectedClasses = Set.of( + "net.minestom.server.extras.selfmodification.CodeModifier", + "net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader" + ); + 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 final Method findParentLoadedClass; + private final Class loadedCodeModifier; + public MinestomOverwriteClassLoader(ClassLoader parent) { super("Minestom ClassLoader", loadURLs(), parent); + try { + findParentLoadedClass = ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class); + findParentLoadedClass.setAccessible(true); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + throw new Error("Failed to access ClassLoader#findLoadedClass", e); + } + + try { + loadedCodeModifier = loadClass("net.minestom.server.extras.selfmodification.CodeModifier"); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + throw new Error("Failed to access CodeModifier class."); + } + + asmClassLoader = newChild(new URL[0]); } private static URL[] loadURLs() { @@ -48,17 +94,27 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { try { Class systemClass = ClassLoader.getPlatformClassLoader().loadClass(name); + log.trace("System class: "+systemClass); return systemClass; } catch (ClassNotFoundException e) { try { + // check if parent already loaded the class + Class loadedByParent = (Class) findParentLoadedClass.invoke(getParent(), name); + if(loadedByParent != null) { + log.trace("Already found in parent: "+loadedByParent); + return super.loadClass(name, resolve); + } + + if(isProtected(name)) { + log.trace("Protected: "+name); + return super.loadClass(name, resolve); + } + String path = name.replace(".", "/") + ".class"; byte[] bytes = getResourceAsStream(path).readAllBytes(); - Class defined = defineClass(name, bytes, 0, bytes.length); - if(resolve) { - resolveClass(defined); - } - return defined; - } catch (Exception ioException) { + return transformAndLoad(name, bytes, 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); @@ -66,7 +122,63 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { } } - public void loadModifier(File originFile, String codeModifierClass) { - throw new UnsupportedOperationException("TODO"); + private boolean isProtected(String name) { + return protectedClasses.contains(name); + } + + private Class transformAndLoad(String name, byte[] bytes, boolean resolve) throws ClassNotFoundException { + 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(); + } + Class defined = defineClass(name, bytes, 0, bytes.length); + log.trace("Loaded with code modifiers: "+name); + if(resolve) { + resolveClass(defined); + } + return defined; + } + + @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); + modifiers.add(modifier); + } + } + } catch (MalformedURLException | ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) { + e.printStackTrace(); + } } } 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/TestExtensionLauncher.java b/src/test/java/testextension/TestExtensionLauncher.java new file mode 100644 index 000000000..a7529725a --- /dev/null +++ b/src/test/java/testextension/TestExtensionLauncher.java @@ -0,0 +1,13 @@ +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 TestExtensionLauncher { + + 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..fccc05940 --- /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 source 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/resources/extension.json b/src/test/resources/extension.json new file mode 100644 index 000000000..c464737a0 --- /dev/null +++ b/src/test/resources/extension.json @@ -0,0 +1,7 @@ +{ + "entrypoint": "testextension.TestExtension", + "name": "Test extension", + "codeModifiers": [ + "testextension.TestModifier" + ] +} \ No newline at end of file From 26b8ad125e7dc441e30cba474ba6a2e5bc06f71a Mon Sep 17 00:00:00 2001 From: jglrxavpok Date: Sun, 23 Aug 2020 22:38:27 +0200 Subject: [PATCH 3/5] Very basic Mixin support --- build.gradle | 13 +- gradle.properties | 2 + .../java/net/minestom/server/Bootstrap.java | 30 ++++- .../MinestomOverwriteClassLoader.java | 123 +++++++++++++----- .../mixins/GlobalPropertyServiceMinestom.java | 68 ++++++++++ .../mixins/MinestomBytecodeProvider.java | 38 ++++++ .../mixins/MinestomClassProvider.java | 34 +++++ .../mixins/MinestomTransformerProvider.java | 75 +++++++++++ .../mixins/MixinCodeModifier.java | 59 +++++++++ .../mixins/MixinPlatformAgentMinestom.java | 29 +++++ .../mixins/MixinServiceMinestom.java | 86 ++++++++++++ .../mixins/MixinServiceMinestomBootstrap.java | 20 +++ ...powered.asm.service.IGlobalPropertyService | 1 + ...rg.spongepowered.asm.service.IMixinService | 1 + ...powered.asm.service.IMixinServiceBootstrap | 1 + src/main/resources/log4j2.xml | 7 +- .../testextension/TestExtensionLauncher.java | 4 + .../TestExtensionLauncherArgs.java | 17 +++ src/test/java/testextension/TestModifier.java | 2 +- .../mixins/InstanceContainerMixin.java | 18 +++ src/test/resources/mixins.testextension.json | 10 ++ 21 files changed, 599 insertions(+), 39 deletions(-) create mode 100644 gradle.properties create mode 100644 src/main/java/net/minestom/server/extras/selfmodification/mixins/GlobalPropertyServiceMinestom.java create mode 100644 src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomBytecodeProvider.java create mode 100644 src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomClassProvider.java create mode 100644 src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomTransformerProvider.java create mode 100644 src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinCodeModifier.java create mode 100644 src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinPlatformAgentMinestom.java create mode 100644 src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinServiceMinestom.java create mode 100644 src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinServiceMinestomBootstrap.java create mode 100644 src/main/resources/META-INF/services/org.spongepowered.asm.service.IGlobalPropertyService create mode 100644 src/main/resources/META-INF/services/org.spongepowered.asm.service.IMixinService create mode 100644 src/main/resources/META-INF/services/org.spongepowered.asm.service.IMixinServiceBootstrap create mode 100644 src/test/java/testextension/TestExtensionLauncherArgs.java create mode 100644 src/test/java/testextension/mixins/InstanceContainerMixin.java create mode 100644 src/test/resources/mixins.testextension.json diff --git a/build.gradle b/build.gradle index 0e5dbc815..50e28e17e 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' + } } } @@ -112,8 +116,13 @@ dependencies { annotationProcessor 'org.projectlombok:lombok:1.18.12' // Code modification - api "org.ow2.asm:asm:6.2.1" - api "org.ow2.asm:asm-tree:6.2.1" + 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.3.1' 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/net/minestom/server/Bootstrap.java b/src/main/java/net/minestom/server/Bootstrap.java index ff48514bd..0c0f20e43 100644 --- a/src/main/java/net/minestom/server/Bootstrap.java +++ b/src/main/java/net/minestom/server/Bootstrap.java @@ -1,9 +1,16 @@ package net.minestom.server; import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; +import net.minestom.server.extras.selfmodification.mixins.MixinCodeModifier; +import org.spongepowered.asm.launch.MixinBootstrap; +import org.spongepowered.asm.launch.platform.CommandLineOptions; +import org.spongepowered.asm.launch.platform.MixinPlatformManager; +import org.spongepowered.asm.mixin.MixinEnvironment; +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 @@ -12,13 +19,17 @@ public class Bootstrap { public static void bootstrap(String mainClassFullName, String[] args) { try { - ClassLoader classLoader = new MinestomOverwriteClassLoader(Bootstrap.class.getClassLoader()); + ClassLoader classLoader = MinestomOverwriteClassLoader.getInstance(); + startMixin(args); + MixinEnvironment.init(MixinEnvironment.Phase.INIT); + MinestomOverwriteClassLoader.getInstance().addCodeModifier(new MixinCodeModifier()); // ensure extensions are loaded when starting the server Class serverClass = classLoader.loadClass("net.minestom.server.MinecraftServer"); Method init = serverClass.getMethod("init"); init.invoke(null); + Class mainClass = classLoader.loadClass(mainClassFullName); Method main = mainClass.getDeclaredMethod("main", String[].class); main.invoke(null, new Object[] { args }); @@ -26,4 +37,21 @@ public class Bootstrap { e.printStackTrace(); } } + + private static void startMixin(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + 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/extras/selfmodification/MinestomOverwriteClassLoader.java b/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java index 80bded808..8690db615 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java @@ -7,11 +7,13 @@ 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.lang.reflect.Method; 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; @@ -19,14 +21,25 @@ import java.util.Set; @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 */ - private static final Set protectedClasses = Set.of( - "net.minestom.server.extras.selfmodification.CodeModifier", - "net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader" - ); + 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"); + } + }; 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 @@ -38,7 +51,7 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { private final Method findParentLoadedClass; private final Class loadedCodeModifier; - public MinestomOverwriteClassLoader(ClassLoader parent) { + private MinestomOverwriteClassLoader(ClassLoader parent) { super("Minestom ClassLoader", loadURLs(), parent); try { findParentLoadedClass = ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class); @@ -58,6 +71,17 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { 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[] loadURLs() { String classpath = System.getProperty("java.class.path"); String[] parts = classpath.split(";"); @@ -110,9 +134,7 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { return super.loadClass(name, resolve); } - String path = name.replace(".", "/") + ".class"; - byte[] bytes = getResourceAsStream(path).readAllBytes(); - return transformAndLoad(name, bytes, 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 @@ -123,32 +145,17 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { } private boolean isProtected(String name) { - return protectedClasses.contains(name); + if(!protectedClasses.contains(name)) { + for(String start : protectedPackages) { + if(name.startsWith(start)) + return true; + } + return false; + } + return true; } - private Class transformAndLoad(String name, byte[] bytes, boolean resolve) throws ClassNotFoundException { - 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(); - } + 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) { @@ -157,6 +164,46 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { return defined; } + public byte[] loadBytes(String name, boolean transform) throws IOException { + 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; + } + + @Override + public Class findClass(String name) throws ClassNotFoundException { + return super.findClass(name); + } + + public void resolve(Class clazz) { + resolveClass(clazz); + } + @NotNull public URLClassLoader newChild(@NotNull URL[] urls) { return URLClassLoader.newInstance(urls, this); @@ -174,11 +221,21 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { CodeModifier modifier = (CodeModifier) modifierClass.getDeclaredConstructor().newInstance(); synchronized (modifiers) { log.warn("Added Code modifier: "+modifier); - modifiers.add(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..a33cc14b8 --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/GlobalPropertyServiceMinestom.java @@ -0,0 +1,68 @@ +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; + +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..e303a6bcc --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomBytecodeProvider.java @@ -0,0 +1,38 @@ +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; + +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..7ee9ce2c1 --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomClassProvider.java @@ -0,0 +1,34 @@ +package net.minestom.server.extras.selfmodification.mixins; + +import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; +import org.spongepowered.asm.service.IClassProvider; + +import java.net.URL; + +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/MinestomTransformerProvider.java b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomTransformerProvider.java new file mode 100644 index 000000000..43bbac597 --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomTransformerProvider.java @@ -0,0 +1,75 @@ +package net.minestom.server.extras.selfmodification.mixins; + +import net.minestom.server.extras.selfmodification.CodeModifier; +import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.service.ITransformer; +import org.spongepowered.asm.service.ITransformerProvider; +import org.spongepowered.asm.service.ITreeClassTransformer; + +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +public class MinestomTransformerProvider implements ITransformerProvider { + private final MinestomOverwriteClassLoader classLoader; + private List transformers; + + public MinestomTransformerProvider(MinestomOverwriteClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public void addTransformerExclusion(String name) { + classLoader.protectedClasses.add(name); + } + + @Override + public Collection getTransformers() { + return getDelegatedTransformers(); + } + + @Override + public Collection getDelegatedTransformers() { + if(transformers == null) { + transformers = buildTransformerList(); + } + return transformers; + } + + private List buildTransformerList() { + List result = new LinkedList<>(); + for(CodeModifier modifier : classLoader.getModifiers()) { + result.add(toMixin(modifier)); + } + + try { + Class clazz = classLoader.loadClass("org.spongepowered.asm.mixin.transformer.MixingTransformer"); + ITransformer mixinTransformer = (ITransformer) clazz.getDeclaredConstructor().newInstance(); + result.add(mixinTransformer); + } catch (ClassNotFoundException | InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException e) { + e.printStackTrace(); + } + return result; + } + + private ITransformer toMixin(CodeModifier modifier) { + return new ITreeClassTransformer() { + @Override + public boolean transformClassNode(String name, String transformedName, ClassNode classNode) { + return modifier.transform(classNode); + } + + @Override + public String getName() { + return modifier.getClass().getName(); + } + + @Override + public boolean isDelegationExcluded() { + return false; + } + }; + } +} 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..7c5dc4123 --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinCodeModifier.java @@ -0,0 +1,59 @@ +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.mixin.transformer.MixinProcessor; +import org.spongepowered.asm.mixin.transformer.ext.Extensions; +import org.spongepowered.asm.mixin.transformer.ext.IHotSwap; +import org.spongepowered.asm.service.ISyntheticClassInfo; +import org.spongepowered.asm.service.ISyntheticClassRegistry; +import org.spongepowered.asm.transformers.TreeTransformer; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class MixinCodeModifier extends CodeModifier { + + private Method transformClassMethod; + private TreeTransformer processor; + + public MixinCodeModifier() { + try { + Class mixinTransformerClass = Class.forName("org.spongepowered.asm.mixin.transformer.MixinTransformer"); + Constructor ctor = mixinTransformerClass.getDeclaredConstructor(); + ctor.setAccessible(true); + this.processor = (TreeTransformer) ctor.newInstance(); + transformClassMethod = mixinTransformerClass.getDeclaredMethod("transformClass", MixinEnvironment.class, String.class, ClassNode.class); + transformClassMethod.setAccessible(true); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + + @Override + public boolean transform(ClassNode source) { + try { + return (boolean) transformClassMethod.invoke(processor, MixinEnvironment.getEnvironment(MixinEnvironment.Phase.DEFAULT), source.name.replace("/", "."), source); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (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..878aac9e8 --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinServiceMinestom.java @@ -0,0 +1,86 @@ +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 MinestomTransformerProvider transformerProvider; + + public MixinServiceMinestom() { + this.classLoader = MinestomOverwriteClassLoader.getInstance(); + classProvider = new MinestomClassProvider(classLoader); + bytecodeProvider = new MinestomBytecodeProvider(classLoader); + transformerProvider = new MinestomTransformerProvider(classLoader); + } + + @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 transformerProvider; + } + + @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); + } + + // TODO: everything below + + @Override + public IClassTracker getClassTracker() { + return null; + } + + @Override + public IMixinAuditTrail getAuditTrail() { + return null; + } + + @Override + public void wire(MixinEnvironment.Phase phase, IConsumer phaseConsumer) { + super.wire(phase, phaseConsumer); + phaseConsumer.accept(MixinEnvironment.Phase.PREINIT); + phaseConsumer.accept(MixinEnvironment.Phase.INIT); + } +} 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/TestExtensionLauncher.java b/src/test/java/testextension/TestExtensionLauncher.java index a7529725a..6479b71af 100644 --- a/src/test/java/testextension/TestExtensionLauncher.java +++ b/src/test/java/testextension/TestExtensionLauncher.java @@ -1,12 +1,16 @@ 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 TestExtensionLauncher { public static void main(String[] args) { + MixinBootstrap.init(); + Mixins.addConfiguration("mixins.testextension.json"); Bootstrap.bootstrap("fr.themode.demo.MainDemo", args); } 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/TestModifier.java b/src/test/java/testextension/TestModifier.java index fccc05940..602ba13f6 100644 --- a/src/test/java/testextension/TestModifier.java +++ b/src/test/java/testextension/TestModifier.java @@ -10,7 +10,7 @@ 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 source of "+source.name); + System.out.println("Modifying code of "+source.name); MethodNode constructor = findConstructor(source.methods); constructor.instructions.insert(constructor.instructions.getFirst(), buildInjectionCode()); return true; diff --git a/src/test/java/testextension/mixins/InstanceContainerMixin.java b/src/test/java/testextension/mixins/InstanceContainerMixin.java new file mode 100644 index 000000000..972ac6c51 --- /dev/null +++ b/src/test/java/testextension/mixins/InstanceContainerMixin.java @@ -0,0 +1,18 @@ +package testextension.mixins; + +import net.minestom.server.instance.InstanceContainer; +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.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/mixins.testextension.json b/src/test/resources/mixins.testextension.json new file mode 100644 index 000000000..42df826a5 --- /dev/null +++ b/src/test/resources/mixins.testextension.json @@ -0,0 +1,10 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "testextension.mixins", + "target": "@env(DEFAULT)", + "compatibilityLevel": "JAVA_11", + "mixins": [ + "InstanceContainerMixin" + ] +} \ No newline at end of file From c3c2b0a34c03f5368852ebee4fa90f48098ef38c Mon Sep 17 00:00:00 2001 From: jglrxavpok Date: Sun, 23 Aug 2020 23:27:53 +0200 Subject: [PATCH 4/5] Loading mixins from extensions --- .../java/net/minestom/server/Bootstrap.java | 3 +- .../server/extensions/ExtensionManager.java | 7 +- .../MinestomOverwriteClassLoader.java | 65 +++++++--------- .../mixins/GlobalPropertyServiceMinestom.java | 3 + .../mixins/MinestomBytecodeProvider.java | 3 + .../mixins/MinestomClassProvider.java | 3 + .../mixins/MinestomTransformerProvider.java | 75 ------------------- .../mixins/MixinAuditTrailMinestom.java | 25 +++++++ .../mixins/MixinCodeModifier.java | 38 ++++------ .../mixins/MixinServiceMinestom.java | 11 ++- ...java => TestExtensionLauncherNoSetup.java} | 4 +- .../mixins/InstanceContainerMixin.java | 1 - src/test/resources/extension.json | 3 +- 13 files changed, 91 insertions(+), 150 deletions(-) delete mode 100644 src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomTransformerProvider.java create mode 100644 src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinAuditTrailMinestom.java rename src/test/java/testextension/{TestExtensionLauncher.java => TestExtensionLauncherNoSetup.java} (77%) diff --git a/src/main/java/net/minestom/server/Bootstrap.java b/src/main/java/net/minestom/server/Bootstrap.java index 0c0f20e43..ec1f5139c 100644 --- a/src/main/java/net/minestom/server/Bootstrap.java +++ b/src/main/java/net/minestom/server/Bootstrap.java @@ -21,15 +21,14 @@ public class Bootstrap { try { ClassLoader classLoader = MinestomOverwriteClassLoader.getInstance(); startMixin(args); - MixinEnvironment.init(MixinEnvironment.Phase.INIT); MinestomOverwriteClassLoader.getInstance().addCodeModifier(new MixinCodeModifier()); + MixinEnvironment.init(MixinEnvironment.Phase.DEFAULT); // ensure extensions are loaded when starting the server Class serverClass = classLoader.loadClass("net.minestom.server.MinecraftServer"); Method init = serverClass.getMethod("init"); init.invoke(null); - Class mainClass = classLoader.loadClass(mainClassFullName); Method main = mainClass.getDeclaredMethod("main", String[].class); main.invoke(null, new Object[] { args }); diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index 33572a0c1..80dd06717 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -6,6 +6,7 @@ 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.*; import java.lang.reflect.Constructor; @@ -232,7 +233,11 @@ public class ExtensionManager { modifiableClassLoader.loadModifier(extension.files, elem.getAsString()); } } - // TODO: special support for mixins + 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); diff --git a/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java b/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java index 8690db615..721f7c30c 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java @@ -9,7 +9,6 @@ import org.objectweb.asm.tree.ClassNode; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; @@ -18,6 +17,9 @@ 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 { @@ -38,8 +40,16 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { 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 @@ -48,26 +58,9 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { // TODO: priorities? private List modifiers = new LinkedList<>(); - private final Method findParentLoadedClass; - private final Class loadedCodeModifier; private MinestomOverwriteClassLoader(ClassLoader parent) { - super("Minestom ClassLoader", loadURLs(), parent); - try { - findParentLoadedClass = ClassLoader.class.getDeclaredMethod("findLoadedClass", String.class); - findParentLoadedClass.setAccessible(true); - } catch (NoSuchMethodException e) { - e.printStackTrace(); - throw new Error("Failed to access ClassLoader#findLoadedClass", e); - } - - try { - loadedCodeModifier = loadClass("net.minestom.server.extras.selfmodification.CodeModifier"); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - throw new Error("Failed to access CodeModifier class."); - } - + super("Minestom ClassLoader", extractURLsFromClasspath(), parent); asmClassLoader = newChild(new URL[0]); } @@ -82,7 +75,7 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { return INSTANCE; } - private static URL[] loadURLs() { + private static URL[] extractURLsFromClasspath() { String classpath = System.getProperty("java.class.path"); String[] parts = classpath.split(";"); URL[] urls = new URL[parts.length]; @@ -103,13 +96,6 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { return urls; } - private static URL[] fromParent(ClassLoader parent) { - if(parent instanceof URLClassLoader) { - return ((URLClassLoader) parent).getURLs(); - } - return new URL[0]; - } - @Override public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { Class loadedClass = findLoadedClass(name); @@ -117,18 +103,12 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { 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 { - // check if parent already loaded the class - Class loadedByParent = (Class) findParentLoadedClass.invoke(getParent(), name); - if(loadedByParent != null) { - log.trace("Already found in parent: "+loadedByParent); - return super.loadClass(name, resolve); - } - if(isProtected(name)) { log.trace("Protected: "+name); return super.loadClass(name, resolve); @@ -164,7 +144,17 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { return defined; } - public byte[] loadBytes(String name, boolean transform) throws IOException { + /** + * 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)) { @@ -195,15 +185,12 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { return bytes; } + // overriden to increase access (from protected to public) @Override public Class findClass(String name) throws ClassNotFoundException { return super.findClass(name); } - public void resolve(Class clazz) { - resolveClass(clazz); - } - @NotNull public URLClassLoader newChild(@NotNull URL[] urls) { return URLClassLoader.newInstance(urls, this); 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 index a33cc14b8..e7f5df0f3 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/mixins/GlobalPropertyServiceMinestom.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/GlobalPropertyServiceMinestom.java @@ -7,6 +7,9 @@ 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 { 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 index e303a6bcc..c8b3f20cb 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomBytecodeProvider.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomBytecodeProvider.java @@ -7,6 +7,9 @@ import org.spongepowered.asm.service.IClassBytecodeProvider; import java.io.IOException; +/** + * Provides class bytecode for Mixin + */ public class MinestomBytecodeProvider implements IClassBytecodeProvider { private final MinestomOverwriteClassLoader classLoader; 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 index 7ee9ce2c1..ae3b98b4f 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomClassProvider.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomClassProvider.java @@ -5,6 +5,9 @@ import org.spongepowered.asm.service.IClassProvider; import java.net.URL; +/** + * Provides classes for Mixin + */ public class MinestomClassProvider implements IClassProvider { private final MinestomOverwriteClassLoader classLoader; diff --git a/src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomTransformerProvider.java b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomTransformerProvider.java deleted file mode 100644 index 43bbac597..000000000 --- a/src/main/java/net/minestom/server/extras/selfmodification/mixins/MinestomTransformerProvider.java +++ /dev/null @@ -1,75 +0,0 @@ -package net.minestom.server.extras.selfmodification.mixins; - -import net.minestom.server.extras.selfmodification.CodeModifier; -import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; -import org.objectweb.asm.tree.ClassNode; -import org.spongepowered.asm.service.ITransformer; -import org.spongepowered.asm.service.ITransformerProvider; -import org.spongepowered.asm.service.ITreeClassTransformer; - -import java.lang.reflect.InvocationTargetException; -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; - -public class MinestomTransformerProvider implements ITransformerProvider { - private final MinestomOverwriteClassLoader classLoader; - private List transformers; - - public MinestomTransformerProvider(MinestomOverwriteClassLoader classLoader) { - this.classLoader = classLoader; - } - - @Override - public void addTransformerExclusion(String name) { - classLoader.protectedClasses.add(name); - } - - @Override - public Collection getTransformers() { - return getDelegatedTransformers(); - } - - @Override - public Collection getDelegatedTransformers() { - if(transformers == null) { - transformers = buildTransformerList(); - } - return transformers; - } - - private List buildTransformerList() { - List result = new LinkedList<>(); - for(CodeModifier modifier : classLoader.getModifiers()) { - result.add(toMixin(modifier)); - } - - try { - Class clazz = classLoader.loadClass("org.spongepowered.asm.mixin.transformer.MixingTransformer"); - ITransformer mixinTransformer = (ITransformer) clazz.getDeclaredConstructor().newInstance(); - result.add(mixinTransformer); - } catch (ClassNotFoundException | InstantiationException | InvocationTargetException | NoSuchMethodException | IllegalAccessException e) { - e.printStackTrace(); - } - return result; - } - - private ITransformer toMixin(CodeModifier modifier) { - return new ITreeClassTransformer() { - @Override - public boolean transformClassNode(String name, String transformedName, ClassNode classNode) { - return modifier.transform(classNode); - } - - @Override - public String getName() { - return modifier.getClass().getName(); - } - - @Override - public boolean isDelegationExcluded() { - return false; - } - }; - } -} 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 index 7c5dc4123..c52e51713 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinCodeModifier.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinCodeModifier.java @@ -3,50 +3,44 @@ 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.mixin.transformer.MixinProcessor; -import org.spongepowered.asm.mixin.transformer.ext.Extensions; -import org.spongepowered.asm.mixin.transformer.ext.IHotSwap; -import org.spongepowered.asm.service.ISyntheticClassInfo; -import org.spongepowered.asm.service.ISyntheticClassRegistry; 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 { - private Method transformClassMethod; - private TreeTransformer processor; + /** + * 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.processor = (TreeTransformer) ctor.newInstance(); + 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 e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (InstantiationException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } catch (ClassNotFoundException e) { - e.printStackTrace(); + } 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(processor, MixinEnvironment.getEnvironment(MixinEnvironment.Phase.DEFAULT), source.name.replace("/", "."), source); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { + return (boolean) transformClassMethod.invoke(transformer, MixinEnvironment.getEnvironment(MixinEnvironment.Phase.DEFAULT), source.name.replace("/", "."), source); + } catch (IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } return false; 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 index 878aac9e8..a3c9a2cc6 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinServiceMinestom.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinServiceMinestom.java @@ -16,13 +16,13 @@ public class MixinServiceMinestom extends MixinServiceAbstract { private final MinestomOverwriteClassLoader classLoader; private final MinestomClassProvider classProvider; private final MinestomBytecodeProvider bytecodeProvider; - private final MinestomTransformerProvider transformerProvider; + private final MixinAuditTrailMinestom auditTrail; public MixinServiceMinestom() { this.classLoader = MinestomOverwriteClassLoader.getInstance(); classProvider = new MinestomClassProvider(classLoader); bytecodeProvider = new MinestomBytecodeProvider(classLoader); - transformerProvider = new MinestomTransformerProvider(classLoader); + auditTrail = new MixinAuditTrailMinestom(); } @Override @@ -47,7 +47,7 @@ public class MixinServiceMinestom extends MixinServiceAbstract { @Override public ITransformerProvider getTransformerProvider() { - return transformerProvider; + return null; } @Override @@ -65,8 +65,6 @@ public class MixinServiceMinestom extends MixinServiceAbstract { return classLoader.getResourceAsStream(name); } - // TODO: everything below - @Override public IClassTracker getClassTracker() { return null; @@ -74,12 +72,13 @@ public class MixinServiceMinestom extends MixinServiceAbstract { @Override public IMixinAuditTrail getAuditTrail() { - return null; + return auditTrail; } @Override public void wire(MixinEnvironment.Phase phase, IConsumer phaseConsumer) { super.wire(phase, phaseConsumer); + // TODO: hook into Minestom initialization process phaseConsumer.accept(MixinEnvironment.Phase.PREINIT); phaseConsumer.accept(MixinEnvironment.Phase.INIT); } diff --git a/src/test/java/testextension/TestExtensionLauncher.java b/src/test/java/testextension/TestExtensionLauncherNoSetup.java similarity index 77% rename from src/test/java/testextension/TestExtensionLauncher.java rename to src/test/java/testextension/TestExtensionLauncherNoSetup.java index 6479b71af..a3e358579 100644 --- a/src/test/java/testextension/TestExtensionLauncher.java +++ b/src/test/java/testextension/TestExtensionLauncherNoSetup.java @@ -6,11 +6,9 @@ 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 TestExtensionLauncher { +public class TestExtensionLauncherNoSetup { public static void main(String[] args) { - MixinBootstrap.init(); - Mixins.addConfiguration("mixins.testextension.json"); Bootstrap.bootstrap("fr.themode.demo.MainDemo", args); } diff --git a/src/test/java/testextension/mixins/InstanceContainerMixin.java b/src/test/java/testextension/mixins/InstanceContainerMixin.java index 972ac6c51..7bed56a2c 100644 --- a/src/test/java/testextension/mixins/InstanceContainerMixin.java +++ b/src/test/java/testextension/mixins/InstanceContainerMixin.java @@ -14,5 +14,4 @@ public class InstanceContainerMixin { System.out.println("Hello from Mixin!!!"); } - } diff --git a/src/test/resources/extension.json b/src/test/resources/extension.json index c464737a0..c7d404173 100644 --- a/src/test/resources/extension.json +++ b/src/test/resources/extension.json @@ -3,5 +3,6 @@ "name": "Test extension", "codeModifiers": [ "testextension.TestModifier" - ] + ], + "mixinConfig": "mixins.testextension.json" } \ No newline at end of file From 21b48fe2914e0996054cfc90e30a14a187c4eb8c Mon Sep 17 00:00:00 2001 From: jglrxavpok Date: Tue, 25 Aug 2020 15:44:17 +0200 Subject: [PATCH 5/5] Wired Mixin phases with Minestom initialization --- .../java/net/minestom/server/Bootstrap.java | 15 +++++----- .../mixins/MixinServiceMinestom.java | 29 +++++++++++++++++-- .../mixins/DynamicChunkMixin.java | 18 ++++++++++++ .../mixins/InstanceContainerMixin.java | 4 +++ src/test/resources/mixins.testextension.json | 3 +- 5 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 src/test/java/testextension/mixins/DynamicChunkMixin.java diff --git a/src/main/java/net/minestom/server/Bootstrap.java b/src/main/java/net/minestom/server/Bootstrap.java index ec1f5139c..4fa25f61a 100644 --- a/src/main/java/net/minestom/server/Bootstrap.java +++ b/src/main/java/net/minestom/server/Bootstrap.java @@ -2,10 +2,9 @@ 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.launch.platform.MixinPlatformManager; -import org.spongepowered.asm.mixin.MixinEnvironment; import org.spongepowered.asm.mixin.Mixins; import java.lang.reflect.InvocationTargetException; @@ -15,19 +14,22 @@ import java.util.Arrays; /** * Used to launch Minestom with the {@link MinestomOverwriteClassLoader} to allow for self-modifications */ -public class Bootstrap { +public final class Bootstrap { public static void bootstrap(String mainClassFullName, String[] args) { try { ClassLoader classLoader = MinestomOverwriteClassLoader.getInstance(); startMixin(args); MinestomOverwriteClassLoader.getInstance().addCodeModifier(new MixinCodeModifier()); - MixinEnvironment.init(MixinEnvironment.Phase.DEFAULT); + 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); @@ -38,6 +40,7 @@ public class Bootstrap { } 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)) ) { @@ -49,8 +52,6 @@ public class Bootstrap { doInit.invoke(null, CommandLineOptions.ofArgs(Arrays.asList(args))); MixinBootstrap.getPlatform().inject(); - Mixins.getConfigs().forEach(c -> { - MinestomOverwriteClassLoader.getInstance().protectedPackages.add(c.getConfig().getMixinPackage()); - }); + Mixins.getConfigs().forEach(c -> MinestomOverwriteClassLoader.getInstance().protectedPackages.add(c.getConfig().getMixinPackage())); } } 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 index a3c9a2cc6..ef4c4c760 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinServiceMinestom.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/mixins/MixinServiceMinestom.java @@ -17,8 +17,11 @@ public class MixinServiceMinestom extends MixinServiceAbstract { 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); @@ -78,8 +81,28 @@ public class MixinServiceMinestom extends MixinServiceAbstract { @Override public void wire(MixinEnvironment.Phase phase, IConsumer phaseConsumer) { super.wire(phase, phaseConsumer); - // TODO: hook into Minestom initialization process - phaseConsumer.accept(MixinEnvironment.Phase.PREINIT); - phaseConsumer.accept(MixinEnvironment.Phase.INIT); + 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/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 index 7bed56a2c..b3299afbd 100644 --- a/src/test/java/testextension/mixins/InstanceContainerMixin.java +++ b/src/test/java/testextension/mixins/InstanceContainerMixin.java @@ -1,9 +1,13 @@ 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) diff --git a/src/test/resources/mixins.testextension.json b/src/test/resources/mixins.testextension.json index 42df826a5..6fd90870c 100644 --- a/src/test/resources/mixins.testextension.json +++ b/src/test/resources/mixins.testextension.json @@ -5,6 +5,7 @@ "target": "@env(DEFAULT)", "compatibilityLevel": "JAVA_11", "mixins": [ - "InstanceContainerMixin" + "InstanceContainerMixin", + "DynamicChunkMixin" ] } \ No newline at end of file