(WIP) Support for runtime code modification

This commit is contained in:
jglrxavpok 2020-08-20 02:06:58 +02:00
parent b8c30d9b58
commit 164719090a
5 changed files with 175 additions and 15 deletions

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

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

View File

@ -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<DiscoveredExtension> 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<DiscoveredExtension> discoverExtensions() {
Gson gson = new Gson();
List<DiscoveredExtension> 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<URL, URLClassLoader> getExtensionLoaders() {
return new HashMap<>(extensionLoaders);
}
/**
* Extensions are allowed to apply Mixin transformers, the magic happens here
*/
private void setupCodeModifiers(List<DiscoveredExtension> 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;
}
}

View File

@ -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");
}
}