mirror of https://github.com/Minestom/Minestom.git
Reloading extensions
This commit is contained in:
parent
25cde2cde7
commit
925f5fa614
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue