Reloading extensions

This commit is contained in:
jglrxavpok 2020-11-03 21:26:46 +01:00
parent 25cde2cde7
commit 925f5fa614
7 changed files with 289 additions and 34 deletions

View File

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

View File

@ -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<String> authors;
private final List<String> dependents = new ArrayList<>();
private final DiscoveredExtension origin;
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.origin = origin;
}
@NotNull
@ -72,5 +84,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

@ -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<Extension> extensionList = new ArrayList<>();
private final List<Extension> extensionList = new ArrayList<>();
private final List<Extension> 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<? extends Extension> 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<? extends Extension> 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<DiscoveredExtension> generateLoadOrder(List<DiscoveredExtension> discoveredExtensions) {
// Do some mapping so we can map strings to extensions.
Map<String, DiscoveredExtension> 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<Map.Entry<DiscoveredExtension, List<DiscoveredExtension>>> 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<DiscoveredExtension> dependencies) {
return dependencies.isEmpty() || dependencies.stream().allMatch(ext -> extensions.containsKey(ext.getName().toLowerCase()));
}
private void loadDependencies(List<DiscoveredExtension> extensions) {
ExtensionDependencyResolver extensionDependencyResolver = new ExtensionDependencyResolver(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();
@ -404,7 +434,7 @@ public class ExtensionManager {
@NotNull
public List<Extension> 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<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);
// 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
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<Extension> 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);
}
}

View File

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

View File

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

View File

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