Let users change extension jar after unload, then load it again

This commit is contained in:
jglrxavpok 2020-11-06 16:03:08 +01:00
parent 925f5fa614
commit d83bec4732
7 changed files with 224 additions and 11 deletions

View File

@ -81,6 +81,16 @@ public final class CommandManager {
this.dispatcher.register(command);
}
/**
* Removes a command from the currently registered commands.
* Does nothing if the command was not registered before
*
* @param command the command to remove
*/
public void unregister(@NotNull Command command) {
this.dispatcher.unregister(command);
}
/**
* Gets the {@link Command} registered by {@link #register(Command)}.
*

View File

@ -23,6 +23,14 @@ public class CommandDispatcher {
this.commands.add(command);
}
public void unregister(Command command) {
commandMap.remove(command.getName().toLowerCase());
for(String alias : command.getAliases()) {
this.commandMap.remove(alias.toLowerCase());
}
commands.remove(command);
}
/**
* Parse the given command
*

View File

@ -12,10 +12,7 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.Mixins;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
@ -495,6 +492,12 @@ public class ExtensionManager {
// remove class loader, required to reload the classes
MinestomExtensionClassLoader classloader = extensionLoaders.remove(id);
try {
// close resources
classloader.close();
} catch (IOException e) {
e.printStackTrace();
}
MinestomRootClassLoader.getInstance().removeChildInHierarchy(classloader);
}
@ -530,6 +533,8 @@ public class ExtensionManager {
log.info("Unloading extension {}", extensionName);
unload(ext);
System.gc();
// ext and its dependents should no longer be referenced from now on
// rediscover extension to reload. We allow dependency changes, so we need to fully reload it
@ -545,32 +550,68 @@ public class ExtensionManager {
}
// ensure correct order of dependencies
log.debug("Reorder extensions to reload to ensure proper load order");
extensionsToReload = generateLoadOrder(extensionsToReload);
loadDependencies(extensionsToReload);
loadExtensionList(extensionsToReload);
}
public void loadDynamicExtension(File jarFile) throws FileNotFoundException {
if(!jarFile.exists()) {
throw new FileNotFoundException("File '"+jarFile.getAbsolutePath()+"' does not exists. Cannot load extension.");
}
log.info("Discover dynamic extension from jar {}", jarFile.getAbsolutePath());
DiscoveredExtension discoveredExtension = discoverFromJar(jarFile);
List<DiscoveredExtension> extensionsToLoad = Collections.singletonList(discoveredExtension);
loadExtensionList(extensionsToLoad);
}
private void 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 : extensionsToReload) {
for (DiscoveredExtension toReload : extensionsToLoad) {
log.debug("Setting up classloader for extension {}", toReload.getName());
setupClassLoader(toReload);
}
// setup code modifiers for these extensions
// TODO: it is possible the new modifiers cannot be applied (because the targeted classes are already loaded), should we issue a warning?
setupCodeModifiers(extensionsToReload);
setupCodeModifiers(extensionsToLoad);
List<Extension> newExtensions = new LinkedList<>();
for(DiscoveredExtension toReload : extensionsToReload) {
for (DiscoveredExtension toReload : extensionsToLoad) {
// reload extensions
log.info("Actually load extension {}", toReload.getName());
Extension loadedExtension = attemptSingleLoad(toReload);
newExtensions.add(loadedExtension);
}
log.info("Reload complete, refiring preinit, init and then postinit callbacks");
log.info("Load complete, firing preinit, init and then postinit callbacks");
// retrigger preinit, init and postinit
newExtensions.forEach(Extension::preInitialize);
newExtensions.forEach(Extension::initialize);
newExtensions.forEach(Extension::postInitialize);
}
public void unloadExtension(String extensionName) {
Extension ext = extensions.get(extensionName.toLowerCase());
if(ext == null) {
throw new IllegalArgumentException("Extension "+extensionName+" is not currently loaded.");
}
List<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

@ -67,4 +67,10 @@ public class MinestomExtensionClassLoader extends HierarchyClassLoader {
throw e;
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.err.println("Class loader "+getName()+" finalized.");
}
}

View File

@ -36,6 +36,8 @@ public class Main {
commandManager.register(new ShutdownCommand());
commandManager.register(new TeleportCommand());
commandManager.register(new ReloadExtensionCommand());
commandManager.register(new UnloadExtensionCommand());
commandManager.register(new LoadExtensionCommand());
StorageManager storageManager = MinecraftServer.getStorageManager();

View File

@ -0,0 +1,70 @@
package demo.commands;
import lombok.extern.slf4j.Slf4j;
import net.minestom.server.MinecraftServer;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Arguments;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.arguments.Argument;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.extensions.Extension;
import net.minestom.server.extensions.ExtensionManager;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
public class LoadExtensionCommand extends Command {
public LoadExtensionCommand() {
super("load");
setDefaultExecutor(this::usage);
Argument extension = ArgumentType.DynamicStringArray("extensionName");
setArgumentCallback(this::gameModeCallback, extension);
addSyntax(this::execute, extension);
}
private void usage(CommandSender sender, Arguments arguments) {
sender.sendMessage("Usage: /load <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);
if(!extensionJar.toAbsolutePath().startsWith(extensionFolder)) {
sender.sendMessage("File name '"+name+"' does not represent a file inside the extensions folder. Will not load");
return;
}
try {
extensionManager.loadDynamicExtension(extensionJar.toFile());
sender.sendMessage("Extension loaded!");
} catch (Exception e) {
e.printStackTrace();
sender.sendMessage("Failed to load extension: "+e.getMessage());
}
}
private void gameModeCallback(CommandSender sender, String extension, int error) {
sender.sendMessage("'" + extension + "' is not a valid extension name!");
}
private String join(String[] extensionNameParts) {
StringBuilder b = new StringBuilder();
for (int i = 0; i < extensionNameParts.length; i++) {
String s = extensionNameParts[i];
if(i != 0) {
b.append(" ");
}
b.append(s);
}
return b.toString();
}
}

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