Merge branch 'extensions-v2'

# Conflicts:
#	src/main/java/net/minestom/server/extensions/Extension.java
#	src/main/java/net/minestom/server/extensions/ExtensionManager.java
#	src/main/java/net/minestom/server/extras/selfmodification/MinestomRootClassLoader.java
This commit is contained in:
jglrxavpok 2020-11-18 09:13:30 +01:00
commit 0a4bb5ab08
25 changed files with 1259 additions and 225 deletions

2
.gitignore vendored
View File

@ -52,4 +52,4 @@ gradle-app.setting
/src/main/java/com/mcecraft/
# When running the demo we generate the extensions folder
extensions/
/extensions/

View File

@ -137,6 +137,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}") {
@ -145,6 +147,8 @@ dependencies {
}
}
implementation "com.github.Minestom:DependencyGetter:v1.0.1"
// LWJGL, for map rendering
lwjglApi platform("org.lwjgl:lwjgl-bom:$lwjglVersion")

View File

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

View File

@ -104,6 +104,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)}.
*

View File

@ -37,6 +37,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);
}
/**
* Parses the given command.
*

View File

@ -0,0 +1,168 @@
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;
import java.util.LinkedList;
import java.util.List;
@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 String[] dependencies;
private ExternalDependencies externalDependencies;
transient List<URL> files = new LinkedList<>();
transient LoadStatus loadStatus = LoadStatus.LOAD_SUCCESS;
transient private File originalJar;
@NotNull
public String getName() {
return name;
}
@NotNull
public String getEntrypoint() {
return entrypoint;
}
@NotNull
public String getVersion() {
return version;
}
@NotNull
public String getMixinConfig() {
return mixinConfig;
}
@NotNull
public String[] getAuthors() {
return authors;
}
@NotNull
public String[] getCodeModifiers() {
if (codeModifiers == null) {
codeModifiers = new String[0];
}
return codeModifiers;
}
@NotNull
public String[] getDependencies() {
return dependencies;
}
@NotNull
public ExternalDependencies getExternalDependencies() {
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();
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);
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 String[0];
}
// No external dependencies were specified;
if (extension.externalDependencies == null) {
extension.externalDependencies = new ExternalDependencies();
}
}
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."),
FAILED_TO_SETUP_CLASSLOADER("Extension classloader could not be setup."),
LOAD_FAILED("Load failed. See logs for more information."),
;
private final String message;
LoadStatus(@NotNull String message) {
this.message = message;
}
@NotNull
public String getMessage() {
return message;
}
}
static final class ExternalDependencies {
Repository[] repositories = new Repository[0];
String[] artifacts = new String[0];
static class Repository {
String name = "";
String url = "";
}
}
}

View File

@ -6,8 +6,11 @@ import org.slf4j.Logger;
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() {
@ -34,24 +37,36 @@ public abstract class Extension {
}
/**
* Called after postTerminate when reloading an extension
*/
public void unload() {
}
@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<String> authors;
private final List<String> dependents = new ArrayList<>();
private final DiscoveredExtension origin;
protected ExtensionDescription(@NotNull String name, @NotNull String version, @NotNull List<String> authors) {
ExtensionDescription(@NotNull String name, @NotNull String version, @NotNull List<String> authors, @NotNull DiscoveredExtension origin) {
this.name = name;
this.version = version;
this.authors = authors;
this.authors = authors != null ? authors : new ArrayList<>();
this.origin = origin;
}
@NotNull
@ -68,5 +83,15 @@ public abstract class Extension {
public List<String> getAuthors() {
return authors;
}
@NotNull
public List<String> getDependents() {
return dependents;
}
@NotNull
DiscoveredExtension getOrigin() {
return origin;
}
}
}

View File

@ -0,0 +1,54 @@
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<String, DiscoveredExtension> extensionMap = new HashMap<>();
public ExtensionDependencyResolver(List<DiscoveredExtension> 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
// 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<ResolvedDependency> 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 + "]";
}
}

View File

@ -1,7 +1,11 @@
package net.minestom.server.extensions;
import com.google.gson.*;
import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader;
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.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;
@ -17,21 +21,25 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.ZipFile;
public final class ExtensionManager {
public final static Logger LOGGER = LoggerFactory.getLogger(ExtensionManager.class);
@Slf4j(topic = "Minestom-Extensions")
public class ExtensionManager {
private final static String INDEV_CLASSES_FOLDER = "minestom.extension.indevfolder.classes";
private final static String INDEV_RESOURCES_FOLDER = "minestom.extension.indevfolder.resources";
private final static Gson GSON = new Gson();
private final Map<String, URLClassLoader> extensionLoaders = new HashMap<>();
private final Map<String, MinestomExtensionClassLoader> extensionLoaders = new HashMap<>();
private final Map<String, Extension> extensions = new HashMap<>();
private final File extensionFolder = new File("extensions");
private final File dependenciesFolder = new File(extensionFolder, ".libs");
private boolean loaded;
private final List<Extension> extensionList = new ArrayList<>();
private final List<Extension> immutableExtensionListView = Collections.unmodifiableList(extensionList);
public ExtensionManager() {
}
@ -41,151 +49,161 @@ public final class ExtensionManager {
if (!extensionFolder.exists()) {
if (!extensionFolder.mkdirs()) {
LOGGER.error("Could not find or create the extension folder, extensions will not be loaded!");
log.error("Could not find or create the extension folder, extensions will not be loaded!");
return;
}
}
final List<DiscoveredExtension> discoveredExtensions = discoverExtensions();
if (!dependenciesFolder.exists()) {
if (!dependenciesFolder.mkdirs()) {
log.error("Could not find nor create the extension dependencies folder, extensions will not be loaded!");
return;
}
}
List<DiscoveredExtension> 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 = 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) {
LOGGER.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("'");
}
LOGGER.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]+")) {
LOGGER.error("Extension '{}' specified an invalid name.", extensionName);
LOGGER.error("Extension '{}' will not be loaded.", extensionName);
continue;
}
// Get ExtensionDescription (authors, version etc.)
Extension.ExtensionDescription extensionDescription;
{
String version;
if (!extensionDescriptionJson.has("version")) {
LOGGER.warn("Extension '{}' did not specify a version.", extensionName);
LOGGER.warn("Extension '{}' will continue to load but should specify a plugin version.", extensionName);
version = "Not Specified";
} else {
version = extensionDescriptionJson.get("version").getAsString();
}
List<String> authors;
if (!extensionDescriptionJson.has("authors")) {
authors = new ArrayList<>();
} else {
authors = Arrays.asList(new Gson().fromJson(extensionDescriptionJson.get("authors"), String[].class));
}
extensionDescription = new Extension.ExtensionDescription(extensionName, version, authors);
}
extensionLoaders.put(extensionName.toLowerCase(), loader);
if (extensions.containsKey(extensionName.toLowerCase())) {
LOGGER.error("An extension called '{}' has already been registered.", extensionName);
continue;
}
Class<?> jarClass;
try {
jarClass = Class.forName(mainClass, true, loader);
} catch (ClassNotFoundException e) {
LOGGER.error("Could not find main class '{}' in extension '{}'.", mainClass, extensionName, e);
continue;
}
Class<? extends Extension> extensionClass;
try {
extensionClass = jarClass.asSubclass(Extension.class);
} catch (ClassCastException e) {
LOGGER.error("Main class '{}' in '{}' does not extend the 'Extension' superclass.", mainClass, extensionName, e);
continue;
}
Constructor<? extends Extension> 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) {
LOGGER.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) {
LOGGER.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) {
LOGGER.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) {
LOGGER.error("Main class '{}' in '{}' has no description field.", mainClass, extensionName, e);
continue;
}
// Set logger
try {
Field descriptionField = extensionClass.getSuperclass().getDeclaredField("logger");
descriptionField.setAccessible(true);
descriptionField.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).
LOGGER.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);
}
extensions.put(extensionName.toLowerCase(), extension);
}
}
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 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()),
discoveredExtension
);
MinestomExtensionClassLoader loader = extensionLoaders.get(extensionName.toLowerCase());
if (extensions.containsKey(extensionName.toLowerCase())) {
log.error("An extension called '{}' has already been registered.", extensionName);
return null;
}
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 null;
}
Class<? extends Extension> 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 null;
}
Constructor<? extends Extension> 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 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 null;
} 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 null;
}
// 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 null;
}
// 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);
}
// 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
private List<DiscoveredExtension> discoverExtensions() {
List<DiscoveredExtension> extensions = new LinkedList<>();
@ -196,28 +214,28 @@ public final 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 = new DiscoveredExtension();
extension.files = new File[]{file};
extension.description = GSON.fromJson(reader, JsonObject.class);
DiscoveredExtension extension = discoverFromJar(file);
if (extension != null && extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) {
extensions.add(extension);
} catch (IOException e) {
e.printStackTrace();
}
}
// this allows developers to have their extension discovered while working on it, without having to build a jar and put in the extension folder
if (System.getProperty(INDEV_CLASSES_FOLDER) != null && System.getProperty(INDEV_RESOURCES_FOLDER) != null) {
LOGGER.info("Found indev folders for extension. Adding to list of discovered extensions.");
log.info("Found indev folders for extension. Adding to list of discovered extensions.");
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();
extension.files = new File[]{new File(extensionClasses), new File(extensionResources)};
extension.description = GSON.fromJson(reader, JsonObject.class);
extensions.add(extension);
DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class);
extension.files.add(new File(extensionClasses).toURI().toURL());
extension.files.add(new File(extensionResources).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();
}
@ -225,14 +243,186 @@ public final 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<DiscoveredExtension> generateLoadOrder(List<DiscoveredExtension> discoveredExtensions) {
// Do some mapping so we can map strings to extensions.
Map<String, DiscoveredExtension> extensionMap = new HashMap<>();
Map<DiscoveredExtension, List<DiscoveredExtension>> dependencyMap = new HashMap<>();
for (DiscoveredExtension discoveredExtension : discoveredExtensions) {
extensionMap.put(discoveredExtension.getName().toLowerCase(), discoveredExtension);
}
for (DiscoveredExtension discoveredExtension : discoveredExtensions) {
List<DiscoveredExtension> dependencies = Arrays.stream(discoveredExtension.getDependencies())
.map(dependencyName -> {
DiscoveredExtension dependencyExtension = extensionMap.get(dependencyName.toLowerCase());
// 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())) {
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());
}).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<DiscoveredExtension> sortedList = new LinkedList<>();
// entries with empty lists
List<Map.Entry<DiscoveredExtension, List<DiscoveredExtension>>> loadableExtensions;
// While there are entries with no more elements (no more dependencies)
while (!(
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.
for (Map.Entry<DiscoveredExtension, List<DiscoveredExtension>> 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<DiscoveredExtension, List<DiscoveredExtension>> 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 boolean areAllDependenciesLoaded(List<DiscoveredExtension> dependencies) {
return dependencies.isEmpty() || dependencies.stream().allMatch(ext -> extensions.containsKey(ext.getName().toLowerCase()));
}
private void loadDependencies(List<DiscoveredExtension> extensions) {
List<DiscoveredExtension> 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();
DiscoveredExtension.ExternalDependencies externalDependencies = ext.getExternalDependencies();
List<MavenRepository> 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);
getter.addResolver(extensionDependencyResolver);
for (var artifact : externalDependencies.artifacts) {
var resolved = getter.get(artifact, dependenciesFolder);
addDependencyFile(resolved.getContentsLocation(), ext);
log.trace("Dependency of extension {}: {}", ext.getName(), resolved);
}
for (var dependencyName : ext.getDependencies()) {
var resolved = getter.get(dependencyName, dependenciesFolder);
addDependencyFile(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 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 URL[] urls) {
return URLClassLoader.newInstance(urls, ExtensionManager.class.getClassLoader());
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) {
// 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;
}
@NotNull
@ -242,7 +432,7 @@ public final class ExtensionManager {
@NotNull
public List<Extension> getExtensions() {
return new ArrayList<>(extensions.values());
return immutableExtensionListView;
}
@Nullable
@ -260,35 +450,177 @@ public final class ExtensionManager {
*/
private void setupCodeModifiers(@NotNull List<DiscoveredExtension> extensions) {
final ClassLoader cl = getClass().getClassLoader();
if (!(cl instanceof MinestomOverwriteClassLoader)) {
LOGGER.warn("Current class loader is not a MinestomOverwriteClassLoader, but " + cl + ". This disables code modifiers (Mixin support is therefore disabled)");
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;
LOGGER.info("Start loading code modifiers...");
MinestomRootClassLoader modifiableClassLoader = (MinestomRootClassLoader) cl;
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.toArray(new File[0]), codeModifierClass);
}
if (extension.description.has("mixinConfig")) {
final String mixinConfigFile = extension.description.get("mixinConfig").getAsString();
if (!extension.getMixinConfig().isEmpty()) {
final String mixinConfigFile = extension.getMixinConfig();
Mixins.addConfiguration(mixinConfigFile);
LOGGER.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();
LOGGER.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);
}
}
LOGGER.info("Done loading code modifiers.");
log.info("Done loading code modifiers.");
}
private static class DiscoveredExtension {
private File[] files;
private JsonObject description;
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);
try {
// close resources
classloader.close();
} catch (IOException e) {
e.printStackTrace();
}
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<String> dependents = new LinkedList<>(ext.getDescription().getDependents()); // copy dependents list
List<File> 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);
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
List<DiscoveredExtension> 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
loadExtensionList(extensionsToReload);
}
public boolean 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<DiscoveredExtension> extensionsToLoad = Collections.singletonList(discoveredExtension);
return loadExtensionList(extensionsToLoad);
}
private boolean loadExtensionList(List<DiscoveredExtension> 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 : 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(extensionsToLoad);
List<Extension> newExtensions = new LinkedList<>();
for (DiscoveredExtension toReload : extensionsToLoad) {
// reload extensions
log.info("Actually load extension {}", toReload.getName());
Extension loadedExtension = attemptSingleLoad(toReload);
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");
// retrigger preinit, init and postinit
newExtensions.forEach(Extension::preInitialize);
newExtensions.forEach(Extension::initialize);
newExtensions.forEach(Extension::postInitialize);
return true;
}
public void unloadExtension(String extensionName) {
Extension ext = extensions.get(extensionName.toLowerCase());
if (ext == null) {
throw new IllegalArgumentException("Extension " + extensionName + " is not currently loaded.");
}
List<String> 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();
}
}

View File

@ -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 {
/**

View File

@ -0,0 +1,41 @@
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<MinestomExtensionClassLoader> 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;
}
public void removeChildInHierarchy(MinestomExtensionClassLoader child) {
children.remove(child);
children.forEach(c -> c.removeChildInHierarchy(child));
}
}

View File

@ -0,0 +1,76 @@
package net.minestom.server.extras.selfmodification;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
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;
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 {
Class<?> loadedClass = findLoadedClass(name);
if(loadedClass != null) {
return loadedClass;
}
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);
}
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;
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.err.println("Class loader "+getName()+" finalized.");
}
}

View File

@ -1,6 +1,6 @@
package net.minestom.server.extras.selfmodification;
import net.minestom.server.MinecraftServer;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory;
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;
@ -22,11 +23,10 @@ import java.util.Set;
/**
* Class Loader that can modify class bytecode when they are loaded
*/
public class MinestomOverwriteClassLoader extends URLClassLoader {
@Slf4j
public class MinestomRootClassLoader extends HierarchyClassLoader {
public final static Logger LOGGER = LoggerFactory.getLogger(MinecraftServer.class);
private static MinestomOverwriteClassLoader INSTANCE;
private static MinestomRootClassLoader INSTANCE;
/**
* Classes that cannot be loaded/modified by this classloader.
@ -47,6 +47,7 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
add("org.apache");
add("org.spongepowered");
add("net.minestom.server.extras.selfmodification");
add("org.jboss.shrinkwrap.resolver");
}
};
/**
@ -62,16 +63,16 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
// TODO: priorities?
private final List<CodeModifier> modifiers = 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());
}
}
}
@ -108,18 +109,18 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
try {
// we do not load system classes by ourselves
Class<?> systemClass = ClassLoader.getPlatformClassLoader().loadClass(name);
LOGGER.trace("System class: " + systemClass);
log.trace("System class: " + systemClass);
return systemClass;
} catch (ClassNotFoundException e) {
try {
if (isProtected(name)) {
LOGGER.trace("Protected: " + name);
log.trace("Protected: " + name);
return super.loadClass(name, resolve);
}
return define(name, loadBytes(name, true), resolve);
return define(name, resolve);
} catch (Exception ex) {
LOGGER.trace("Fail to load class, resorting to parent loader: " + name, ex);
log.trace("Fail to load class, resorting to parent loader: " + name, ex);
// fail to load class, let parent load
// this forbids code modification, but at least it will load
return super.loadClass(name, resolve);
@ -138,13 +139,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);
LOGGER.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;
}
/**
@ -160,9 +177,35 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
if (name == null)
throw new ClassNotFoundException();
String path = name.replace(".", "/") + ".class";
byte[] bytes = getResourceAsStream(path).readAllBytes();
if (transform && !isProtected(name)) {
ClassReader reader = new ClassReader(bytes);
InputStream input = getResourceAsStream(path);
if(input == null) {
throw new ClassNotFoundException("Could not find resource "+path);
}
byte[] originalBytes = input.readAllBytes();
if(transform) {
return transformBytes(originalBytes, name);
}
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);
ClassNode node = new ClassNode();
reader.accept(node, 0);
boolean modified = false;
@ -182,11 +225,11 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
}
};
node.accept(writer);
bytes = writer.toByteArray();
LOGGER.trace("Modified " + name);
classBytecode = writer.toByteArray();
log.trace("Modified " + name);
}
}
return bytes;
return classBytecode;
}
// overriden to increase access (from protected to public)
@ -211,7 +254,7 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
if (CodeModifier.class.isAssignableFrom(modifierClass)) {
CodeModifier modifier = (CodeModifier) modifierClass.getDeclaredConstructor().newInstance();
synchronized (modifiers) {
LOGGER.warn("Added Code modifier: " + modifier);
log.warn("Added Code modifier: " + modifier);
addCodeModifier(modifier);
}
}
@ -226,6 +269,11 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
}
}
@Override
public void addURL(URL url) {
super.addURL(url);
}
public List<CodeModifier> getModifiers() {
return modifiers;
}

View File

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

View File

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

View File

@ -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();
@ -65,7 +65,7 @@ public class MixinServiceMinestom extends MixinServiceAbstract {
@Override
public InputStream getResourceAsStream(String name) {
return classLoader.getResourceAsStream(name);
return classLoader.getResourceAsStreamWithChildren(name);
}
@Override

View File

@ -36,6 +36,9 @@ public class Main {
commandManager.register(new DimensionCommand());
commandManager.register(new ShutdownCommand());
commandManager.register(new TeleportCommand());
commandManager.register(new ReloadExtensionCommand());
commandManager.register(new UnloadExtensionCommand());
commandManager.register(new LoadExtensionCommand());
commandManager.setUnknownCommandCallback((sender, command) -> sender.sendMessage("unknown command"));

View File

@ -0,0 +1,80 @@
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 <extension file name>");
}
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);
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 {
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());
}
}
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();
}
}

View File

@ -0,0 +1,98 @@
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 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.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<String> extensionsName = MinecraftServer.getExtensionManager().getExtensions()
.stream()
.map(extension -> extension.getDescription().getName())
.collect(Collectors.toList());
ReloadExtensionCommand.extensionsName = extensionsName.toArray(new String[0]);
}
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 <extension name>");
}
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!");
}
@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) {
b.append(" ");
}
b.append(s);
}
return b.toString();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{
"entrypoint": "testextension.TestExtension",
"name": "Test extension",
"name": "Test_extension",
"codeModifiers": [
"testextension.TestModifier"
],