From 535e8946b60cc1d3bc140a4dbea210aa45857a74 Mon Sep 17 00:00:00 2001 From: jglrxavpok Date: Sat, 24 Oct 2020 22:57:38 +0200 Subject: [PATCH 01/13] Extensions can declare Maven dependencies which will be auto-downloaded --- .gitignore | 2 +- build.gradle | 4 + .../extensions/DiscoveredExtension.java | 120 +++++++++++++++ .../server/extensions/ExtensionManager.java | 145 +++++++++++------- .../MinestomOverwriteClassLoader.java | 6 + .../TestExtensionLauncherArgs.java | 2 +- .../TestExtensionLauncherNoSetup.java | 2 +- .../mixins/DynamicChunkMixin.java | 2 +- src/test/resources/extension.json | 2 +- 9 files changed, 223 insertions(+), 62 deletions(-) create mode 100644 src/main/java/net/minestom/server/extensions/DiscoveredExtension.java diff --git a/.gitignore b/.gitignore index 3e3e2abbb..b1a8308b6 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,4 @@ gradle-app.setting /src/main/java/com/mcecraft/ # When running the demo we generate the extensions folder -extensions/ +/extensions/ diff --git a/build.gradle b/build.gradle index 16df9c413..57ffd461d 100644 --- a/build.gradle +++ b/build.gradle @@ -141,6 +141,8 @@ dependencies { api 'com.github.MadMartian:hydrazine-path-finding:1.4.2' api "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + + // NBT parsing/manipulation/saving api("com.github.jglrxavpok:Hephaistos:${project.hephaistos_version}") api("com.github.jglrxavpok:Hephaistos:${project.hephaistos_version}:gson") api("com.github.jglrxavpok:Hephaistos:${project.hephaistos_version}") { @@ -149,6 +151,8 @@ dependencies { } } + implementation "com.github.Minestom:DependencyGetter:v1.0.1" + // LWJGL, for map rendering lwjglApi platform("org.lwjgl:lwjgl-bom:$lwjglVersion") diff --git a/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java b/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java new file mode 100644 index 000000000..094345266 --- /dev/null +++ b/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java @@ -0,0 +1,120 @@ +package net.minestom.server.extensions; + +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; + +@Slf4j +class DiscoveredExtension { + private static String NAME_REGEX = "[A-Za-z][_A-Za-z0-9]+"; + + enum LoadStatus { + LOAD_SUCCESS("Actually, it did not fail. This message should not have been printed."), + MISSING_DEPENDENCIES("Missing dependencies, check your logs."), + INVALID_NAME("Invalid name."), + NO_ENTRYPOINT("No entrypoint specified."), + ; + + private final String message; + + LoadStatus(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + } + + static class Dependencies { + static class Repository { + String name; + String url; + } + + Repository[] repositories; + String[] artifacts; + } + + transient File[] files = new File[0]; + transient LoadStatus loadStatus = LoadStatus.LOAD_SUCCESS; + + private String[] codeModifiers; + private String[] authors; + private String mixinConfig; + private String name; + private String version; + private String entrypoint; + private Dependencies dependencies; + + void checkIntegrity() { + if(name == null) { + StringBuilder fileList = new StringBuilder(); + for(File f : files) { + fileList.append(f.getAbsolutePath()).append(", "); + } + log.error("Extension with no name. (at {}})", fileList); + log.error("Extension at ({}) will not be loaded.", fileList); + loadStatus = LoadStatus.INVALID_NAME; + return; + } + if(!name.matches(NAME_REGEX)) { + log.error("Extension '{}' specified an invalid name.", name); + log.error("Extension '{}' will not be loaded.", name); + loadStatus = LoadStatus.INVALID_NAME; + return; + } + if(entrypoint == null) { + log.error("Extension '{}' did not specify an entry point (via 'entrypoint').", name); + log.error("Extension '{}' will not be loaded.", name); + loadStatus = LoadStatus.NO_ENTRYPOINT; + return; + } + if(codeModifiers == null) { + codeModifiers = new String[0]; + } + } + + @NotNull + public String getName() { + if(name == null) { + throw new IllegalStateException("Missing extension name"); + } + return name; + } + + @NotNull + public String[] getCodeModifiers() { + if(codeModifiers == null) { + codeModifiers = new String[0]; + } + return codeModifiers; + } + + @Nullable + public String getMixinConfig() { + return mixinConfig; + } + + @Nullable + public String[] getAuthors() { + return authors; + } + + @Nullable + public String getVersion() { + return version; + } + + @NotNull + public String getEntrypoint() { + return entrypoint; + } + + @Nullable + public Dependencies getDependencies() { + return dependencies; + } +} diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index 699622633..364c969e8 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -1,11 +1,9 @@ package net.minestom.server.extensions; import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; +import net.minestom.dependencies.DependencyGetter; +import net.minestom.dependencies.maven.MavenRepository; import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; @@ -16,20 +14,15 @@ import org.spongepowered.asm.mixin.Mixins; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Constructor; import java.lang.reflect.Field; 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.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.zip.ZipFile; @Slf4j @@ -42,6 +35,7 @@ public class ExtensionManager { private final Map extensionLoaders = new HashMap<>(); private final Map extensions = new HashMap<>(); private final File extensionFolder = new File("extensions"); + private final File dependenciesFolder = new File(extensionFolder, ".libs"); private boolean loaded; public ExtensionManager() { @@ -58,7 +52,17 @@ public class ExtensionManager { } } + if (!dependenciesFolder.exists()) { + if (!dependenciesFolder.mkdirs()) { + log.error("Could not find nor create the extension dependencies folder, extensions will not be loaded!"); + return; + } + } + final List discoveredExtensions = discoverExtensions(); + loadDependencies(discoveredExtensions); + // remove invalid extensions + discoveredExtensions.removeIf(ext -> ext.loadStatus != DiscoveredExtension.LoadStatus.LOAD_SUCCESS); setupCodeModifiers(discoveredExtensions); for (DiscoveredExtension discoveredExtension : discoveredExtensions) { @@ -73,47 +77,25 @@ public class ExtensionManager { log.error("Failed to get URL.", e); continue; } - // TODO: Can't we use discoveredExtension.description here? Someone should test that. - final InputStream extensionInputStream = loader.getResourceAsStream("extension.json"); - if (extensionInputStream == null) { - 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("Failed to find extension.json in the urls '{}'.", urlsString); - continue; - } - JsonObject extensionDescriptionJson = JsonParser.parseReader(new InputStreamReader(extensionInputStream)).getAsJsonObject(); - - final String mainClass = extensionDescriptionJson.get("entrypoint").getAsString(); - final String extensionName = extensionDescriptionJson.get("name").getAsString(); - // Check the validity of the extension's name. - if (!extensionName.matches("[A-Za-z]+")) { - log.error("Extension '{}' specified an invalid name.", extensionName); - log.error("Extension '{}' will not be loaded.", extensionName); - continue; - } // Get ExtensionDescription (authors, version etc.) + String extensionName = discoveredExtension.getName(); + String mainClass = discoveredExtension.getEntrypoint(); Extension.ExtensionDescription extensionDescription; { String version; - if (!extensionDescriptionJson.has("version")) { + if (discoveredExtension.getVersion() == null) { log.warn("Extension '{}' did not specify a version.", extensionName); log.warn("Extension '{}' will continue to load but should specify a plugin version.", extensionName); version = "Not Specified"; } else { - version = extensionDescriptionJson.get("version").getAsString(); + version = discoveredExtension.getVersion(); } List authors; - if (!extensionDescriptionJson.has("authors")) { + if (discoveredExtension.getAuthors() == null) { authors = new ArrayList<>(); } else { - authors = Arrays.asList(new Gson().fromJson(extensionDescriptionJson.get("authors"), String[].class)); + authors = Arrays.asList(discoveredExtension.getAuthors()); } extensionDescription = new Extension.ExtensionDescription(extensionName, version, authors); @@ -198,6 +180,57 @@ public class ExtensionManager { } } + private void loadDependencies(List extensions) { + for(DiscoveredExtension ext : extensions) { + try { + DependencyGetter getter = new DependencyGetter(); + DiscoveredExtension.Dependencies dependencies = ext.getDependencies(); + if(dependencies.repositories == null) { + throw new IllegalStateException("Missing 'repositories' array."); + } + if(dependencies.artifacts == null) { + throw new IllegalStateException("Missing 'artifacts' array."); + } + List repoList = new LinkedList<>(); + for(var repository : dependencies.repositories) { + if(repository.name == null) { + throw new IllegalStateException("Missing 'name' element in repository object."); + } + if(repository.url == null) { + throw new IllegalStateException("Missing 'url' element in repository object."); + } + repoList.add(new MavenRepository(repository.name, repository.url)); + } + getter.addMavenResolver(repoList); + + for(var artifact : dependencies.artifacts) { + var resolved = getter.get(artifact, dependenciesFolder); + injectIntoClasspath(resolved.getContentsLocation(), ext); + log.trace("Dependency of extension {}: {}", ext.getName(), resolved); + } + } catch (Exception e) { + ext.loadStatus = DiscoveredExtension.LoadStatus.MISSING_DEPENDENCIES; + log.error("Failed to load dependencies for extension {}", ext.getName()); + log.error("Extension '{}' will not be loaded", ext.getName()); + log.error("This is the exception", e); + } + } + } + + private void injectIntoClasspath(URL dependency, DiscoveredExtension extension) { + final ClassLoader cl = getClass().getClassLoader(); + if (!(cl instanceof URLClassLoader)) { + throw new IllegalStateException("Current class loader is not a URLClassLoader, but " + cl + ". This prevents adding URLs into the classpath at runtime."); + } + try { + Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + addURL.setAccessible(true); + addURL.invoke(cl, dependency); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException("Failed to inject URL "+dependency+" into classpath. From extension "+extension.getName(), e); + } + } + @NotNull private List discoverExtensions() { List extensions = new LinkedList<>(); @@ -211,10 +244,12 @@ public class ExtensionManager { try (ZipFile f = new ZipFile(file); InputStreamReader reader = new InputStreamReader(f.getInputStream(f.getEntry("extension.json")))) { - DiscoveredExtension extension = new DiscoveredExtension(); + DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class); extension.files = new File[]{file}; - extension.description = GSON.fromJson(reader, JsonObject.class); - extensions.add(extension); + extension.checkIntegrity(); + if(extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) { + extensions.add(extension); + } } catch (IOException e) { e.printStackTrace(); } @@ -226,10 +261,12 @@ public class ExtensionManager { final String extensionClasses = System.getProperty(INDEV_CLASSES_FOLDER); final String extensionResources = System.getProperty(INDEV_RESOURCES_FOLDER); try (InputStreamReader reader = new InputStreamReader(new FileInputStream(new File(extensionResources, "extension.json")))) { - DiscoveredExtension extension = new DiscoveredExtension(); + DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class); extension.files = new File[]{new File(extensionClasses), new File(extensionResources)}; - extension.description = GSON.fromJson(reader, JsonObject.class); - extensions.add(extension); + extension.checkIntegrity(); + if(extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) { + extensions.add(extension); + } } catch (IOException e) { e.printStackTrace(); } @@ -280,16 +317,13 @@ public class ExtensionManager { log.info("Start loading code modifiers..."); for (DiscoveredExtension extension : extensions) { try { - if (extension.description.has("codeModifiers")) { - final JsonArray codeModifierClasses = extension.description.getAsJsonArray("codeModifiers"); - for (JsonElement elem : codeModifierClasses) { - modifiableClassLoader.loadModifier(extension.files, elem.getAsString()); - } + for (String codeModifierClass : extension.getCodeModifiers()) { + modifiableClassLoader.loadModifier(extension.files, codeModifierClass); } - if (extension.description.has("mixinConfig")) { - final String mixinConfigFile = extension.description.get("mixinConfig").getAsString(); + if (extension.getMixinConfig() != null) { + final String mixinConfigFile = extension.getMixinConfig(); Mixins.addConfiguration(mixinConfigFile); - log.info("Found mixin in extension " + extension.description.get("name").getAsString() + ": " + mixinConfigFile); + log.info("Found mixin in extension " + extension.getName() + ": " + mixinConfigFile); } } catch (Exception e) { e.printStackTrace(); @@ -299,8 +333,5 @@ public class ExtensionManager { log.info("Done loading code modifiers."); } - private static class DiscoveredExtension { - private File[] files; - 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 index 76a07d675..951f20862 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java @@ -44,6 +44,7 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { add("org.apache"); add("org.spongepowered"); add("net.minestom.server.extras.selfmodification"); + add("org.jboss.shrinkwrap.resolver"); } }; /** @@ -223,6 +224,11 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { } } + @Override + public void addURL(URL url) { + super.addURL(url); + } + public List getModifiers() { return modifiers; } diff --git a/src/test/java/testextension/TestExtensionLauncherArgs.java b/src/test/java/testextension/TestExtensionLauncherArgs.java index f9130f23f..eb36cab23 100644 --- a/src/test/java/testextension/TestExtensionLauncherArgs.java +++ b/src/test/java/testextension/TestExtensionLauncherArgs.java @@ -11,7 +11,7 @@ public class TestExtensionLauncherArgs { 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); + Bootstrap.bootstrap("demo.MainDemo", argsWithMixins); } } diff --git a/src/test/java/testextension/TestExtensionLauncherNoSetup.java b/src/test/java/testextension/TestExtensionLauncherNoSetup.java index a3e358579..ef4b757fe 100644 --- a/src/test/java/testextension/TestExtensionLauncherNoSetup.java +++ b/src/test/java/testextension/TestExtensionLauncherNoSetup.java @@ -9,7 +9,7 @@ import org.spongepowered.asm.mixin.Mixins; public class TestExtensionLauncherNoSetup { public static void main(String[] args) { - Bootstrap.bootstrap("fr.themode.demo.MainDemo", args); + Bootstrap.bootstrap("demo.MainDemo", args); } } diff --git a/src/test/java/testextension/mixins/DynamicChunkMixin.java b/src/test/java/testextension/mixins/DynamicChunkMixin.java index a71075469..468687446 100644 --- a/src/test/java/testextension/mixins/DynamicChunkMixin.java +++ b/src/test/java/testextension/mixins/DynamicChunkMixin.java @@ -9,7 +9,7 @@ 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) + @ModifyVariable(method = "UNSAFE_setBlock", at = @At("HEAD"), index = 4, require = 1, argsOnly = true, remap = false) public short oopsAllTnt(short blockStateId) { if(blockStateId != 0) return Block.TNT.getBlockId(); diff --git a/src/test/resources/extension.json b/src/test/resources/extension.json index c7d404173..46a91d0b9 100644 --- a/src/test/resources/extension.json +++ b/src/test/resources/extension.json @@ -1,6 +1,6 @@ { "entrypoint": "testextension.TestExtension", - "name": "Test extension", + "name": "Test_extension", "codeModifiers": [ "testextension.TestModifier" ], From 521796425997b7d9cab105da788c5a20186ed0d3 Mon Sep 17 00:00:00 2001 From: Articdive <13535885+Articdive@users.noreply.github.com> Date: Sun, 25 Oct 2020 10:41:51 +0100 Subject: [PATCH 02/13] Cleanup and improve extension changes. --- .../extensions/DiscoveredExtension.java | 204 ++++++++++-------- .../minestom/server/extensions/Extension.java | 14 +- .../server/extensions/ExtensionManager.java | 81 ++++--- 3 files changed, 161 insertions(+), 138 deletions(-) diff --git a/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java b/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java index 094345266..260fd6417 100644 --- a/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java +++ b/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java @@ -2,13 +2,114 @@ package net.minestom.server.extensions; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.io.File; -@Slf4j -class DiscoveredExtension { - private static String NAME_REGEX = "[A-Za-z][_A-Za-z0-9]+"; +@Slf4j(topic = "minestom-extensions") +final class DiscoveredExtension { + public static final String NAME_REGEX = "[A-Za-z][_A-Za-z0-9]+"; + private String name; + private String entrypoint; + private String version; + private String mixinConfig; + private String[] authors; + private String[] codeModifiers; + private Dependencies dependencies; + transient File[] files = new File[0]; + transient LoadStatus loadStatus = LoadStatus.LOAD_SUCCESS; + + @NotNull + public String getName() { + return name; + } + + @NotNull + public String[] getCodeModifiers() { + if (codeModifiers == null) { + codeModifiers = new String[0]; + } + return codeModifiers; + } + + @NotNull + public String getMixinConfig() { + return mixinConfig; + } + + @NotNull + public String[] getAuthors() { + return authors; + } + + @NotNull + public String getVersion() { + return version; + } + + @NotNull + public String getEntrypoint() { + return entrypoint; + } + + @NotNull + public Dependencies getDependencies() { + return dependencies; + } + + static void verifyIntegrity(@NotNull DiscoveredExtension extension) { + if (extension.name == null) { + StringBuilder fileList = new StringBuilder(); + for (File f : extension.files) { + fileList.append(f.getAbsolutePath()).append(", "); + } + log.error("Extension with no name. (at {}})", fileList); + log.error("Extension at ({}) will not be loaded.", fileList); + extension.loadStatus = DiscoveredExtension.LoadStatus.INVALID_NAME; + + // To ensure @NotNull: name = INVALID_NAME + extension.name = extension.loadStatus.name(); + return; + } + if (!extension.name.matches(NAME_REGEX)) { + log.error("Extension '{}' specified an invalid name.", extension.name); + log.error("Extension '{}' will not be loaded.", extension.name); + extension.loadStatus = DiscoveredExtension.LoadStatus.INVALID_NAME; + + // To ensure @NotNull: name = INVALID_NAME + extension.name = extension.loadStatus.name(); + return; + } + if (extension.entrypoint == null) { + log.error("Extension '{}' did not specify an entry point (via 'entrypoint').", extension.name); + log.error("Extension '{}' will not be loaded.", extension.name); + extension.loadStatus = DiscoveredExtension.LoadStatus.NO_ENTRYPOINT; + + // To ensure @NotNull: entrypoint = NO_ENTRYPOINT + extension.entrypoint = extension.loadStatus.name(); + return; + } + // Handle defaults + // If we reach this code, then the extension will most likely be loaded: + if (extension.version == null) { + log.warn("Extension '{}' did not specify a version.", extension.name); + log.warn("Extension '{}' will continue to load but should specify a plugin version.", extension.name); + extension.version = "Unspecified"; + } + if (extension.mixinConfig == null) { + extension.mixinConfig = ""; + } + if (extension.authors == null) { + extension.authors = new String[0]; + } + if (extension.codeModifiers == null) { + extension.codeModifiers = new String[0]; + } + // No dependencies were specified + if (extension.dependencies == null) { + extension.dependencies = new Dependencies(); + } + + } enum LoadStatus { LOAD_SUCCESS("Actually, it did not fail. This message should not have been printed."), @@ -19,102 +120,23 @@ class DiscoveredExtension { private final String message; - LoadStatus(String message) { + LoadStatus(@NotNull String message) { this.message = message; } + @NotNull public String getMessage() { return message; } } - static class Dependencies { + static final class Dependencies { + Repository[] repositories = new Repository[0]; + String[] artifacts = new String[0]; + static class Repository { - String name; - String url; + String name = ""; + String url = ""; } - - Repository[] repositories; - String[] artifacts; - } - - transient File[] files = new File[0]; - transient LoadStatus loadStatus = LoadStatus.LOAD_SUCCESS; - - private String[] codeModifiers; - private String[] authors; - private String mixinConfig; - private String name; - private String version; - private String entrypoint; - private Dependencies dependencies; - - void checkIntegrity() { - if(name == null) { - StringBuilder fileList = new StringBuilder(); - for(File f : files) { - fileList.append(f.getAbsolutePath()).append(", "); - } - log.error("Extension with no name. (at {}})", fileList); - log.error("Extension at ({}) will not be loaded.", fileList); - loadStatus = LoadStatus.INVALID_NAME; - return; - } - if(!name.matches(NAME_REGEX)) { - log.error("Extension '{}' specified an invalid name.", name); - log.error("Extension '{}' will not be loaded.", name); - loadStatus = LoadStatus.INVALID_NAME; - return; - } - if(entrypoint == null) { - log.error("Extension '{}' did not specify an entry point (via 'entrypoint').", name); - log.error("Extension '{}' will not be loaded.", name); - loadStatus = LoadStatus.NO_ENTRYPOINT; - return; - } - if(codeModifiers == null) { - codeModifiers = new String[0]; - } - } - - @NotNull - public String getName() { - if(name == null) { - throw new IllegalStateException("Missing extension name"); - } - return name; - } - - @NotNull - public String[] getCodeModifiers() { - if(codeModifiers == null) { - codeModifiers = new String[0]; - } - return codeModifiers; - } - - @Nullable - public String getMixinConfig() { - return mixinConfig; - } - - @Nullable - public String[] getAuthors() { - return authors; - } - - @Nullable - public String getVersion() { - return version; - } - - @NotNull - public String getEntrypoint() { - return entrypoint; - } - - @Nullable - public Dependencies getDependencies() { - return dependencies; } } diff --git a/src/main/java/net/minestom/server/extensions/Extension.java b/src/main/java/net/minestom/server/extensions/Extension.java index 913ca98a8..6a50779b9 100644 --- a/src/main/java/net/minestom/server/extensions/Extension.java +++ b/src/main/java/net/minestom/server/extensions/Extension.java @@ -1,14 +1,16 @@ package net.minestom.server.extensions; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; -import java.util.ArrayList; import java.util.List; public abstract class Extension { + // Set by reflection + @SuppressWarnings("unused") private ExtensionDescription description; + // Set by reflection + @SuppressWarnings("unused") private Logger logger; protected Extension() { @@ -35,23 +37,25 @@ public abstract class Extension { } + @NotNull public ExtensionDescription getDescription() { return description; } + @NotNull protected Logger getLogger() { return logger; } - protected static class ExtensionDescription { + public static class ExtensionDescription { private final String name; private final String version; private final List authors; - protected ExtensionDescription(@NotNull String name, @NotNull String version, @Nullable List authors) { + ExtensionDescription(@NotNull String name, @NotNull String version, @NotNull List authors) { this.name = name; this.version = version; - this.authors = authors != null ? authors : new ArrayList<>(); + this.authors = authors; } @NotNull diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index 364c969e8..eed15d1d1 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -22,10 +22,15 @@ import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; import java.util.zip.ZipFile; -@Slf4j +@Slf4j(topic = "Minestom-Extensions") public class ExtensionManager { private final static String INDEV_CLASSES_FOLDER = "minestom.extension.indevfolder.classes"; @@ -78,28 +83,14 @@ public class ExtensionManager { continue; } - // Get ExtensionDescription (authors, version etc.) + // Create ExtensionDescription (authors, version etc.) String extensionName = discoveredExtension.getName(); String mainClass = discoveredExtension.getEntrypoint(); - Extension.ExtensionDescription extensionDescription; - { - String version; - if (discoveredExtension.getVersion() == null) { - log.warn("Extension '{}' did not specify a version.", extensionName); - log.warn("Extension '{}' will continue to load but should specify a plugin version.", extensionName); - version = "Not Specified"; - } else { - version = discoveredExtension.getVersion(); - } - List authors; - if (discoveredExtension.getAuthors() == null) { - authors = new ArrayList<>(); - } else { - authors = Arrays.asList(discoveredExtension.getAuthors()); - } - - extensionDescription = new Extension.ExtensionDescription(extensionName, version, authors); - } + Extension.ExtensionDescription extensionDescription = new Extension.ExtensionDescription( + extensionName, + discoveredExtension.getVersion(), + Arrays.asList(discoveredExtension.getAuthors()) + ); extensionLoaders.put(extensionName.toLowerCase(), loader); @@ -165,9 +156,9 @@ public class ExtensionManager { // Set logger try { - Field descriptionField = extensionClass.getSuperclass().getDeclaredField("logger"); - descriptionField.setAccessible(true); - descriptionField.set(extension, LoggerFactory.getLogger(extensionClass)); + Field loggerField = extensionClass.getSuperclass().getDeclaredField("logger"); + loggerField.setAccessible(true); + loggerField.set(extension, LoggerFactory.getLogger(extensionClass)); } catch (IllegalAccessException e) { // We made it accessible, should not occur e.printStackTrace(); @@ -181,29 +172,29 @@ public class ExtensionManager { } private void loadDependencies(List extensions) { - for(DiscoveredExtension ext : extensions) { + for (DiscoveredExtension ext : extensions) { try { DependencyGetter getter = new DependencyGetter(); DiscoveredExtension.Dependencies dependencies = ext.getDependencies(); - if(dependencies.repositories == null) { - throw new IllegalStateException("Missing 'repositories' array."); - } - if(dependencies.artifacts == null) { - throw new IllegalStateException("Missing 'artifacts' array."); - } List repoList = new LinkedList<>(); - for(var repository : dependencies.repositories) { - if(repository.name == null) { + for (var repository : dependencies.repositories) { + if (repository.name == null) { throw new IllegalStateException("Missing 'name' element in repository object."); } - if(repository.url == null) { + if (repository.name.isEmpty()) { + throw new IllegalStateException("Invalid 'name' element in repository object."); + } + if (repository.url == null) { throw new IllegalStateException("Missing 'url' element in repository object."); } + if (repository.url.isEmpty()) { + throw new IllegalStateException("Invalid 'url' element in repository object."); + } repoList.add(new MavenRepository(repository.name, repository.url)); } getter.addMavenResolver(repoList); - for(var artifact : dependencies.artifacts) { + for (var artifact : dependencies.artifacts) { var resolved = getter.get(artifact, dependenciesFolder); injectIntoClasspath(resolved.getContentsLocation(), ext); log.trace("Dependency of extension {}: {}", ext.getName(), resolved); @@ -227,7 +218,7 @@ public class ExtensionManager { addURL.setAccessible(true); addURL.invoke(cl, dependency); } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - throw new RuntimeException("Failed to inject URL "+dependency+" into classpath. From extension "+extension.getName(), e); + throw new RuntimeException("Failed to inject URL " + dependency + " into classpath. From extension " + extension.getName(), e); } } @@ -246,8 +237,11 @@ public class ExtensionManager { DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class); extension.files = new File[]{file}; - extension.checkIntegrity(); - if(extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) { + + // Verify integrity and ensure defaults + DiscoveredExtension.verifyIntegrity(extension); + + if (extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) { extensions.add(extension); } } catch (IOException e) { @@ -263,8 +257,11 @@ public class ExtensionManager { try (InputStreamReader reader = new InputStreamReader(new FileInputStream(new File(extensionResources, "extension.json")))) { DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class); extension.files = new File[]{new File(extensionClasses), new File(extensionResources)}; - extension.checkIntegrity(); - if(extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) { + + // Verify integrity and ensure defaults + DiscoveredExtension.verifyIntegrity(extension); + + if (extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) { extensions.add(extension); } } catch (IOException e) { @@ -320,7 +317,7 @@ public class ExtensionManager { for (String codeModifierClass : extension.getCodeModifiers()) { modifiableClassLoader.loadModifier(extension.files, codeModifierClass); } - if (extension.getMixinConfig() != null) { + if (!extension.getMixinConfig().isEmpty()) { final String mixinConfigFile = extension.getMixinConfig(); Mixins.addConfiguration(mixinConfigFile); log.info("Found mixin in extension " + extension.getName() + ": " + mixinConfigFile); From 0167a8f9ef3c57262d636b632c5863383692b023 Mon Sep 17 00:00:00 2001 From: Articdive <13535885+Articdive@users.noreply.github.com> Date: Sun, 25 Oct 2020 16:45:28 +0100 Subject: [PATCH 03/13] Add extension intra-dependency (hopefully) --- .../extensions/DiscoveredExtension.java | 42 +++-- .../server/extensions/ExtensionManager.java | 177 ++++++++++++------ 2 files changed, 149 insertions(+), 70 deletions(-) diff --git a/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java b/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java index 260fd6417..63a009006 100644 --- a/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java +++ b/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java @@ -14,7 +14,8 @@ final class DiscoveredExtension { private String mixinConfig; private String[] authors; private String[] codeModifiers; - private Dependencies dependencies; + private String[] dependencies; + private ExternalDependencies externalDependencies; transient File[] files = new File[0]; transient LoadStatus loadStatus = LoadStatus.LOAD_SUCCESS; @@ -24,11 +25,13 @@ final class DiscoveredExtension { } @NotNull - public String[] getCodeModifiers() { - if (codeModifiers == null) { - codeModifiers = new String[0]; - } - return codeModifiers; + public String getEntrypoint() { + return entrypoint; + } + + @NotNull + public String getVersion() { + return version; } @NotNull @@ -42,20 +45,23 @@ final class DiscoveredExtension { } @NotNull - public String getVersion() { - return version; + public String[] getCodeModifiers() { + if (codeModifiers == null) { + codeModifiers = new String[0]; + } + return codeModifiers; } @NotNull - public String getEntrypoint() { - return entrypoint; - } - - @NotNull - public Dependencies getDependencies() { + public String[] getDependencies() { return dependencies; } + @NotNull + public ExternalDependencies getExternalDependencies() { + return externalDependencies; + } + static void verifyIntegrity(@NotNull DiscoveredExtension extension) { if (extension.name == null) { StringBuilder fileList = new StringBuilder(); @@ -106,7 +112,11 @@ final class DiscoveredExtension { } // No dependencies were specified if (extension.dependencies == null) { - extension.dependencies = new Dependencies(); + extension.dependencies = new String[0]; + } + // No external dependencies were specified; + if (extension.externalDependencies == null) { + extension.externalDependencies = new ExternalDependencies(); } } @@ -130,7 +140,7 @@ final class DiscoveredExtension { } } - static final class Dependencies { + static final class ExternalDependencies { Repository[] repositories = new Repository[0]; String[] artifacts = new String[0]; diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index eed15d1d1..770268d57 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -28,6 +28,7 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import java.util.zip.ZipFile; @Slf4j(topic = "Minestom-Extensions") @@ -64,7 +65,8 @@ public class ExtensionManager { } } - final List discoveredExtensions = discoverExtensions(); + List discoveredExtensions = discoverExtensions(); + discoveredExtensions = generateLoadOrder(discoveredExtensions); loadDependencies(discoveredExtensions); // remove invalid extensions discoveredExtensions.removeIf(ext -> ext.loadStatus != DiscoveredExtension.LoadStatus.LOAD_SUCCESS); @@ -171,57 +173,6 @@ public class ExtensionManager { } } - private void loadDependencies(List extensions) { - for (DiscoveredExtension ext : extensions) { - try { - DependencyGetter getter = new DependencyGetter(); - DiscoveredExtension.Dependencies dependencies = ext.getDependencies(); - List repoList = new LinkedList<>(); - for (var repository : dependencies.repositories) { - if (repository.name == null) { - throw new IllegalStateException("Missing 'name' element in repository object."); - } - if (repository.name.isEmpty()) { - throw new IllegalStateException("Invalid 'name' element in repository object."); - } - if (repository.url == null) { - throw new IllegalStateException("Missing 'url' element in repository object."); - } - if (repository.url.isEmpty()) { - throw new IllegalStateException("Invalid 'url' element in repository object."); - } - repoList.add(new MavenRepository(repository.name, repository.url)); - } - getter.addMavenResolver(repoList); - - for (var artifact : dependencies.artifacts) { - var resolved = getter.get(artifact, dependenciesFolder); - injectIntoClasspath(resolved.getContentsLocation(), ext); - log.trace("Dependency of extension {}: {}", ext.getName(), resolved); - } - } catch (Exception e) { - ext.loadStatus = DiscoveredExtension.LoadStatus.MISSING_DEPENDENCIES; - log.error("Failed to load dependencies for extension {}", ext.getName()); - log.error("Extension '{}' will not be loaded", ext.getName()); - log.error("This is the exception", e); - } - } - } - - private void injectIntoClasspath(URL dependency, DiscoveredExtension extension) { - final ClassLoader cl = getClass().getClassLoader(); - if (!(cl instanceof URLClassLoader)) { - throw new IllegalStateException("Current class loader is not a URLClassLoader, but " + cl + ". This prevents adding URLs into the classpath at runtime."); - } - try { - Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); - addURL.setAccessible(true); - addURL.invoke(cl, dependency); - } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - throw new RuntimeException("Failed to inject URL " + dependency + " into classpath. From extension " + extension.getName(), e); - } - } - @NotNull private List discoverExtensions() { List extensions = new LinkedList<>(); @@ -271,6 +222,126 @@ public class ExtensionManager { return extensions; } + private List generateLoadOrder(List discoveredExtensions) { + // Do some mapping so we can map strings to extensions. + Map extensionMap = new HashMap<>(); + Map> dependencyMap = new HashMap<>(); + for (DiscoveredExtension discoveredExtension : discoveredExtensions) { + extensionMap.put(discoveredExtension.getName().toLowerCase(), discoveredExtension); + } + for (DiscoveredExtension discoveredExtension : discoveredExtensions) { + + List dependencies = Arrays.stream(discoveredExtension.getDependencies()) + .map(dependencyName -> { + DiscoveredExtension dependencyExtension = extensionMap.get(dependencyName.toLowerCase()); + // Specifies an extension we don't have. + if (dependencyExtension == null) { + log.error("Extension {} requires an extension called {}.", discoveredExtension.getName(), dependencyName); + log.error("However the extension {} could not be found.", dependencyName); + log.error("Therefore {} will not be loaded.", dependencyName); + } + // This will return null for an unknown-extension + return extensionMap.get(dependencyName.toLowerCase()); + }).collect(Collectors.toList()); + + // If the list contains null ignore it. + if (!dependencies.contains(null)) { + dependencyMap.put( + discoveredExtension, + dependencies + ); + } + } + + // List containing the real load order. + LinkedList sortedList = new LinkedList<>(); + + // entries with empty lists + List>> loadableExtensions; + // While there are entries with no more elements (no more dependencies) + while (!( + loadableExtensions = dependencyMap.entrySet().stream().filter(entry -> entry.getValue().isEmpty()).collect(Collectors.toList()) + ).isEmpty() + ) { + // Get all "loadable" (not actually being loaded!) extensions and put them in the sorted list. + for (Map.Entry> entry : loadableExtensions) { + // Add to sorted list. + sortedList.add(entry.getKey()); + // Remove to make the next iterations a little bit quicker (hopefully) and to find cyclic dependencies. + dependencyMap.remove(entry.getKey()); + // Remove this dependency from all the lists (if they include it) to make way for next level of extensions. + dependencyMap.forEach((key, dependencyList) -> dependencyList.remove(entry.getKey())); + } + } + + // Check if there are cyclic extensions. + if (!dependencyMap.isEmpty()) { + log.error("Minestom found " + dependencyMap.size() + " cyclic extensions."); + log.error("Cyclic extensions depend on each other and can therefore not be loaded."); + for (Map.Entry> entry : dependencyMap.entrySet()) { + DiscoveredExtension discoveredExtension = entry.getKey(); + log.error(discoveredExtension.getName() + " could not be loaded, as it depends on: " + + entry.getValue().stream().map(DiscoveredExtension::getName).collect(Collectors.joining(", ")) + + "." + ); + } + + } + + return sortedList; + } + + private void loadDependencies(List extensions) { + for (DiscoveredExtension ext : extensions) { + try { + DependencyGetter getter = new DependencyGetter(); + DiscoveredExtension.ExternalDependencies externalDependencies = ext.getExternalDependencies(); + List repoList = new LinkedList<>(); + for (var repository : externalDependencies.repositories) { + if (repository.name == null) { + throw new IllegalStateException("Missing 'name' element in repository object."); + } + if (repository.name.isEmpty()) { + throw new IllegalStateException("Invalid 'name' element in repository object."); + } + if (repository.url == null) { + throw new IllegalStateException("Missing 'url' element in repository object."); + } + if (repository.url.isEmpty()) { + throw new IllegalStateException("Invalid 'url' element in repository object."); + } + repoList.add(new MavenRepository(repository.name, repository.url)); + } + getter.addMavenResolver(repoList); + + for (var artifact : externalDependencies.artifacts) { + var resolved = getter.get(artifact, dependenciesFolder); + injectIntoClasspath(resolved.getContentsLocation(), ext); + log.trace("Dependency of extension {}: {}", ext.getName(), resolved); + } + } catch (Exception e) { + ext.loadStatus = DiscoveredExtension.LoadStatus.MISSING_DEPENDENCIES; + log.error("Failed to load dependencies for extension {}", ext.getName()); + log.error("Extension '{}' will not be loaded", ext.getName()); + log.error("This is the exception", e); + } + } + } + + private void injectIntoClasspath(URL dependency, DiscoveredExtension extension) { + final ClassLoader cl = getClass().getClassLoader(); + if (!(cl instanceof URLClassLoader)) { + throw new IllegalStateException("Current class loader is not a URLClassLoader, but " + cl + ". This prevents adding URLs into the classpath at runtime."); + } + try { + Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + addURL.setAccessible(true); + addURL.invoke(cl, dependency); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException("Failed to inject URL " + dependency + " into classpath. From extension " + extension.getName(), e); + } + } + /** * Loads a URL into the classpath. * @@ -329,6 +400,4 @@ public class ExtensionManager { } log.info("Done loading code modifiers."); } - - } From e47464068726752fdae045b2fb65e13211c28983 Mon Sep 17 00:00:00 2001 From: jglrxavpok Date: Sun, 25 Oct 2020 18:03:28 +0100 Subject: [PATCH 04/13] Update discovered extension status when an extension-dependency is not found --- .../java/net/minestom/server/extensions/ExtensionManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index 770268d57..fa922972b 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -239,6 +239,7 @@ public class ExtensionManager { log.error("Extension {} requires an extension called {}.", discoveredExtension.getName(), dependencyName); log.error("However the extension {} could not be found.", dependencyName); log.error("Therefore {} will not be loaded.", dependencyName); + discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.MISSING_DEPENDENCIES; } // This will return null for an unknown-extension return extensionMap.get(dependencyName.toLowerCase()); From 6df285723a8d87724f37780998db5075056aedcb Mon Sep 17 00:00:00 2001 From: jglrxavpok Date: Sun, 25 Oct 2020 19:58:19 +0100 Subject: [PATCH 05/13] Load extension dependencies --- prismarine-minecraft-data | 2 +- .../extensions/DiscoveredExtension.java | 9 ++-- .../ExtensionDependencyResolver.java | 49 ++++++++++++++++++ .../server/extensions/ExtensionManager.java | 51 +++++++++++-------- .../MinestomOverwriteClassLoader.java | 7 ++- 5 files changed, 91 insertions(+), 27 deletions(-) create mode 100644 src/main/java/net/minestom/server/extensions/ExtensionDependencyResolver.java diff --git a/prismarine-minecraft-data b/prismarine-minecraft-data index 08dfbb32b..472b3c041 160000 --- a/prismarine-minecraft-data +++ b/prismarine-minecraft-data @@ -1 +1 @@ -Subproject commit 08dfbb32bdb0c0f6ee2c5229f92fef3132668405 +Subproject commit 472b3c041c58bd0a4397fb9144b1035682022cbe diff --git a/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java b/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java index 63a009006..17d4d424f 100644 --- a/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java +++ b/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java @@ -4,6 +4,9 @@ import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import java.io.File; +import java.net.URL; +import java.util.LinkedList; +import java.util.List; @Slf4j(topic = "minestom-extensions") final class DiscoveredExtension { @@ -16,7 +19,7 @@ final class DiscoveredExtension { private String[] codeModifiers; private String[] dependencies; private ExternalDependencies externalDependencies; - transient File[] files = new File[0]; + transient List files = new LinkedList<>(); transient LoadStatus loadStatus = LoadStatus.LOAD_SUCCESS; @NotNull @@ -65,8 +68,8 @@ final class DiscoveredExtension { static void verifyIntegrity(@NotNull DiscoveredExtension extension) { if (extension.name == null) { StringBuilder fileList = new StringBuilder(); - for (File f : extension.files) { - fileList.append(f.getAbsolutePath()).append(", "); + for (URL f : extension.files) { + fileList.append(f.toExternalForm()).append(", "); } log.error("Extension with no name. (at {}})", fileList); log.error("Extension at ({}) will not be loaded.", fileList); diff --git a/src/main/java/net/minestom/server/extensions/ExtensionDependencyResolver.java b/src/main/java/net/minestom/server/extensions/ExtensionDependencyResolver.java new file mode 100644 index 000000000..a3a0a2415 --- /dev/null +++ b/src/main/java/net/minestom/server/extensions/ExtensionDependencyResolver.java @@ -0,0 +1,49 @@ +package net.minestom.server.extensions; + +import net.minestom.dependencies.DependencyResolver; +import net.minestom.dependencies.ResolvedDependency; +import net.minestom.dependencies.UnresolvedDependencyException; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.net.URL; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Does NOT relocate extensions + */ +public class ExtensionDependencyResolver implements DependencyResolver { + + private Map extensionMap = new HashMap<>(); + + public ExtensionDependencyResolver(List extensions) { + for(DiscoveredExtension ext : extensions) { + extensionMap.put(ext.getName(), ext); + } + } + + @NotNull + @Override + public ResolvedDependency resolve(@NotNull String extensionName, @NotNull File file) throws UnresolvedDependencyException { + if(extensionMap.containsKey(extensionName)) { + DiscoveredExtension ext = extensionMap.get(extensionName); + // convert extension URLs to subdependencies + List deps = new LinkedList<>(); + for(URL u : ext.files) { + deps.add(new ResolvedDependency(u.toExternalForm(), u.toExternalForm(), "", u, new LinkedList<>())); + } + return new ResolvedDependency(ext.getName(), ext.getName(), ext.getVersion(), ext.files.get(0), deps); + } + throw new UnresolvedDependencyException("No extension named "+extensionName); + } + + @Override + public String toString() { + String list = extensionMap.values().stream().map(entry -> entry.getName()).collect(Collectors.joining(", ")); + return "ExtensionDependencyResolver[" + list + "]"; + } +} diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index fa922972b..d47153f90 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -74,16 +74,8 @@ public class ExtensionManager { for (DiscoveredExtension discoveredExtension : discoveredExtensions) { URLClassLoader loader; - URL[] urls = new URL[discoveredExtension.files.length]; - try { - for (int i = 0; i < urls.length; i++) { - urls[i] = discoveredExtension.files[i].toURI().toURL(); - } - loader = newClassLoader(urls); - } catch (MalformedURLException e) { - log.error("Failed to get URL.", e); - continue; - } + URL[] urls = discoveredExtension.files.toArray(new URL[0]); + loader = newClassLoader(urls); // Create ExtensionDescription (authors, version etc.) String extensionName = discoveredExtension.getName(); @@ -187,7 +179,7 @@ public class ExtensionManager { InputStreamReader reader = new InputStreamReader(f.getInputStream(f.getEntry("extension.json")))) { DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class); - extension.files = new File[]{file}; + extension.files.add(file.toURI().toURL()); // Verify integrity and ensure defaults DiscoveredExtension.verifyIntegrity(extension); @@ -207,7 +199,8 @@ public class ExtensionManager { final String extensionResources = System.getProperty(INDEV_RESOURCES_FOLDER); try (InputStreamReader reader = new InputStreamReader(new FileInputStream(new File(extensionResources, "extension.json")))) { DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class); - extension.files = new File[]{new File(extensionClasses), new File(extensionResources)}; + extension.files.add(new File(extensionClasses).toURI().toURL()); + extension.files.add(new File(extensionResources).toURI().toURL()); // Verify integrity and ensure defaults DiscoveredExtension.verifyIntegrity(extension); @@ -293,6 +286,7 @@ public class ExtensionManager { } private void loadDependencies(List extensions) { + ExtensionDependencyResolver extensionDependencyResolver = new ExtensionDependencyResolver(extensions); for (DiscoveredExtension ext : extensions) { try { DependencyGetter getter = new DependencyGetter(); @@ -314,12 +308,19 @@ public class ExtensionManager { repoList.add(new MavenRepository(repository.name, repository.url)); } getter.addMavenResolver(repoList); + getter.addResolver(extensionDependencyResolver); for (var artifact : externalDependencies.artifacts) { var resolved = getter.get(artifact, dependenciesFolder); injectIntoClasspath(resolved.getContentsLocation(), ext); log.trace("Dependency of extension {}: {}", ext.getName(), resolved); } + + for (var dependencyName : ext.getDependencies()) { + var resolved = getter.get(dependencyName, dependenciesFolder); + injectIntoClasspath(resolved.getContentsLocation(), ext); + log.trace("Dependency of extension {}: {}", ext.getName(), resolved); + } } catch (Exception e) { ext.loadStatus = DiscoveredExtension.LoadStatus.MISSING_DEPENDENCIES; log.error("Failed to load dependencies for extension {}", ext.getName()); @@ -330,17 +331,23 @@ public class ExtensionManager { } private void injectIntoClasspath(URL dependency, DiscoveredExtension extension) { - final ClassLoader cl = getClass().getClassLoader(); + extension.files.add(dependency); + log.trace("Added dependency {} to extension {} classpath", dependency.toExternalForm(), extension.getName()); + /*final ClassLoader cl = getClass().getClassLoader(); if (!(cl instanceof URLClassLoader)) { throw new IllegalStateException("Current class loader is not a URLClassLoader, but " + cl + ". This prevents adding URLs into the classpath at runtime."); } - try { - Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); - addURL.setAccessible(true); - addURL.invoke(cl, dependency); - } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - throw new RuntimeException("Failed to inject URL " + dependency + " into classpath. From extension " + extension.getName(), e); - } + if(cl instanceof MinestomOverwriteClassLoader) { + ((MinestomOverwriteClassLoader) cl).addURL(dependency); // no reflection warnings for us! + } else { + try { + Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + addURL.setAccessible(true); + addURL.invoke(cl, dependency); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException("Failed to inject URL " + dependency + " into classpath. From extension " + extension.getName(), e); + } + }*/ } /** @@ -387,7 +394,7 @@ public class ExtensionManager { for (DiscoveredExtension extension : extensions) { try { for (String codeModifierClass : extension.getCodeModifiers()) { - modifiableClassLoader.loadModifier(extension.files, codeModifierClass); + modifiableClassLoader.loadModifier(extension.files.toArray(new File[0]), codeModifierClass); } if (!extension.getMixinConfig().isEmpty()) { final String mixinConfigFile = extension.getMixinConfig(); @@ -396,7 +403,7 @@ public class ExtensionManager { } } catch (Exception e) { e.printStackTrace(); - log.error("Failed to load code modifier for extension in files: " + Arrays.toString(extension.files), e); + log.error("Failed to load code modifier for extension in files: " + extension.files.stream().map(u -> u.toExternalForm()).collect(Collectors.joining(", ")), e); } } log.info("Done loading code modifiers."); 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 951f20862..0ce77b25e 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java @@ -8,6 +8,7 @@ import org.objectweb.asm.tree.ClassNode; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; import java.net.URL; @@ -158,7 +159,11 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { if (name == null) throw new ClassNotFoundException(); String path = name.replace(".", "/") + ".class"; - byte[] bytes = getResourceAsStream(path).readAllBytes(); + InputStream input = getResourceAsStream(path); + if(input == null) { + throw new ClassNotFoundException("Could not find resource "+path); + } + byte[] bytes = input.readAllBytes(); if (transform && !isProtected(name)) { ClassReader reader = new ClassReader(bytes); ClassNode node = new ClassNode(); From 66e85f166e4d81e271ef1042dd20595585300791 Mon Sep 17 00:00:00 2001 From: jglrxavpok Date: Sun, 25 Oct 2020 20:27:34 +0100 Subject: [PATCH 06/13] Fix load order --- .../server/extensions/ExtensionManager.java | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index d47153f90..1f7bf28a0 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -19,15 +19,9 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; 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.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import java.util.zip.ZipFile; @@ -44,6 +38,9 @@ public class ExtensionManager { private final File dependenciesFolder = new File(extensionFolder, ".libs"); private boolean loaded; + // not final to add to it, and then make it immutable + private List extensionList = new ArrayList<>(); + public ExtensionManager() { } @@ -75,6 +72,10 @@ public class ExtensionManager { for (DiscoveredExtension discoveredExtension : discoveredExtensions) { URLClassLoader loader; URL[] urls = discoveredExtension.files.toArray(new URL[0]); + // TODO: Only putting each extension into its own classloader prevents code modifications (via code modifiers or mixins) + // TODO: If we want modifications to be possible, we need to add these urls to the current classloader + // TODO: Indeed, without adding the urls, the classloader is not able to load the bytecode of extension classes + // TODO: Whether we want to allow extensions to modify one-another is our choice now. loader = newClassLoader(urls); // Create ExtensionDescription (authors, version etc.) @@ -161,8 +162,10 @@ public class ExtensionManager { log.error("Main class '{}' in '{}' has no logger field.", mainClass, extensionName, e); } + extensionList.add(extension); // add to a list, as lists preserve order extensions.put(extensionName.toLowerCase(), extension); } + extensionList = Collections.unmodifiableList(extensionList); } @NotNull @@ -312,13 +315,13 @@ public class ExtensionManager { for (var artifact : externalDependencies.artifacts) { var resolved = getter.get(artifact, dependenciesFolder); - injectIntoClasspath(resolved.getContentsLocation(), ext); + addDependencyFile(resolved.getContentsLocation(), ext); log.trace("Dependency of extension {}: {}", ext.getName(), resolved); } for (var dependencyName : ext.getDependencies()) { var resolved = getter.get(dependencyName, dependenciesFolder); - injectIntoClasspath(resolved.getContentsLocation(), ext); + addDependencyFile(resolved.getContentsLocation(), ext); log.trace("Dependency of extension {}: {}", ext.getName(), resolved); } } catch (Exception e) { @@ -330,10 +333,10 @@ public class ExtensionManager { } } + // TODO: remove if extensions cannot modify one-another + // TODO: use if they can private void injectIntoClasspath(URL dependency, DiscoveredExtension extension) { - extension.files.add(dependency); - log.trace("Added dependency {} to extension {} classpath", dependency.toExternalForm(), extension.getName()); - /*final ClassLoader cl = getClass().getClassLoader(); + final ClassLoader cl = getClass().getClassLoader(); if (!(cl instanceof URLClassLoader)) { throw new IllegalStateException("Current class loader is not a URLClassLoader, but " + cl + ". This prevents adding URLs into the classpath at runtime."); } @@ -347,7 +350,12 @@ public class ExtensionManager { } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { throw new RuntimeException("Failed to inject URL " + dependency + " into classpath. From extension " + extension.getName(), e); } - }*/ + } + } + + private void addDependencyFile(URL dependency, DiscoveredExtension extension) { + extension.files.add(dependency); + log.trace("Added dependency {} to extension {} classpath", dependency.toExternalForm(), extension.getName()); } /** @@ -367,7 +375,7 @@ public class ExtensionManager { @NotNull public List getExtensions() { - return new ArrayList<>(extensions.values()); + return extensionList; } @Nullable From 35f7b27135457e21a6511af2f00e300ccd3c1971 Mon Sep 17 00:00:00 2001 From: jglrxavpok Date: Sun, 25 Oct 2020 20:32:50 +0100 Subject: [PATCH 07/13] Comment to remember to fix extension dependency resolution in case we keep separate classloaders for extensions --- .../server/extensions/ExtensionDependencyResolver.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/net/minestom/server/extensions/ExtensionDependencyResolver.java b/src/main/java/net/minestom/server/extensions/ExtensionDependencyResolver.java index a3a0a2415..b228c2850 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionDependencyResolver.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionDependencyResolver.java @@ -32,6 +32,11 @@ public class ExtensionDependencyResolver implements DependencyResolver { if(extensionMap.containsKey(extensionName)) { DiscoveredExtension ext = extensionMap.get(extensionName); // convert extension URLs to subdependencies + // FIXME: this is not a deep conversion, this might create an issue in this scenario with different classloaders: + // A depends on an external lib (Ext<-A) + // B depends on A (A<-B) + // When loading B, with no deep conversion, Ext will not be added to the list of dependencies (because it is not a direct dependency) + // But when trying to call/access code from extension A, the parts dependent on Ext won't be inside B's dependencies, triggering a ClassNotFoundException List deps = new LinkedList<>(); for(URL u : ext.files) { deps.add(new ResolvedDependency(u.toExternalForm(), u.toExternalForm(), "", u, new LinkedList<>())); From 47eb0084eb969f5abdb2498a6df2d66d5f7864d9 Mon Sep 17 00:00:00 2001 From: jglrxavpok Date: Wed, 28 Oct 2020 16:24:29 +0100 Subject: [PATCH 08/13] Start of tree-based classloading --- .../java/net/minestom/server/Bootstrap.java | 10 +-- .../server/extensions/ExtensionManager.java | 21 +++--- .../extras/selfmodification/CodeModifier.java | 2 +- .../MinestomExtensionClassLoader.java | 70 +++++++++++++++++++ ...ader.java => MinestomRootClassLoader.java} | 66 ++++++++++++----- .../mixins/MinestomBytecodeProvider.java | 6 +- .../mixins/MinestomClassProvider.java | 6 +- .../mixins/MixinServiceMinestom.java | 6 +- 8 files changed, 145 insertions(+), 42 deletions(-) create mode 100644 src/main/java/net/minestom/server/extras/selfmodification/MinestomExtensionClassLoader.java rename src/main/java/net/minestom/server/extras/selfmodification/{MinestomOverwriteClassLoader.java => MinestomRootClassLoader.java} (78%) diff --git a/src/main/java/net/minestom/server/Bootstrap.java b/src/main/java/net/minestom/server/Bootstrap.java index 5b1174acd..9e96b65df 100644 --- a/src/main/java/net/minestom/server/Bootstrap.java +++ b/src/main/java/net/minestom/server/Bootstrap.java @@ -1,6 +1,6 @@ package net.minestom.server; -import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; +import net.minestom.server.extras.selfmodification.MinestomRootClassLoader; import net.minestom.server.extras.selfmodification.mixins.MixinCodeModifier; import net.minestom.server.extras.selfmodification.mixins.MixinServiceMinestom; import org.spongepowered.asm.launch.MixinBootstrap; @@ -12,15 +12,15 @@ import java.lang.reflect.Method; import java.util.Arrays; /** - * Used to launch Minestom with the {@link MinestomOverwriteClassLoader} to allow for self-modifications + * Used to launch Minestom with the {@link MinestomRootClassLoader} to allow for self-modifications */ public final class Bootstrap { public static void bootstrap(String mainClassFullName, String[] args) { try { - ClassLoader classLoader = MinestomOverwriteClassLoader.getInstance(); + ClassLoader classLoader = MinestomRootClassLoader.getInstance(); startMixin(args); - MinestomOverwriteClassLoader.getInstance().addCodeModifier(new MixinCodeModifier()); + MinestomRootClassLoader.getInstance().addCodeModifier(new MixinCodeModifier()); MixinServiceMinestom.gotoPreinitPhase(); // ensure extensions are loaded when starting the server @@ -53,6 +53,6 @@ public final 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 -> MinestomRootClassLoader.getInstance().protectedPackages.add(c.getConfig().getMixinPackage())); } } diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index 1f7bf28a0..a52e40a67 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -4,7 +4,8 @@ import com.google.gson.Gson; import lombok.extern.slf4j.Slf4j; import net.minestom.dependencies.DependencyGetter; import net.minestom.dependencies.maven.MavenRepository; -import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; +import net.minestom.server.extras.selfmodification.MinestomExtensionClassLoader; +import net.minestom.server.extras.selfmodification.MinestomRootClassLoader; import net.minestom.server.utils.validate.Check; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -76,7 +77,7 @@ public class ExtensionManager { // TODO: If we want modifications to be possible, we need to add these urls to the current classloader // TODO: Indeed, without adding the urls, the classloader is not able to load the bytecode of extension classes // TODO: Whether we want to allow extensions to modify one-another is our choice now. - loader = newClassLoader(urls); + loader = newClassLoader(discoveredExtension, urls); // Create ExtensionDescription (authors, version etc.) String extensionName = discoveredExtension.getName(); @@ -340,8 +341,8 @@ public class ExtensionManager { if (!(cl instanceof URLClassLoader)) { throw new IllegalStateException("Current class loader is not a URLClassLoader, but " + cl + ". This prevents adding URLs into the classpath at runtime."); } - if(cl instanceof MinestomOverwriteClassLoader) { - ((MinestomOverwriteClassLoader) cl).addURL(dependency); // no reflection warnings for us! + if(cl instanceof MinestomRootClassLoader) { + ((MinestomRootClassLoader) cl).addURL(dependency); // no reflection warnings for us! } else { try { Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); @@ -364,8 +365,12 @@ public class ExtensionManager { * @param urls {@link URL} (usually a JAR) that should be loaded. */ @NotNull - public URLClassLoader newClassLoader(@NotNull URL[] urls) { - return URLClassLoader.newInstance(urls, ExtensionManager.class.getClassLoader()); + public URLClassLoader newClassLoader(@NotNull DiscoveredExtension extension, @NotNull URL[] urls) { + MinestomRootClassLoader root = MinestomRootClassLoader.getInstance(); + MinestomExtensionClassLoader loader = new MinestomExtensionClassLoader(extension.getName(), urls, root); + // TODO: tree structure + root.addChild(loader); + return loader; } @NotNull @@ -393,11 +398,11 @@ public class ExtensionManager { */ private void setupCodeModifiers(@NotNull List extensions) { final ClassLoader cl = getClass().getClassLoader(); - if (!(cl instanceof MinestomOverwriteClassLoader)) { + if (!(cl instanceof MinestomRootClassLoader)) { 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; + MinestomRootClassLoader modifiableClassLoader = (MinestomRootClassLoader) cl; log.info("Start loading code modifiers..."); for (DiscoveredExtension extension : extensions) { try { diff --git a/src/main/java/net/minestom/server/extras/selfmodification/CodeModifier.java b/src/main/java/net/minestom/server/extras/selfmodification/CodeModifier.java index faa739c0a..b02ebd4e4 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/CodeModifier.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/CodeModifier.java @@ -3,7 +3,7 @@ package net.minestom.server.extras.selfmodification; import org.objectweb.asm.tree.ClassNode; /** - * Will be called by {@link MinestomOverwriteClassLoader} to transform classes at load-time + * Will be called by {@link MinestomRootClassLoader} to transform classes at load-time */ public abstract class CodeModifier { /** diff --git a/src/main/java/net/minestom/server/extras/selfmodification/MinestomExtensionClassLoader.java b/src/main/java/net/minestom/server/extras/selfmodification/MinestomExtensionClassLoader.java new file mode 100644 index 000000000..5bd6bd804 --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/MinestomExtensionClassLoader.java @@ -0,0 +1,70 @@ +package net.minestom.server.extras.selfmodification; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.LinkedList; +import java.util.List; + +public class MinestomExtensionClassLoader extends URLClassLoader { + /** + * Root ClassLoader, everything goes through it before any attempt at loading is done inside this classloader + */ + private final MinestomRootClassLoader root; + private final List children = new LinkedList<>(); + + public MinestomExtensionClassLoader(String name, URL[] urls, MinestomRootClassLoader root) { + super(name, urls, root); + this.root = root; + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + return root.loadClass(name); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + return root.loadClass(name, resolve); + } + + /** + * Assumes the name is not null, nor it does represent a protected class + * @param name + * @return + * @throws ClassNotFoundException if the class is not found inside this classloader + */ + public Class loadClassAsChild(String name, boolean resolve) throws ClassNotFoundException { + for(MinestomExtensionClassLoader child : children) { + try { + Class loaded = child.loadClassAsChild(name, resolve); + return loaded; + } catch (ClassNotFoundException e) { + // move on to next child + } + } + + Class loadedClass = findLoadedClass(name); + if(loadedClass != null) { + return loadedClass; + } + // not in children, attempt load in this classloader + String path = name.replace(".", "/") + ".class"; + InputStream in = getResourceAsStream(path); + if(in == null) { + throw new ClassNotFoundException("Could not load class "+name); + } + try(in) { + byte[] bytes = in.readAllBytes(); + bytes = root.transformBytes(bytes, name); + Class clazz = defineClass(name, bytes, 0, bytes.length); + if(resolve) { + resolveClass(clazz); + } + return clazz; + } catch (IOException e) { + throw new ClassNotFoundException("Could not load class "+name, e); + } + } +} diff --git a/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java b/src/main/java/net/minestom/server/extras/selfmodification/MinestomRootClassLoader.java similarity index 78% rename from src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java rename to src/main/java/net/minestom/server/extras/selfmodification/MinestomRootClassLoader.java index 0ce77b25e..f5cf3e390 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/MinestomOverwriteClassLoader.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/MinestomRootClassLoader.java @@ -22,9 +22,9 @@ import java.util.Set; * Class Loader that can modify class bytecode when they are loaded */ @Slf4j -public class MinestomOverwriteClassLoader extends URLClassLoader { +public class MinestomRootClassLoader extends URLClassLoader { - private static MinestomOverwriteClassLoader INSTANCE; + private static MinestomRootClassLoader INSTANCE; /** * Classes that cannot be loaded/modified by this classloader. @@ -60,17 +60,18 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { // TODO: priorities? private final List modifiers = new LinkedList<>(); + private final List children = new LinkedList<>(); - private MinestomOverwriteClassLoader(ClassLoader parent) { - super("Minestom ClassLoader", extractURLsFromClasspath(), parent); + private MinestomRootClassLoader(ClassLoader parent) { + super("Minestom Root ClassLoader", extractURLsFromClasspath(), parent); asmClassLoader = newChild(new URL[0]); } - public static MinestomOverwriteClassLoader getInstance() { + public static MinestomRootClassLoader getInstance() { if (INSTANCE == null) { - synchronized (MinestomOverwriteClassLoader.class) { + synchronized (MinestomRootClassLoader.class) { if (INSTANCE == null) { - INSTANCE = new MinestomOverwriteClassLoader(MinestomOverwriteClassLoader.class.getClassLoader()); + INSTANCE = new MinestomRootClassLoader(MinestomRootClassLoader.class.getClassLoader()); } } } @@ -116,7 +117,7 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { return super.loadClass(name, resolve); } - return define(name, loadBytes(name, true), resolve); + return define(name, resolve); } catch (Exception ex) { log.trace("Fail to load class, resorting to parent loader: " + name, ex); // fail to load class, let parent load @@ -137,13 +138,29 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { return true; } - private Class define(String name, byte[] bytes, boolean resolve) { - Class defined = defineClass(name, bytes, 0, bytes.length); - log.trace("Loaded with code modifiers: " + name); - if (resolve) { - resolveClass(defined); + private Class define(String name, boolean resolve) throws IOException, ClassNotFoundException { + try { + byte[] bytes = loadBytes(name, true); + Class defined = defineClass(name, bytes, 0, bytes.length); + log.trace("Loaded with code modifiers: " + name); + if (resolve) { + resolveClass(defined); + } + return defined; + } catch (ClassNotFoundException e) { + // could not load inside this classloader, attempt with children + Class defined = null; + for(MinestomExtensionClassLoader subloader : children) { + try { + defined = subloader.loadClassAsChild(name, resolve); + log.trace("Loaded from child {}: {}", subloader, name); + return defined; + } catch (ClassNotFoundException e1) { + // not found inside this child, move on to next + } + } + throw e; } - return defined; } /** @@ -163,9 +180,16 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { if(input == null) { throw new ClassNotFoundException("Could not find resource "+path); } - byte[] bytes = input.readAllBytes(); - if (transform && !isProtected(name)) { - ClassReader reader = new ClassReader(bytes); + byte[] originalBytes = input.readAllBytes(); + if(transform) { + return transformBytes(originalBytes, name); + } + return originalBytes; + } + + byte[] transformBytes(byte[] classBytecode, String name) { + if (!isProtected(name)) { + ClassReader reader = new ClassReader(classBytecode); ClassNode node = new ClassNode(); reader.accept(node, 0); boolean modified = false; @@ -185,11 +209,11 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { } }; node.accept(writer); - bytes = writer.toByteArray(); + classBytecode = writer.toByteArray(); log.trace("Modified " + name); } } - return bytes; + return classBytecode; } // overriden to increase access (from protected to public) @@ -237,4 +261,8 @@ public class MinestomOverwriteClassLoader extends URLClassLoader { public List getModifiers() { return modifiers; } + + public void addChild(MinestomExtensionClassLoader loader) { + children.add(loader); + } } 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 cca2f37d0..87dd2feab 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 @@ -1,6 +1,6 @@ package net.minestom.server.extras.selfmodification.mixins; -import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; +import net.minestom.server.extras.selfmodification.MinestomRootClassLoader; import org.objectweb.asm.ClassReader; import org.objectweb.asm.tree.ClassNode; import org.spongepowered.asm.service.IClassBytecodeProvider; @@ -11,9 +11,9 @@ import java.io.IOException; * Provides class bytecode for Mixin */ public class MinestomBytecodeProvider implements IClassBytecodeProvider { - private final MinestomOverwriteClassLoader classLoader; + private final MinestomRootClassLoader classLoader; - public MinestomBytecodeProvider(MinestomOverwriteClassLoader classLoader) { + public MinestomBytecodeProvider(MinestomRootClassLoader classLoader) { this.classLoader = 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 ae3b98b4f..b661dee4b 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 @@ -1,6 +1,6 @@ package net.minestom.server.extras.selfmodification.mixins; -import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; +import net.minestom.server.extras.selfmodification.MinestomRootClassLoader; import org.spongepowered.asm.service.IClassProvider; import java.net.URL; @@ -9,9 +9,9 @@ import java.net.URL; * Provides classes for Mixin */ public class MinestomClassProvider implements IClassProvider { - private final MinestomOverwriteClassLoader classLoader; + private final MinestomRootClassLoader classLoader; - public MinestomClassProvider(MinestomOverwriteClassLoader classLoader) { + public MinestomClassProvider(MinestomRootClassLoader classLoader) { this.classLoader = classLoader; } 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 ef4c4c760..b68ba7627 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 @@ -1,6 +1,6 @@ package net.minestom.server.extras.selfmodification.mixins; -import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader; +import net.minestom.server.extras.selfmodification.MinestomRootClassLoader; import org.spongepowered.asm.launch.platform.container.ContainerHandleVirtual; import org.spongepowered.asm.launch.platform.container.IContainerHandle; import org.spongepowered.asm.mixin.MixinEnvironment; @@ -13,7 +13,7 @@ import java.util.Collections; public class MixinServiceMinestom extends MixinServiceAbstract { - private final MinestomOverwriteClassLoader classLoader; + private final MinestomRootClassLoader classLoader; private final MinestomClassProvider classProvider; private final MinestomBytecodeProvider bytecodeProvider; private final MixinAuditTrailMinestom auditTrail; @@ -22,7 +22,7 @@ public class MixinServiceMinestom extends MixinServiceAbstract { public MixinServiceMinestom() { INSTANCE = this; - this.classLoader = MinestomOverwriteClassLoader.getInstance(); + this.classLoader = MinestomRootClassLoader.getInstance(); classProvider = new MinestomClassProvider(classLoader); bytecodeProvider = new MinestomBytecodeProvider(classLoader); auditTrail = new MixinAuditTrailMinestom(); From 25cde2cde7d5c280f74d16dd0f943297db059060 Mon Sep 17 00:00:00 2001 From: jglrxavpok Date: Tue, 3 Nov 2020 10:26:31 +0100 Subject: [PATCH 09/13] Mixin modifications between extensions is now possible --- .../extensions/DiscoveredExtension.java | 2 + .../server/extensions/ExtensionManager.java | 256 ++++++++++-------- .../HierarchyClassLoader.java | 36 +++ .../MinestomExtensionClassLoader.java | 58 ++-- .../MinestomRootClassLoader.java | 22 +- .../mixins/MinestomBytecodeProvider.java | 2 +- .../mixins/MixinServiceMinestom.java | 2 +- 7 files changed, 225 insertions(+), 153 deletions(-) create mode 100644 src/main/java/net/minestom/server/extras/selfmodification/HierarchyClassLoader.java diff --git a/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java b/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java index 17d4d424f..4b165367f 100644 --- a/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java +++ b/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java @@ -129,6 +129,8 @@ final class DiscoveredExtension { MISSING_DEPENDENCIES("Missing dependencies, check your logs."), INVALID_NAME("Invalid name."), NO_ENTRYPOINT("No entrypoint specified."), + FAILED_TO_SETUP_CLASSLOADER("Extension classloader could not be setup."), + LOAD_FAILED("Load failed. See logs for more information."), ; private final String message; diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index a52e40a67..81414f757 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -33,7 +33,7 @@ public class ExtensionManager { private final static String INDEV_RESOURCES_FOLDER = "minestom.extension.indevfolder.resources"; private final static Gson GSON = new Gson(); - 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 File dependenciesFolder = new File(extensionFolder, ".libs"); @@ -66,109 +66,134 @@ public class ExtensionManager { List discoveredExtensions = discoverExtensions(); discoveredExtensions = generateLoadOrder(discoveredExtensions); loadDependencies(discoveredExtensions); + // remove invalid extensions + discoveredExtensions.removeIf(ext -> ext.loadStatus != DiscoveredExtension.LoadStatus.LOAD_SUCCESS); + + for(DiscoveredExtension discoveredExtension : discoveredExtensions) { + try { + setupClassLoader(discoveredExtension); + } catch (Exception e) { + discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.FAILED_TO_SETUP_CLASSLOADER; + e.printStackTrace(); + log.error("Failed to load extension {}", discoveredExtension.getName()); + log.error("Failed to load extension", e); + } + } + // remove invalid extensions discoveredExtensions.removeIf(ext -> ext.loadStatus != DiscoveredExtension.LoadStatus.LOAD_SUCCESS); setupCodeModifiers(discoveredExtensions); for (DiscoveredExtension discoveredExtension : discoveredExtensions) { - URLClassLoader loader; - URL[] urls = discoveredExtension.files.toArray(new URL[0]); - // TODO: Only putting each extension into its own classloader prevents code modifications (via code modifiers or mixins) - // TODO: If we want modifications to be possible, we need to add these urls to the current classloader - // TODO: Indeed, without adding the urls, the classloader is not able to load the bytecode of extension classes - // TODO: Whether we want to allow extensions to modify one-another is our choice now. - loader = newClassLoader(discoveredExtension, urls); - - // Create ExtensionDescription (authors, version etc.) - String extensionName = discoveredExtension.getName(); - String mainClass = discoveredExtension.getEntrypoint(); - Extension.ExtensionDescription extensionDescription = new Extension.ExtensionDescription( - extensionName, - discoveredExtension.getVersion(), - Arrays.asList(discoveredExtension.getAuthors()) - ); - - extensionLoaders.put(extensionName.toLowerCase(), loader); - - if (extensions.containsKey(extensionName.toLowerCase())) { - log.error("An extension called '{}' has already been registered.", extensionName); - continue; - } - - Class jarClass; try { - jarClass = Class.forName(mainClass, true, loader); - } catch (ClassNotFoundException e) { - log.error("Could not find main class '{}' in extension '{}'.", mainClass, extensionName, e); - continue; - } - - Class extensionClass; - try { - extensionClass = jarClass.asSubclass(Extension.class); - } catch (ClassCastException e) { - log.error("Main class '{}' in '{}' does not extend the 'Extension' superclass.", mainClass, extensionName, e); - continue; - } - - Constructor constructor; - try { - constructor = extensionClass.getDeclaredConstructor(); - // Let's just make it accessible, plugin creators don't have to make this public. - constructor.setAccessible(true); - } catch (NoSuchMethodException e) { - log.error("Main class '{}' in '{}' does not define a no-args constructor.", mainClass, extensionName, e); - continue; - } - Extension extension = null; - try { - extension = constructor.newInstance(); - } catch (InstantiationException e) { - log.error("Main class '{}' in '{}' cannot be an abstract class.", mainClass, extensionName, e); - continue; - } catch (IllegalAccessException ignored) { - // We made it accessible, should not occur - } catch (InvocationTargetException e) { - log.error( - "While instantiating the main class '{}' in '{}' an exception was thrown.", - mainClass, - extensionName, - e.getTargetException() - ); - continue; - } - - // Set extension description - try { - Field descriptionField = extensionClass.getSuperclass().getDeclaredField("description"); - descriptionField.setAccessible(true); - descriptionField.set(extension, extensionDescription); - } catch (IllegalAccessException e) { - // We made it accessible, should not occur - } catch (NoSuchFieldException e) { - log.error("Main class '{}' in '{}' has no description field.", mainClass, extensionName, e); - continue; - } - - // Set logger - try { - Field loggerField = extensionClass.getSuperclass().getDeclaredField("logger"); - loggerField.setAccessible(true); - loggerField.set(extension, LoggerFactory.getLogger(extensionClass)); - } catch (IllegalAccessException e) { - // We made it accessible, should not occur + attemptSingleLoad(discoveredExtension); + } catch (Exception e) { + discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.LOAD_FAILED; e.printStackTrace(); - } catch (NoSuchFieldException e) { - // This should also not occur (unless someone changed the logger in Extension superclass). - log.error("Main class '{}' in '{}' has no logger field.", mainClass, extensionName, e); + log.error("Failed to load extension {}", discoveredExtension.getName()); + log.error("Failed to load extension", e); } - - extensionList.add(extension); // add to a list, as lists preserve order - extensions.put(extensionName.toLowerCase(), extension); } extensionList = Collections.unmodifiableList(extensionList); } + private void setupClassLoader(DiscoveredExtension discoveredExtension) { + String extensionName = discoveredExtension.getName(); + MinestomExtensionClassLoader loader; + URL[] urls = discoveredExtension.files.toArray(new URL[0]); + loader = newClassLoader(discoveredExtension, urls); + extensionLoaders.put(extensionName.toLowerCase(), loader); + } + + private void attemptSingleLoad(DiscoveredExtension discoveredExtension) { + // Create ExtensionDescription (authors, version etc.) + String extensionName = discoveredExtension.getName(); + String mainClass = discoveredExtension.getEntrypoint(); + Extension.ExtensionDescription extensionDescription = new Extension.ExtensionDescription( + extensionName, + discoveredExtension.getVersion(), + Arrays.asList(discoveredExtension.getAuthors()) + ); + + MinestomExtensionClassLoader loader = extensionLoaders.get(extensionName.toLowerCase()); + + if (extensions.containsKey(extensionName.toLowerCase())) { + log.error("An extension called '{}' has already been registered.", extensionName); + return; + } + + Class jarClass; + try { + jarClass = Class.forName(mainClass, true, loader); + } catch (ClassNotFoundException e) { + log.error("Could not find main class '{}' in extension '{}'.", mainClass, extensionName, e); + return; + } + + Class extensionClass; + try { + extensionClass = jarClass.asSubclass(Extension.class); + } catch (ClassCastException e) { + log.error("Main class '{}' in '{}' does not extend the 'Extension' superclass.", mainClass, extensionName, e); + return; + } + + Constructor constructor; + try { + constructor = extensionClass.getDeclaredConstructor(); + // Let's just make it accessible, plugin creators don't have to make this public. + constructor.setAccessible(true); + } catch (NoSuchMethodException e) { + log.error("Main class '{}' in '{}' does not define a no-args constructor.", mainClass, extensionName, e); + return; + } + Extension extension = null; + try { + extension = constructor.newInstance(); + } catch (InstantiationException e) { + log.error("Main class '{}' in '{}' cannot be an abstract class.", mainClass, extensionName, e); + return; + } catch (IllegalAccessException ignored) { + // We made it accessible, should not occur + } catch (InvocationTargetException e) { + log.error( + "While instantiating the main class '{}' in '{}' an exception was thrown.", + mainClass, + extensionName, + e.getTargetException() + ); + return; + } + + // Set extension description + try { + Field descriptionField = extensionClass.getSuperclass().getDeclaredField("description"); + descriptionField.setAccessible(true); + descriptionField.set(extension, extensionDescription); + } catch (IllegalAccessException e) { + // We made it accessible, should not occur + } catch (NoSuchFieldException e) { + log.error("Main class '{}' in '{}' has no description field.", mainClass, extensionName, e); + return; + } + + // Set logger + try { + Field loggerField = extensionClass.getSuperclass().getDeclaredField("logger"); + loggerField.setAccessible(true); + loggerField.set(extension, LoggerFactory.getLogger(extensionClass)); + } catch (IllegalAccessException e) { + // We made it accessible, should not occur + e.printStackTrace(); + } catch (NoSuchFieldException e) { + // This should also not occur (unless someone changed the logger in Extension superclass). + log.error("Main class '{}' in '{}' has no logger field.", mainClass, extensionName, e); + } + + extensionList.add(extension); // add to a list, as lists preserve order + extensions.put(extensionName.toLowerCase(), extension); + } + @NotNull private List discoverExtensions() { List extensions = new LinkedList<>(); @@ -334,42 +359,41 @@ public class ExtensionManager { } } - // TODO: remove if extensions cannot modify one-another - // TODO: use if they can - private void injectIntoClasspath(URL dependency, DiscoveredExtension extension) { - final ClassLoader cl = getClass().getClassLoader(); - if (!(cl instanceof URLClassLoader)) { - throw new IllegalStateException("Current class loader is not a URLClassLoader, but " + cl + ". This prevents adding URLs into the classpath at runtime."); - } - if(cl instanceof MinestomRootClassLoader) { - ((MinestomRootClassLoader) cl).addURL(dependency); // no reflection warnings for us! - } else { - try { - Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); - addURL.setAccessible(true); - addURL.invoke(cl, dependency); - } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - throw new RuntimeException("Failed to inject URL " + dependency + " into classpath. From extension " + extension.getName(), e); - } - } - } - private void addDependencyFile(URL dependency, DiscoveredExtension extension) { extension.files.add(dependency); log.trace("Added dependency {} to extension {} classpath", dependency.toExternalForm(), extension.getName()); } /** - * Loads a URL into the classpath. + * Creates a new class loader for the given extension. + * Will add the new loader as a child of all its dependencies' loaders. * * @param urls {@link URL} (usually a JAR) that should be loaded. */ @NotNull - public URLClassLoader newClassLoader(@NotNull DiscoveredExtension extension, @NotNull URL[] urls) { + public MinestomExtensionClassLoader newClassLoader(@NotNull DiscoveredExtension extension, @NotNull URL[] urls) { MinestomRootClassLoader root = MinestomRootClassLoader.getInstance(); MinestomExtensionClassLoader loader = new MinestomExtensionClassLoader(extension.getName(), urls, root); - // TODO: tree structure - root.addChild(loader); + if(extension.getDependencies().length == 0) { + // orphaned extension, we can insert it directly + root.addChild(loader); + } else { + // we need to keep track that it has actually been inserted + // even though it should always be (due to the order in which extensions are loaders), it is an additional layer of """security""" + boolean foundOne = false; + for(String dependency : extension.getDependencies()) { + if(extensionLoaders.containsKey(dependency.toLowerCase())) { + MinestomExtensionClassLoader parentLoader = extensionLoaders.get(dependency.toLowerCase()); + parentLoader.addChild(loader); + foundOne = true; + } + } + + if(!foundOne) { + log.error("Could not load extension {}, could not find any parent inside classloader hierarchy.", extension.getName()); + throw new RuntimeException("Could not load extension "+extension.getName()+", could not find any parent inside classloader hierarchy."); + } + } return loader; } diff --git a/src/main/java/net/minestom/server/extras/selfmodification/HierarchyClassLoader.java b/src/main/java/net/minestom/server/extras/selfmodification/HierarchyClassLoader.java new file mode 100644 index 000000000..5dc54ae6a --- /dev/null +++ b/src/main/java/net/minestom/server/extras/selfmodification/HierarchyClassLoader.java @@ -0,0 +1,36 @@ +package net.minestom.server.extras.selfmodification; + +import org.jetbrains.annotations.NotNull; + +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.LinkedList; +import java.util.List; + +/** + * Classloader part of a hierarchy of classloader + */ +public abstract class HierarchyClassLoader extends URLClassLoader { + protected final List children = new LinkedList<>(); + + public HierarchyClassLoader(String name, URL[] urls, ClassLoader parent) { + super(name, urls, parent); + } + + public void addChild(@NotNull MinestomExtensionClassLoader loader) { + children.add(loader); + } + + public InputStream getResourceAsStreamWithChildren(String name) { + InputStream in = getResourceAsStream(name); + if(in != null) return in; + + for(MinestomExtensionClassLoader child : children) { + InputStream childInput = child.getResourceAsStreamWithChildren(name); + if(childInput != null) + return childInput; + } + return null; + } +} diff --git a/src/main/java/net/minestom/server/extras/selfmodification/MinestomExtensionClassLoader.java b/src/main/java/net/minestom/server/extras/selfmodification/MinestomExtensionClassLoader.java index 5bd6bd804..ca3008895 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/MinestomExtensionClassLoader.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/MinestomExtensionClassLoader.java @@ -3,16 +3,12 @@ package net.minestom.server.extras.selfmodification; import java.io.IOException; import java.io.InputStream; import java.net.URL; -import java.net.URLClassLoader; -import java.util.LinkedList; -import java.util.List; -public class MinestomExtensionClassLoader extends URLClassLoader { +public class MinestomExtensionClassLoader extends HierarchyClassLoader { /** * Root ClassLoader, everything goes through it before any attempt at loading is done inside this classloader */ private final MinestomRootClassLoader root; - private final List children = new LinkedList<>(); public MinestomExtensionClassLoader(String name, URL[] urls, MinestomRootClassLoader root) { super(name, urls, root); @@ -36,35 +32,39 @@ public class MinestomExtensionClassLoader extends URLClassLoader { * @throws ClassNotFoundException if the class is not found inside this classloader */ public Class loadClassAsChild(String name, boolean resolve) throws ClassNotFoundException { - for(MinestomExtensionClassLoader child : children) { - try { - Class loaded = child.loadClassAsChild(name, resolve); - return loaded; - } catch (ClassNotFoundException e) { - // move on to next child - } - } - Class loadedClass = findLoadedClass(name); if(loadedClass != null) { return loadedClass; } - // not in children, attempt load in this classloader - String path = name.replace(".", "/") + ".class"; - InputStream in = getResourceAsStream(path); - if(in == null) { - throw new ClassNotFoundException("Could not load class "+name); - } - try(in) { - byte[] bytes = in.readAllBytes(); - bytes = root.transformBytes(bytes, name); - Class clazz = defineClass(name, bytes, 0, bytes.length); - if(resolve) { - resolveClass(clazz); + + try { + // not in children, attempt load in this classloader + String path = name.replace(".", "/") + ".class"; + InputStream in = getResourceAsStream(path); + if (in == null) { + throw new ClassNotFoundException("Could not load class " + name); } - return clazz; - } catch (IOException e) { - throw new ClassNotFoundException("Could not load class "+name, e); + try (in) { + byte[] bytes = in.readAllBytes(); + bytes = root.transformBytes(bytes, name); + Class clazz = defineClass(name, bytes, 0, bytes.length); + if (resolve) { + resolveClass(clazz); + } + return clazz; + } catch (IOException e) { + throw new ClassNotFoundException("Could not load class " + name, e); + } + } catch (ClassNotFoundException e) { + for(MinestomExtensionClassLoader child : children) { + try { + Class loaded = child.loadClassAsChild(name, resolve); + return loaded; + } catch (ClassNotFoundException e1) { + // move on to next child + } + } + throw e; } } } diff --git a/src/main/java/net/minestom/server/extras/selfmodification/MinestomRootClassLoader.java b/src/main/java/net/minestom/server/extras/selfmodification/MinestomRootClassLoader.java index f5cf3e390..d3eceb6e0 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/MinestomRootClassLoader.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/MinestomRootClassLoader.java @@ -22,7 +22,7 @@ import java.util.Set; * Class Loader that can modify class bytecode when they are loaded */ @Slf4j -public class MinestomRootClassLoader extends URLClassLoader { +public class MinestomRootClassLoader extends HierarchyClassLoader { private static MinestomRootClassLoader INSTANCE; @@ -60,7 +60,6 @@ public class MinestomRootClassLoader extends URLClassLoader { // TODO: priorities? private final List modifiers = new LinkedList<>(); - private final List children = new LinkedList<>(); private MinestomRootClassLoader(ClassLoader parent) { super("Minestom Root ClassLoader", extractURLsFromClasspath(), parent); @@ -187,6 +186,21 @@ public class MinestomRootClassLoader extends URLClassLoader { return originalBytes; } + public byte[] loadBytesWithChildren(String name, boolean transform) throws IOException, ClassNotFoundException { + if (name == null) + throw new ClassNotFoundException(); + String path = name.replace(".", "/") + ".class"; + InputStream input = getResourceAsStreamWithChildren(path); + if(input == null) { + throw new ClassNotFoundException("Could not find resource "+path); + } + byte[] originalBytes = input.readAllBytes(); + if(transform) { + return transformBytes(originalBytes, name); + } + return originalBytes; + } + byte[] transformBytes(byte[] classBytecode, String name) { if (!isProtected(name)) { ClassReader reader = new ClassReader(classBytecode); @@ -261,8 +275,4 @@ public class MinestomRootClassLoader extends URLClassLoader { public List getModifiers() { return modifiers; } - - public void addChild(MinestomExtensionClassLoader loader) { - children.add(loader); - } } 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 87dd2feab..7c7e718eb 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 @@ -26,7 +26,7 @@ public class MinestomBytecodeProvider implements IClassBytecodeProvider { ClassNode node = new ClassNode(); ClassReader reader; try { - reader = new ClassReader(classLoader.loadBytes(name, transform)); + reader = new ClassReader(classLoader.loadBytesWithChildren(name, transform)); } catch (IOException e) { throw new ClassNotFoundException("Could not load ClassNode with name " + name, e); } 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 b68ba7627..5af794698 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 @@ -65,7 +65,7 @@ public class MixinServiceMinestom extends MixinServiceAbstract { @Override public InputStream getResourceAsStream(String name) { - return classLoader.getResourceAsStream(name); + return classLoader.getResourceAsStreamWithChildren(name); } @Override From 925f5fa614ad6c38056fe23e02bb48b013409eec Mon Sep 17 00:00:00 2001 From: jglrxavpok Date: Tue, 3 Nov 2020 21:26:46 +0100 Subject: [PATCH 10/13] Reloading extensions --- .../extensions/DiscoveredExtension.java | 11 + .../minestom/server/extensions/Extension.java | 24 ++- .../server/extensions/ExtensionManager.java | 194 +++++++++++++++--- .../HierarchyClassLoader.java | 5 + src/test/java/demo/Main.java | 1 + .../demo/commands/ReloadExtensionCommand.java | 77 +++++++ .../java/testextension/TestDemoLauncher.java | 11 + 7 files changed, 289 insertions(+), 34 deletions(-) create mode 100644 src/test/java/demo/commands/ReloadExtensionCommand.java create mode 100644 src/test/java/testextension/TestDemoLauncher.java diff --git a/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java b/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java index 4b165367f..a78bd990a 100644 --- a/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java +++ b/src/main/java/net/minestom/server/extensions/DiscoveredExtension.java @@ -2,6 +2,7 @@ package net.minestom.server.extensions; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.File; import java.net.URL; @@ -21,6 +22,7 @@ final class DiscoveredExtension { private ExternalDependencies externalDependencies; transient List files = new LinkedList<>(); transient LoadStatus loadStatus = LoadStatus.LOAD_SUCCESS; + transient private File originalJar; @NotNull public String getName() { @@ -65,6 +67,15 @@ final class DiscoveredExtension { return externalDependencies; } + void setOriginalJar(@Nullable File file) { + originalJar = file; + } + + @Nullable + File getOriginalJar() { + return originalJar; + } + static void verifyIntegrity(@NotNull DiscoveredExtension extension) { if (extension.name == null) { StringBuilder fileList = new StringBuilder(); diff --git a/src/main/java/net/minestom/server/extensions/Extension.java b/src/main/java/net/minestom/server/extensions/Extension.java index 6a50779b9..82caa6063 100644 --- a/src/main/java/net/minestom/server/extensions/Extension.java +++ b/src/main/java/net/minestom/server/extensions/Extension.java @@ -3,6 +3,7 @@ package net.minestom.server.extensions; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; +import java.util.ArrayList; import java.util.List; public abstract class Extension { @@ -37,6 +38,13 @@ public abstract class Extension { } + /** + * Called after postTerminate when reloading an extension + */ + public void unload() { + + } + @NotNull public ExtensionDescription getDescription() { return description; @@ -47,15 +55,19 @@ public abstract class Extension { return logger; } + public static class ExtensionDescription { private final String name; private final String version; private final List authors; + private final List dependents = new ArrayList<>(); + private final DiscoveredExtension origin; - ExtensionDescription(@NotNull String name, @NotNull String version, @NotNull List authors) { + ExtensionDescription(@NotNull String name, @NotNull String version, @NotNull List authors, @NotNull DiscoveredExtension origin) { this.name = name; this.version = version; this.authors = authors; + this.origin = origin; } @NotNull @@ -72,5 +84,15 @@ public abstract class Extension { public List getAuthors() { return authors; } + + @NotNull + public List getDependents() { + return dependents; + } + + @NotNull + DiscoveredExtension getOrigin() { + return origin; + } } } diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index 81414f757..2e8144276 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -39,8 +39,8 @@ public class ExtensionManager { private final File dependenciesFolder = new File(extensionFolder, ".libs"); private boolean loaded; - // not final to add to it, and then make it immutable - private List extensionList = new ArrayList<>(); + private final List extensionList = new ArrayList<>(); + private final List immutableExtensionListView = Collections.unmodifiableList(extensionList); public ExtensionManager() { } @@ -94,7 +94,6 @@ public class ExtensionManager { log.error("Failed to load extension", e); } } - extensionList = Collections.unmodifiableList(extensionList); } private void setupClassLoader(DiscoveredExtension discoveredExtension) { @@ -105,21 +104,22 @@ public class ExtensionManager { extensionLoaders.put(extensionName.toLowerCase(), loader); } - private void attemptSingleLoad(DiscoveredExtension discoveredExtension) { + private Extension attemptSingleLoad(DiscoveredExtension discoveredExtension) { // Create ExtensionDescription (authors, version etc.) String extensionName = discoveredExtension.getName(); String mainClass = discoveredExtension.getEntrypoint(); Extension.ExtensionDescription extensionDescription = new Extension.ExtensionDescription( extensionName, discoveredExtension.getVersion(), - Arrays.asList(discoveredExtension.getAuthors()) + Arrays.asList(discoveredExtension.getAuthors()), + discoveredExtension ); MinestomExtensionClassLoader loader = extensionLoaders.get(extensionName.toLowerCase()); if (extensions.containsKey(extensionName.toLowerCase())) { log.error("An extension called '{}' has already been registered.", extensionName); - return; + return null; } Class jarClass; @@ -127,7 +127,7 @@ public class ExtensionManager { jarClass = Class.forName(mainClass, true, loader); } catch (ClassNotFoundException e) { log.error("Could not find main class '{}' in extension '{}'.", mainClass, extensionName, e); - return; + return null; } Class extensionClass; @@ -135,7 +135,7 @@ public class ExtensionManager { extensionClass = jarClass.asSubclass(Extension.class); } catch (ClassCastException e) { log.error("Main class '{}' in '{}' does not extend the 'Extension' superclass.", mainClass, extensionName, e); - return; + return null; } Constructor constructor; @@ -145,14 +145,14 @@ public class ExtensionManager { constructor.setAccessible(true); } catch (NoSuchMethodException e) { log.error("Main class '{}' in '{}' does not define a no-args constructor.", mainClass, extensionName, e); - return; + return null; } Extension extension = null; try { extension = constructor.newInstance(); } catch (InstantiationException e) { log.error("Main class '{}' in '{}' cannot be an abstract class.", mainClass, extensionName, e); - return; + return null; } catch (IllegalAccessException ignored) { // We made it accessible, should not occur } catch (InvocationTargetException e) { @@ -162,7 +162,7 @@ public class ExtensionManager { extensionName, e.getTargetException() ); - return; + return null; } // Set extension description @@ -174,7 +174,7 @@ public class ExtensionManager { // We made it accessible, should not occur } catch (NoSuchFieldException e) { log.error("Main class '{}' in '{}' has no description field.", mainClass, extensionName, e); - return; + return null; } // Set logger @@ -190,8 +190,20 @@ public class ExtensionManager { log.error("Main class '{}' in '{}' has no logger field.", mainClass, extensionName, e); } + // add dependents to pre-existing extensions, so that they can easily be found during reloading + for(String dependency : discoveredExtension.getDependencies()) { + Extension dep = extensions.get(dependency.toLowerCase()); + if(dep == null) { + log.warn("Dependency {} of {} is null? This means the extension has been loaded without its dependency, which could cause issues later.", dependency, discoveredExtension.getName()); + } else { + dep.getDescription().getDependents().add(discoveredExtension.getName()); + } + } + extensionList.add(extension); // add to a list, as lists preserve order extensions.put(extensionName.toLowerCase(), extension); + + return extension; } @NotNull @@ -204,20 +216,9 @@ public class ExtensionManager { if (!file.getName().endsWith(".jar")) { continue; } - try (ZipFile f = new ZipFile(file); - InputStreamReader reader = new InputStreamReader(f.getInputStream(f.getEntry("extension.json")))) { - - DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class); - extension.files.add(file.toURI().toURL()); - - // Verify integrity and ensure defaults - DiscoveredExtension.verifyIntegrity(extension); - - if (extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) { - extensions.add(extension); - } - } catch (IOException e) { - e.printStackTrace(); + DiscoveredExtension extension = discoverFromJar(file); + if(extension != null && extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) { + extensions.add(extension); } } @@ -244,6 +245,24 @@ public class ExtensionManager { return extensions; } + private DiscoveredExtension discoverFromJar(File file) { + try (ZipFile f = new ZipFile(file); + InputStreamReader reader = new InputStreamReader(f.getInputStream(f.getEntry("extension.json")))) { + + DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class); + extension.setOriginalJar(file); + extension.files.add(file.toURI().toURL()); + + // Verify integrity and ensure defaults + DiscoveredExtension.verifyIntegrity(extension); + + return extension; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + private List generateLoadOrder(List discoveredExtensions) { // Do some mapping so we can map strings to extensions. Map extensionMap = new HashMap<>(); @@ -258,10 +277,15 @@ public class ExtensionManager { DiscoveredExtension dependencyExtension = extensionMap.get(dependencyName.toLowerCase()); // Specifies an extension we don't have. if (dependencyExtension == null) { - log.error("Extension {} requires an extension called {}.", discoveredExtension.getName(), dependencyName); - log.error("However the extension {} could not be found.", dependencyName); - log.error("Therefore {} will not be loaded.", dependencyName); - discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.MISSING_DEPENDENCIES; + // attempt to see if it is not already loaded (happens with dynamic (re)loading) + if(extensions.containsKey(dependencyName.toLowerCase())) { + return extensions.get(dependencyName.toLowerCase()).getDescription().getOrigin(); + } else { + log.error("Extension {} requires an extension called {}.", discoveredExtension.getName(), dependencyName); + log.error("However the extension {} could not be found.", dependencyName); + log.error("Therefore {} will not be loaded.", discoveredExtension.getName()); + discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.MISSING_DEPENDENCIES; + } } // This will return null for an unknown-extension return extensionMap.get(dependencyName.toLowerCase()); @@ -283,7 +307,7 @@ public class ExtensionManager { List>> loadableExtensions; // While there are entries with no more elements (no more dependencies) while (!( - loadableExtensions = dependencyMap.entrySet().stream().filter(entry -> entry.getValue().isEmpty()).collect(Collectors.toList()) + loadableExtensions = dependencyMap.entrySet().stream().filter(entry -> areAllDependenciesLoaded(entry.getValue())).collect(Collectors.toList()) ).isEmpty() ) { // Get all "loadable" (not actually being loaded!) extensions and put them in the sorted list. @@ -314,8 +338,14 @@ public class ExtensionManager { return sortedList; } + private boolean areAllDependenciesLoaded(List dependencies) { + return dependencies.isEmpty() || dependencies.stream().allMatch(ext -> extensions.containsKey(ext.getName().toLowerCase())); + } + private void loadDependencies(List extensions) { - ExtensionDependencyResolver extensionDependencyResolver = new ExtensionDependencyResolver(extensions); + List allLoadedExtensions = new LinkedList<>(extensions); + extensionList.stream().map(ext -> ext.getDescription().getOrigin()).forEach(allLoadedExtensions::add); + ExtensionDependencyResolver extensionDependencyResolver = new ExtensionDependencyResolver(allLoadedExtensions); for (DiscoveredExtension ext : extensions) { try { DependencyGetter getter = new DependencyGetter(); @@ -404,7 +434,7 @@ public class ExtensionManager { @NotNull public List getExtensions() { - return extensionList; + return immutableExtensionListView; } @Nullable @@ -445,4 +475,102 @@ public class ExtensionManager { } log.info("Done loading code modifiers."); } + + private void unload(Extension ext) { + ext.preTerminate(); + ext.terminate(); + ext.postTerminate(); + ext.unload(); + + // remove as dependent of other extensions + // this avoids issues where a dependent extension fails to reload, and prevents the base extension to reload too + for(Extension e : extensionList) { + e.getDescription().getDependents().remove(ext.getDescription().getName()); + } + + String id = ext.getDescription().getName().toLowerCase(); + // remove from loaded extensions + extensions.remove(id); + extensionList.remove(ext); + + // remove class loader, required to reload the classes + MinestomExtensionClassLoader classloader = extensionLoaders.remove(id); + MinestomRootClassLoader.getInstance().removeChildInHierarchy(classloader); + } + + public void reload(String extensionName) { + Extension ext = extensions.get(extensionName.toLowerCase()); + if(ext == null) { + throw new IllegalArgumentException("Extension "+extensionName+" is not currently loaded."); + } + + File originalJar = ext.getDescription().getOrigin().getOriginalJar(); + if(originalJar == null) { + log.error("Cannot reload extension {} that is not from a .jar file!", extensionName); + return; + } + + log.info("Reload extension {} from jar file {}", extensionName, originalJar.getAbsolutePath()); + List dependents = new LinkedList<>(ext.getDescription().getDependents()); // copy dependents list + List originalJarsOfDependents = new LinkedList<>(); + + for(String dependentID : dependents) { + Extension dependentExt = extensions.get(dependentID.toLowerCase()); + File dependentOriginalJar = dependentExt.getDescription().getOrigin().getOriginalJar(); + originalJarsOfDependents.add(dependentOriginalJar); + if(dependentOriginalJar == null) { + log.error("Cannot reload extension {} that is not from a .jar file!", dependentID); + return; + } + + log.info("Unloading dependent extension {} (because it depends on {})", dependentID, extensionName); + unload(dependentExt); + } + + log.info("Unloading extension {}", extensionName); + unload(ext); + + // ext and its dependents should no longer be referenced from now on + + // rediscover extension to reload. We allow dependency changes, so we need to fully reload it + List extensionsToReload = new LinkedList<>(); + log.info("Rediscover extension {} from jar {}", extensionName, originalJar.getAbsolutePath()); + DiscoveredExtension rediscoveredExtension = discoverFromJar(originalJar); + extensionsToReload.add(rediscoveredExtension); + + for(File dependentJar : originalJarsOfDependents) { + // rediscover dependent extension to reload + log.info("Rediscover dependent extension (depends on {}) from jar {}", extensionName, dependentJar.getAbsolutePath()); + extensionsToReload.add(discoverFromJar(dependentJar)); + } + + // ensure correct order of dependencies + log.debug("Reorder extensions to reload to ensure proper load order"); + extensionsToReload = generateLoadOrder(extensionsToReload); + loadDependencies(extensionsToReload); + + // setup new classloaders for the extensions to reload + for(DiscoveredExtension toReload : extensionsToReload) { + log.debug("Setting up classloader for extension {}", toReload.getName()); + setupClassLoader(toReload); + } + + // setup code modifiers for these extensions + // TODO: it is possible the new modifiers cannot be applied (because the targeted classes are already loaded), should we issue a warning? + setupCodeModifiers(extensionsToReload); + + List newExtensions = new LinkedList<>(); + for(DiscoveredExtension toReload : extensionsToReload) { + // reload extensions + log.info("Actually load extension {}", toReload.getName()); + Extension loadedExtension = attemptSingleLoad(toReload); + newExtensions.add(loadedExtension); + } + + log.info("Reload complete, refiring preinit, init and then postinit callbacks"); + // retrigger preinit, init and postinit + newExtensions.forEach(Extension::preInitialize); + newExtensions.forEach(Extension::initialize); + newExtensions.forEach(Extension::postInitialize); + } } diff --git a/src/main/java/net/minestom/server/extras/selfmodification/HierarchyClassLoader.java b/src/main/java/net/minestom/server/extras/selfmodification/HierarchyClassLoader.java index 5dc54ae6a..1c5ad5c34 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/HierarchyClassLoader.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/HierarchyClassLoader.java @@ -33,4 +33,9 @@ public abstract class HierarchyClassLoader extends URLClassLoader { } return null; } + + public void removeChildInHierarchy(MinestomExtensionClassLoader child) { + children.remove(child); + children.forEach(c -> c.removeChildInHierarchy(child)); + } } diff --git a/src/test/java/demo/Main.java b/src/test/java/demo/Main.java index 36c883119..35eb6dd43 100644 --- a/src/test/java/demo/Main.java +++ b/src/test/java/demo/Main.java @@ -35,6 +35,7 @@ public class Main { commandManager.register(new DimensionCommand()); commandManager.register(new ShutdownCommand()); commandManager.register(new TeleportCommand()); + commandManager.register(new ReloadExtensionCommand()); StorageManager storageManager = MinecraftServer.getStorageManager(); diff --git a/src/test/java/demo/commands/ReloadExtensionCommand.java b/src/test/java/demo/commands/ReloadExtensionCommand.java new file mode 100644 index 000000000..f9f5d8c17 --- /dev/null +++ b/src/test/java/demo/commands/ReloadExtensionCommand.java @@ -0,0 +1,77 @@ +package demo.commands; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.command.CommandSender; +import net.minestom.server.command.builder.Arguments; +import net.minestom.server.command.builder.Command; +import net.minestom.server.command.builder.arguments.Argument; +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.extensions.Extension; +import net.minestom.server.extensions.ExtensionManager; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public class ReloadExtensionCommand extends Command { + public ReloadExtensionCommand() { + super("reload"); + + setDefaultExecutor(this::usage); + + Argument extension = ArgumentType.DynamicStringArray("extensionName"); + + setArgumentCallback(this::gameModeCallback, extension); + + addSyntax(this::execute, extension); + } + + private void usage(CommandSender sender, Arguments arguments) { + sender.sendMessage("Usage: /reload "); + } + + private void execute(CommandSender sender, Arguments arguments) { + String name = join(arguments.getStringArray("extensionName")); + sender.sendMessage("extensionName = "+name+"...."); + + ExtensionManager extensionManager = MinecraftServer.getExtensionManager(); + Extension ext = extensionManager.getExtension(name); + if(ext != null) { + try { + extensionManager.reload(name); + } catch (Throwable t) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + t.printStackTrace(); + t.printStackTrace(new PrintStream(baos)); + baos.flush(); + baos.close(); + String contents = new String(baos.toByteArray(), StandardCharsets.UTF_8); + contents.lines().forEach(sender::sendMessage); + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + sender.sendMessage("Extension '"+name+"' does not exist."); + } + } + + private void gameModeCallback(CommandSender sender, String extension, int error) { + sender.sendMessage("'" + extension + "' is not a valid extension name!"); + } + + private String join(String[] extensionNameParts) { + StringBuilder b = new StringBuilder(); + for (int i = 0; i < extensionNameParts.length; i++) { + String s = extensionNameParts[i]; + if(i != 0) { + b.append(" "); + } + b.append(s); + } + return b.toString(); + } +} diff --git a/src/test/java/testextension/TestDemoLauncher.java b/src/test/java/testextension/TestDemoLauncher.java new file mode 100644 index 000000000..775d2daa4 --- /dev/null +++ b/src/test/java/testextension/TestDemoLauncher.java @@ -0,0 +1,11 @@ +package testextension; + +import net.minestom.server.Bootstrap; + +public class TestDemoLauncher { + + public static void main(String[] args) { + Bootstrap.bootstrap("demo.Main", args); + } + +} From d83bec4732dacb1ad4e647743d4b1be418c5b64f Mon Sep 17 00:00:00 2001 From: jglrxavpok Date: Fri, 6 Nov 2020 16:03:08 +0100 Subject: [PATCH 11/13] Let users change extension jar after unload, then load it again --- .../server/command/CommandManager.java | 10 +++ .../command/builder/CommandDispatcher.java | 8 ++ .../server/extensions/ExtensionManager.java | 63 ++++++++++++--- .../MinestomExtensionClassLoader.java | 6 ++ src/test/java/demo/Main.java | 2 + .../demo/commands/LoadExtensionCommand.java | 70 +++++++++++++++++ .../demo/commands/UnloadExtensionCommand.java | 76 +++++++++++++++++++ 7 files changed, 224 insertions(+), 11 deletions(-) create mode 100644 src/test/java/demo/commands/LoadExtensionCommand.java create mode 100644 src/test/java/demo/commands/UnloadExtensionCommand.java diff --git a/src/main/java/net/minestom/server/command/CommandManager.java b/src/main/java/net/minestom/server/command/CommandManager.java index c7454e99a..2c351e9a5 100644 --- a/src/main/java/net/minestom/server/command/CommandManager.java +++ b/src/main/java/net/minestom/server/command/CommandManager.java @@ -81,6 +81,16 @@ public final class CommandManager { this.dispatcher.register(command); } + /** + * Removes a command from the currently registered commands. + * Does nothing if the command was not registered before + * + * @param command the command to remove + */ + public void unregister(@NotNull Command command) { + this.dispatcher.unregister(command); + } + /** * Gets the {@link Command} registered by {@link #register(Command)}. * diff --git a/src/main/java/net/minestom/server/command/builder/CommandDispatcher.java b/src/main/java/net/minestom/server/command/builder/CommandDispatcher.java index 46b58c8e0..47ffdf0c1 100644 --- a/src/main/java/net/minestom/server/command/builder/CommandDispatcher.java +++ b/src/main/java/net/minestom/server/command/builder/CommandDispatcher.java @@ -23,6 +23,14 @@ public class CommandDispatcher { this.commands.add(command); } + public void unregister(Command command) { + commandMap.remove(command.getName().toLowerCase()); + for(String alias : command.getAliases()) { + this.commandMap.remove(alias.toLowerCase()); + } + commands.remove(command); + } + /** * Parse the given command * diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index 2e8144276..65a8dc338 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -12,10 +12,7 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.LoggerFactory; import org.spongepowered.asm.mixin.Mixins; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; +import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -495,6 +492,12 @@ public class ExtensionManager { // remove class loader, required to reload the classes MinestomExtensionClassLoader classloader = extensionLoaders.remove(id); + try { + // close resources + classloader.close(); + } catch (IOException e) { + e.printStackTrace(); + } MinestomRootClassLoader.getInstance().removeChildInHierarchy(classloader); } @@ -530,6 +533,8 @@ public class ExtensionManager { log.info("Unloading extension {}", extensionName); unload(ext); + System.gc(); + // ext and its dependents should no longer be referenced from now on // rediscover extension to reload. We allow dependency changes, so we need to fully reload it @@ -545,32 +550,68 @@ public class ExtensionManager { } // ensure correct order of dependencies - log.debug("Reorder extensions to reload to ensure proper load order"); - extensionsToReload = generateLoadOrder(extensionsToReload); - loadDependencies(extensionsToReload); + loadExtensionList(extensionsToReload); + } + + public void loadDynamicExtension(File jarFile) throws FileNotFoundException { + if(!jarFile.exists()) { + throw new FileNotFoundException("File '"+jarFile.getAbsolutePath()+"' does not exists. Cannot load extension."); + } + + log.info("Discover dynamic extension from jar {}", jarFile.getAbsolutePath()); + DiscoveredExtension discoveredExtension = discoverFromJar(jarFile); + List extensionsToLoad = Collections.singletonList(discoveredExtension); + loadExtensionList(extensionsToLoad); + } + + private void loadExtensionList(List extensionsToLoad) { + // ensure correct order of dependencies + log.debug("Reorder extensions to ensure proper load order"); + extensionsToLoad = generateLoadOrder(extensionsToLoad); + loadDependencies(extensionsToLoad); // setup new classloaders for the extensions to reload - for(DiscoveredExtension toReload : extensionsToReload) { + for (DiscoveredExtension toReload : extensionsToLoad) { log.debug("Setting up classloader for extension {}", toReload.getName()); setupClassLoader(toReload); } // setup code modifiers for these extensions // TODO: it is possible the new modifiers cannot be applied (because the targeted classes are already loaded), should we issue a warning? - setupCodeModifiers(extensionsToReload); + setupCodeModifiers(extensionsToLoad); List newExtensions = new LinkedList<>(); - for(DiscoveredExtension toReload : extensionsToReload) { + for (DiscoveredExtension toReload : extensionsToLoad) { // reload extensions log.info("Actually load extension {}", toReload.getName()); Extension loadedExtension = attemptSingleLoad(toReload); newExtensions.add(loadedExtension); } - log.info("Reload complete, refiring preinit, init and then postinit callbacks"); + log.info("Load complete, firing preinit, init and then postinit callbacks"); // retrigger preinit, init and postinit newExtensions.forEach(Extension::preInitialize); newExtensions.forEach(Extension::initialize); newExtensions.forEach(Extension::postInitialize); } + + public void unloadExtension(String extensionName) { + Extension ext = extensions.get(extensionName.toLowerCase()); + if(ext == null) { + throw new IllegalArgumentException("Extension "+extensionName+" is not currently loaded."); + } + List dependents = new LinkedList<>(ext.getDescription().getDependents()); // copy dependents list + + for(String dependentID : dependents) { + Extension dependentExt = extensions.get(dependentID.toLowerCase()); + log.info("Unloading dependent extension {} (because it depends on {})", dependentID, extensionName); + unload(dependentExt); + } + + log.info("Unloading extension {}", extensionName); + unload(ext); + + // call GC to try to get rid of classes and classloader + System.gc(); + } } diff --git a/src/main/java/net/minestom/server/extras/selfmodification/MinestomExtensionClassLoader.java b/src/main/java/net/minestom/server/extras/selfmodification/MinestomExtensionClassLoader.java index ca3008895..7df07b288 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/MinestomExtensionClassLoader.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/MinestomExtensionClassLoader.java @@ -67,4 +67,10 @@ public class MinestomExtensionClassLoader extends HierarchyClassLoader { throw e; } } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + System.err.println("Class loader "+getName()+" finalized."); + } } diff --git a/src/test/java/demo/Main.java b/src/test/java/demo/Main.java index 35eb6dd43..5e934678d 100644 --- a/src/test/java/demo/Main.java +++ b/src/test/java/demo/Main.java @@ -36,6 +36,8 @@ public class Main { commandManager.register(new ShutdownCommand()); commandManager.register(new TeleportCommand()); commandManager.register(new ReloadExtensionCommand()); + commandManager.register(new UnloadExtensionCommand()); + commandManager.register(new LoadExtensionCommand()); StorageManager storageManager = MinecraftServer.getStorageManager(); diff --git a/src/test/java/demo/commands/LoadExtensionCommand.java b/src/test/java/demo/commands/LoadExtensionCommand.java new file mode 100644 index 000000000..d5750e6be --- /dev/null +++ b/src/test/java/demo/commands/LoadExtensionCommand.java @@ -0,0 +1,70 @@ +package demo.commands; + +import lombok.extern.slf4j.Slf4j; +import net.minestom.server.MinecraftServer; +import net.minestom.server.command.CommandSender; +import net.minestom.server.command.builder.Arguments; +import net.minestom.server.command.builder.Command; +import net.minestom.server.command.builder.arguments.Argument; +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.extensions.Extension; +import net.minestom.server.extensions.ExtensionManager; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; + +public class LoadExtensionCommand extends Command { + public LoadExtensionCommand() { + super("load"); + + setDefaultExecutor(this::usage); + + Argument extension = ArgumentType.DynamicStringArray("extensionName"); + + setArgumentCallback(this::gameModeCallback, extension); + + addSyntax(this::execute, extension); + } + + private void usage(CommandSender sender, Arguments arguments) { + sender.sendMessage("Usage: /load "); + } + + private void execute(CommandSender sender, Arguments arguments) { + String name = join(arguments.getStringArray("extensionName")); + sender.sendMessage("extensionFile = "+name+"...."); + + ExtensionManager extensionManager = MinecraftServer.getExtensionManager(); + Path extensionFolder = extensionManager.getExtensionFolder().toPath().toAbsolutePath(); + Path extensionJar = extensionFolder.resolve(name); + if(!extensionJar.toAbsolutePath().startsWith(extensionFolder)) { + sender.sendMessage("File name '"+name+"' does not represent a file inside the extensions folder. Will not load"); + return; + } + + try { + extensionManager.loadDynamicExtension(extensionJar.toFile()); + sender.sendMessage("Extension loaded!"); + } catch (Exception e) { + e.printStackTrace(); + sender.sendMessage("Failed to load extension: "+e.getMessage()); + } + } + + private void gameModeCallback(CommandSender sender, String extension, int error) { + sender.sendMessage("'" + extension + "' is not a valid extension name!"); + } + + private String join(String[] extensionNameParts) { + StringBuilder b = new StringBuilder(); + for (int i = 0; i < extensionNameParts.length; i++) { + String s = extensionNameParts[i]; + if(i != 0) { + b.append(" "); + } + b.append(s); + } + return b.toString(); + } +} diff --git a/src/test/java/demo/commands/UnloadExtensionCommand.java b/src/test/java/demo/commands/UnloadExtensionCommand.java new file mode 100644 index 000000000..b2bd247eb --- /dev/null +++ b/src/test/java/demo/commands/UnloadExtensionCommand.java @@ -0,0 +1,76 @@ +package demo.commands; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.command.CommandSender; +import net.minestom.server.command.builder.Arguments; +import net.minestom.server.command.builder.Command; +import net.minestom.server.command.builder.arguments.Argument; +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.extensions.Extension; +import net.minestom.server.extensions.ExtensionManager; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; + +public class UnloadExtensionCommand extends Command { + public UnloadExtensionCommand() { + super("unload"); + + setDefaultExecutor(this::usage); + + Argument extension = ArgumentType.DynamicStringArray("extensionName"); + + setArgumentCallback(this::gameModeCallback, extension); + + addSyntax(this::execute, extension); + } + + private void usage(CommandSender sender, Arguments arguments) { + sender.sendMessage("Usage: /unload "); + } + + private void execute(CommandSender sender, Arguments arguments) { + String name = join(arguments.getStringArray("extensionName")); + sender.sendMessage("extensionName = "+name+"...."); + + ExtensionManager extensionManager = MinecraftServer.getExtensionManager(); + Extension ext = extensionManager.getExtension(name); + if(ext != null) { + try { + extensionManager.unloadExtension(name); + } catch (Throwable t) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + t.printStackTrace(); + t.printStackTrace(new PrintStream(baos)); + baos.flush(); + baos.close(); + String contents = new String(baos.toByteArray(), StandardCharsets.UTF_8); + contents.lines().forEach(sender::sendMessage); + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + sender.sendMessage("Extension '"+name+"' does not exist."); + } + } + + private void gameModeCallback(CommandSender sender, String extension, int error) { + sender.sendMessage("'" + extension + "' is not a valid extension name!"); + } + + private String join(String[] extensionNameParts) { + StringBuilder b = new StringBuilder(); + for (int i = 0; i < extensionNameParts.length; i++) { + String s = extensionNameParts[i]; + if(i != 0) { + b.append(" "); + } + b.append(s); + } + return b.toString(); + } +} From 69f424eb6a2b958845fdb29028a7e17dbe3fb97c Mon Sep 17 00:00:00 2001 From: jglrxavpok Date: Fri, 6 Nov 2020 16:10:44 +0100 Subject: [PATCH 12/13] Improve load command safety --- .../server/extensions/ExtensionManager.java | 17 ++++++++++++----- .../demo/commands/LoadExtensionCommand.java | 18 ++++++++++++++---- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index 65a8dc338..dbe90838f 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -16,7 +16,6 @@ import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.util.*; @@ -553,7 +552,7 @@ public class ExtensionManager { loadExtensionList(extensionsToReload); } - public void loadDynamicExtension(File jarFile) throws FileNotFoundException { + public boolean loadDynamicExtension(File jarFile) throws FileNotFoundException { if(!jarFile.exists()) { throw new FileNotFoundException("File '"+jarFile.getAbsolutePath()+"' does not exists. Cannot load extension."); } @@ -561,10 +560,10 @@ public class ExtensionManager { log.info("Discover dynamic extension from jar {}", jarFile.getAbsolutePath()); DiscoveredExtension discoveredExtension = discoverFromJar(jarFile); List extensionsToLoad = Collections.singletonList(discoveredExtension); - loadExtensionList(extensionsToLoad); + return loadExtensionList(extensionsToLoad); } - private void loadExtensionList(List extensionsToLoad) { + private boolean loadExtensionList(List extensionsToLoad) { // ensure correct order of dependencies log.debug("Reorder extensions to ensure proper load order"); extensionsToLoad = generateLoadOrder(extensionsToLoad); @@ -585,7 +584,14 @@ public class ExtensionManager { // reload extensions log.info("Actually load extension {}", toReload.getName()); Extension loadedExtension = attemptSingleLoad(toReload); - newExtensions.add(loadedExtension); + if(loadedExtension != null) { + newExtensions.add(loadedExtension); + } + } + + if(newExtensions.isEmpty()) { + log.error("No extensions to load, skipping callbacks"); + return false; } log.info("Load complete, firing preinit, init and then postinit callbacks"); @@ -593,6 +599,7 @@ public class ExtensionManager { newExtensions.forEach(Extension::preInitialize); newExtensions.forEach(Extension::initialize); newExtensions.forEach(Extension::postInitialize); + return true; } public void unloadExtension(String extensionName) { diff --git a/src/test/java/demo/commands/LoadExtensionCommand.java b/src/test/java/demo/commands/LoadExtensionCommand.java index d5750e6be..dcb0399a8 100644 --- a/src/test/java/demo/commands/LoadExtensionCommand.java +++ b/src/test/java/demo/commands/LoadExtensionCommand.java @@ -38,14 +38,24 @@ public class LoadExtensionCommand extends Command { ExtensionManager extensionManager = MinecraftServer.getExtensionManager(); Path extensionFolder = extensionManager.getExtensionFolder().toPath().toAbsolutePath(); Path extensionJar = extensionFolder.resolve(name); - if(!extensionJar.toAbsolutePath().startsWith(extensionFolder)) { - sender.sendMessage("File name '"+name+"' does not represent a file inside the extensions folder. Will not load"); + try { + if(!extensionJar.toFile().getCanonicalPath().startsWith(extensionFolder.toFile().getCanonicalPath())) { + sender.sendMessage("File name '"+name+"' does not represent a file inside the extensions folder. Will not load"); + return; + } + } catch (IOException e) { + e.printStackTrace(); + sender.sendMessage("Failed to load extension: "+e.getMessage()); return; } try { - extensionManager.loadDynamicExtension(extensionJar.toFile()); - sender.sendMessage("Extension loaded!"); + boolean managed = extensionManager.loadDynamicExtension(extensionJar.toFile()); + if(managed) { + sender.sendMessage("Extension loaded!"); + } else { + sender.sendMessage("Failed to load extension, check your logs."); + } } catch (Exception e) { e.printStackTrace(); sender.sendMessage("Failed to load extension: "+e.getMessage()); From 8c8ba3794758bb577fa3f70ceba49d6292aac434 Mon Sep 17 00:00:00 2001 From: themode Date: Fri, 6 Nov 2020 23:35:31 +0100 Subject: [PATCH 13/13] Added auto suggestion in the reload command --- .../server/extensions/ExtensionManager.java | 48 +++++++++---------- .../demo/commands/ReloadExtensionCommand.java | 31 ++++++++++-- 2 files changed, 50 insertions(+), 29 deletions(-) diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index dbe90838f..b5f8101cc 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -65,7 +65,7 @@ public class ExtensionManager { // remove invalid extensions discoveredExtensions.removeIf(ext -> ext.loadStatus != DiscoveredExtension.LoadStatus.LOAD_SUCCESS); - for(DiscoveredExtension discoveredExtension : discoveredExtensions) { + for (DiscoveredExtension discoveredExtension : discoveredExtensions) { try { setupClassLoader(discoveredExtension); } catch (Exception e) { @@ -187,9 +187,9 @@ public class ExtensionManager { } // add dependents to pre-existing extensions, so that they can easily be found during reloading - for(String dependency : discoveredExtension.getDependencies()) { + for (String dependency : discoveredExtension.getDependencies()) { Extension dep = extensions.get(dependency.toLowerCase()); - if(dep == null) { + if (dep == null) { log.warn("Dependency {} of {} is null? This means the extension has been loaded without its dependency, which could cause issues later.", dependency, discoveredExtension.getName()); } else { dep.getDescription().getDependents().add(discoveredExtension.getName()); @@ -213,7 +213,7 @@ public class ExtensionManager { continue; } DiscoveredExtension extension = discoverFromJar(file); - if(extension != null && extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) { + if (extension != null && extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) { extensions.add(extension); } } @@ -274,7 +274,7 @@ public class ExtensionManager { // Specifies an extension we don't have. if (dependencyExtension == null) { // attempt to see if it is not already loaded (happens with dynamic (re)loading) - if(extensions.containsKey(dependencyName.toLowerCase())) { + if (extensions.containsKey(dependencyName.toLowerCase())) { return extensions.get(dependencyName.toLowerCase()).getDescription().getOrigin(); } else { log.error("Extension {} requires an extension called {}.", discoveredExtension.getName(), dependencyName); @@ -400,24 +400,24 @@ public class ExtensionManager { public MinestomExtensionClassLoader newClassLoader(@NotNull DiscoveredExtension extension, @NotNull URL[] urls) { MinestomRootClassLoader root = MinestomRootClassLoader.getInstance(); MinestomExtensionClassLoader loader = new MinestomExtensionClassLoader(extension.getName(), urls, root); - if(extension.getDependencies().length == 0) { + if (extension.getDependencies().length == 0) { // orphaned extension, we can insert it directly root.addChild(loader); } else { // we need to keep track that it has actually been inserted // even though it should always be (due to the order in which extensions are loaders), it is an additional layer of """security""" boolean foundOne = false; - for(String dependency : extension.getDependencies()) { - if(extensionLoaders.containsKey(dependency.toLowerCase())) { + for (String dependency : extension.getDependencies()) { + if (extensionLoaders.containsKey(dependency.toLowerCase())) { MinestomExtensionClassLoader parentLoader = extensionLoaders.get(dependency.toLowerCase()); parentLoader.addChild(loader); foundOne = true; } } - if(!foundOne) { + if (!foundOne) { log.error("Could not load extension {}, could not find any parent inside classloader hierarchy.", extension.getName()); - throw new RuntimeException("Could not load extension "+extension.getName()+", could not find any parent inside classloader hierarchy."); + throw new RuntimeException("Could not load extension " + extension.getName() + ", could not find any parent inside classloader hierarchy."); } } return loader; @@ -480,7 +480,7 @@ public class ExtensionManager { // remove as dependent of other extensions // this avoids issues where a dependent extension fails to reload, and prevents the base extension to reload too - for(Extension e : extensionList) { + for (Extension e : extensionList) { e.getDescription().getDependents().remove(ext.getDescription().getName()); } @@ -502,12 +502,12 @@ public class ExtensionManager { public void reload(String extensionName) { Extension ext = extensions.get(extensionName.toLowerCase()); - if(ext == null) { - throw new IllegalArgumentException("Extension "+extensionName+" is not currently loaded."); + if (ext == null) { + throw new IllegalArgumentException("Extension " + extensionName + " is not currently loaded."); } File originalJar = ext.getDescription().getOrigin().getOriginalJar(); - if(originalJar == null) { + if (originalJar == null) { log.error("Cannot reload extension {} that is not from a .jar file!", extensionName); return; } @@ -516,11 +516,11 @@ public class ExtensionManager { List dependents = new LinkedList<>(ext.getDescription().getDependents()); // copy dependents list List originalJarsOfDependents = new LinkedList<>(); - for(String dependentID : dependents) { + for (String dependentID : dependents) { Extension dependentExt = extensions.get(dependentID.toLowerCase()); File dependentOriginalJar = dependentExt.getDescription().getOrigin().getOriginalJar(); originalJarsOfDependents.add(dependentOriginalJar); - if(dependentOriginalJar == null) { + if (dependentOriginalJar == null) { log.error("Cannot reload extension {} that is not from a .jar file!", dependentID); return; } @@ -542,7 +542,7 @@ public class ExtensionManager { DiscoveredExtension rediscoveredExtension = discoverFromJar(originalJar); extensionsToReload.add(rediscoveredExtension); - for(File dependentJar : originalJarsOfDependents) { + for (File dependentJar : originalJarsOfDependents) { // rediscover dependent extension to reload log.info("Rediscover dependent extension (depends on {}) from jar {}", extensionName, dependentJar.getAbsolutePath()); extensionsToReload.add(discoverFromJar(dependentJar)); @@ -553,8 +553,8 @@ public class ExtensionManager { } public boolean loadDynamicExtension(File jarFile) throws FileNotFoundException { - if(!jarFile.exists()) { - throw new FileNotFoundException("File '"+jarFile.getAbsolutePath()+"' does not exists. Cannot load extension."); + if (!jarFile.exists()) { + throw new FileNotFoundException("File '" + jarFile.getAbsolutePath() + "' does not exists. Cannot load extension."); } log.info("Discover dynamic extension from jar {}", jarFile.getAbsolutePath()); @@ -584,12 +584,12 @@ public class ExtensionManager { // reload extensions log.info("Actually load extension {}", toReload.getName()); Extension loadedExtension = attemptSingleLoad(toReload); - if(loadedExtension != null) { + if (loadedExtension != null) { newExtensions.add(loadedExtension); } } - if(newExtensions.isEmpty()) { + if (newExtensions.isEmpty()) { log.error("No extensions to load, skipping callbacks"); return false; } @@ -604,12 +604,12 @@ public class ExtensionManager { public void unloadExtension(String extensionName) { Extension ext = extensions.get(extensionName.toLowerCase()); - if(ext == null) { - throw new IllegalArgumentException("Extension "+extensionName+" is not currently loaded."); + if (ext == null) { + throw new IllegalArgumentException("Extension " + extensionName + " is not currently loaded."); } List dependents = new LinkedList<>(ext.getDescription().getDependents()); // copy dependents list - for(String dependentID : dependents) { + for (String dependentID : dependents) { Extension dependentExt = extensions.get(dependentID.toLowerCase()); log.info("Unloading dependent extension {} (because it depends on {})", dependentID, extensionName); unload(dependentExt); diff --git a/src/test/java/demo/commands/ReloadExtensionCommand.java b/src/test/java/demo/commands/ReloadExtensionCommand.java index f9f5d8c17..17d94feeb 100644 --- a/src/test/java/demo/commands/ReloadExtensionCommand.java +++ b/src/test/java/demo/commands/ReloadExtensionCommand.java @@ -8,14 +8,29 @@ import net.minestom.server.command.builder.arguments.Argument; import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.extensions.Extension; import net.minestom.server.extensions.ExtensionManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.Collectors; public class ReloadExtensionCommand extends Command { + + // the extensions name as an array + private static String[] extensionsName; + + static { + List extensionsName = MinecraftServer.getExtensionManager().getExtensions() + .stream() + .map(extension -> extension.getDescription().getName()) + .collect(Collectors.toList()); + ReloadExtensionCommand.extensionsName = extensionsName.toArray(new String[0]); + } + public ReloadExtensionCommand() { super("reload"); @@ -34,11 +49,11 @@ public class ReloadExtensionCommand extends Command { private void execute(CommandSender sender, Arguments arguments) { String name = join(arguments.getStringArray("extensionName")); - sender.sendMessage("extensionName = "+name+"...."); + sender.sendMessage("extensionName = " + name + "...."); ExtensionManager extensionManager = MinecraftServer.getExtensionManager(); Extension ext = extensionManager.getExtension(name); - if(ext != null) { + if (ext != null) { try { extensionManager.reload(name); } catch (Throwable t) { @@ -55,7 +70,7 @@ public class ReloadExtensionCommand extends Command { } } } else { - sender.sendMessage("Extension '"+name+"' does not exist."); + sender.sendMessage("Extension '" + name + "' does not exist."); } } @@ -63,11 +78,17 @@ public class ReloadExtensionCommand extends Command { sender.sendMessage("'" + extension + "' is not a valid extension name!"); } + @Nullable + @Override + public String[] onDynamicWrite(@NotNull String text) { + return extensionsName; + } + private String join(String[] extensionNameParts) { StringBuilder b = new StringBuilder(); for (int i = 0; i < extensionNameParts.length; i++) { String s = extensionNameParts[i]; - if(i != 0) { + if (i != 0) { b.append(" "); } b.append(s);