diff --git a/src/main/java/net/minestom/server/command/CommandManager.java b/src/main/java/net/minestom/server/command/CommandManager.java index c7454e99a..2c351e9a5 100644 --- a/src/main/java/net/minestom/server/command/CommandManager.java +++ b/src/main/java/net/minestom/server/command/CommandManager.java @@ -81,6 +81,16 @@ public final class CommandManager { this.dispatcher.register(command); } + /** + * Removes a command from the currently registered commands. + * Does nothing if the command was not registered before + * + * @param command the command to remove + */ + public void unregister(@NotNull Command command) { + this.dispatcher.unregister(command); + } + /** * Gets the {@link Command} registered by {@link #register(Command)}. * diff --git a/src/main/java/net/minestom/server/command/builder/CommandDispatcher.java b/src/main/java/net/minestom/server/command/builder/CommandDispatcher.java index 46b58c8e0..47ffdf0c1 100644 --- a/src/main/java/net/minestom/server/command/builder/CommandDispatcher.java +++ b/src/main/java/net/minestom/server/command/builder/CommandDispatcher.java @@ -23,6 +23,14 @@ public class CommandDispatcher { this.commands.add(command); } + public void unregister(Command command) { + commandMap.remove(command.getName().toLowerCase()); + for(String alias : command.getAliases()) { + this.commandMap.remove(alias.toLowerCase()); + } + commands.remove(command); + } + /** * Parse the given command * diff --git a/src/main/java/net/minestom/server/extensions/ExtensionManager.java b/src/main/java/net/minestom/server/extensions/ExtensionManager.java index 2e8144276..65a8dc338 100644 --- a/src/main/java/net/minestom/server/extensions/ExtensionManager.java +++ b/src/main/java/net/minestom/server/extensions/ExtensionManager.java @@ -12,10 +12,7 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.LoggerFactory; import org.spongepowered.asm.mixin.Mixins; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; +import java.io.*; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; @@ -495,6 +492,12 @@ public class ExtensionManager { // remove class loader, required to reload the classes MinestomExtensionClassLoader classloader = extensionLoaders.remove(id); + try { + // close resources + classloader.close(); + } catch (IOException e) { + e.printStackTrace(); + } MinestomRootClassLoader.getInstance().removeChildInHierarchy(classloader); } @@ -530,6 +533,8 @@ public class ExtensionManager { log.info("Unloading extension {}", extensionName); unload(ext); + System.gc(); + // ext and its dependents should no longer be referenced from now on // rediscover extension to reload. We allow dependency changes, so we need to fully reload it @@ -545,32 +550,68 @@ public class ExtensionManager { } // ensure correct order of dependencies - log.debug("Reorder extensions to reload to ensure proper load order"); - extensionsToReload = generateLoadOrder(extensionsToReload); - loadDependencies(extensionsToReload); + loadExtensionList(extensionsToReload); + } + + public void loadDynamicExtension(File jarFile) throws FileNotFoundException { + if(!jarFile.exists()) { + throw new FileNotFoundException("File '"+jarFile.getAbsolutePath()+"' does not exists. Cannot load extension."); + } + + log.info("Discover dynamic extension from jar {}", jarFile.getAbsolutePath()); + DiscoveredExtension discoveredExtension = discoverFromJar(jarFile); + List extensionsToLoad = Collections.singletonList(discoveredExtension); + loadExtensionList(extensionsToLoad); + } + + private void loadExtensionList(List extensionsToLoad) { + // ensure correct order of dependencies + log.debug("Reorder extensions to ensure proper load order"); + extensionsToLoad = generateLoadOrder(extensionsToLoad); + loadDependencies(extensionsToLoad); // setup new classloaders for the extensions to reload - for(DiscoveredExtension toReload : extensionsToReload) { + for (DiscoveredExtension toReload : extensionsToLoad) { log.debug("Setting up classloader for extension {}", toReload.getName()); setupClassLoader(toReload); } // setup code modifiers for these extensions // TODO: it is possible the new modifiers cannot be applied (because the targeted classes are already loaded), should we issue a warning? - setupCodeModifiers(extensionsToReload); + setupCodeModifiers(extensionsToLoad); List newExtensions = new LinkedList<>(); - for(DiscoveredExtension toReload : extensionsToReload) { + for (DiscoveredExtension toReload : extensionsToLoad) { // reload extensions log.info("Actually load extension {}", toReload.getName()); Extension loadedExtension = attemptSingleLoad(toReload); newExtensions.add(loadedExtension); } - log.info("Reload complete, refiring preinit, init and then postinit callbacks"); + log.info("Load complete, firing preinit, init and then postinit callbacks"); // retrigger preinit, init and postinit newExtensions.forEach(Extension::preInitialize); newExtensions.forEach(Extension::initialize); newExtensions.forEach(Extension::postInitialize); } + + public void unloadExtension(String extensionName) { + Extension ext = extensions.get(extensionName.toLowerCase()); + if(ext == null) { + throw new IllegalArgumentException("Extension "+extensionName+" is not currently loaded."); + } + List dependents = new LinkedList<>(ext.getDescription().getDependents()); // copy dependents list + + for(String dependentID : dependents) { + Extension dependentExt = extensions.get(dependentID.toLowerCase()); + log.info("Unloading dependent extension {} (because it depends on {})", dependentID, extensionName); + unload(dependentExt); + } + + log.info("Unloading extension {}", extensionName); + unload(ext); + + // call GC to try to get rid of classes and classloader + System.gc(); + } } diff --git a/src/main/java/net/minestom/server/extras/selfmodification/MinestomExtensionClassLoader.java b/src/main/java/net/minestom/server/extras/selfmodification/MinestomExtensionClassLoader.java index ca3008895..7df07b288 100644 --- a/src/main/java/net/minestom/server/extras/selfmodification/MinestomExtensionClassLoader.java +++ b/src/main/java/net/minestom/server/extras/selfmodification/MinestomExtensionClassLoader.java @@ -67,4 +67,10 @@ public class MinestomExtensionClassLoader extends HierarchyClassLoader { throw e; } } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + System.err.println("Class loader "+getName()+" finalized."); + } } diff --git a/src/test/java/demo/Main.java b/src/test/java/demo/Main.java index 35eb6dd43..5e934678d 100644 --- a/src/test/java/demo/Main.java +++ b/src/test/java/demo/Main.java @@ -36,6 +36,8 @@ public class Main { commandManager.register(new ShutdownCommand()); commandManager.register(new TeleportCommand()); commandManager.register(new ReloadExtensionCommand()); + commandManager.register(new UnloadExtensionCommand()); + commandManager.register(new LoadExtensionCommand()); StorageManager storageManager = MinecraftServer.getStorageManager(); diff --git a/src/test/java/demo/commands/LoadExtensionCommand.java b/src/test/java/demo/commands/LoadExtensionCommand.java new file mode 100644 index 000000000..d5750e6be --- /dev/null +++ b/src/test/java/demo/commands/LoadExtensionCommand.java @@ -0,0 +1,70 @@ +package demo.commands; + +import lombok.extern.slf4j.Slf4j; +import net.minestom.server.MinecraftServer; +import net.minestom.server.command.CommandSender; +import net.minestom.server.command.builder.Arguments; +import net.minestom.server.command.builder.Command; +import net.minestom.server.command.builder.arguments.Argument; +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.extensions.Extension; +import net.minestom.server.extensions.ExtensionManager; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; + +public class LoadExtensionCommand extends Command { + public LoadExtensionCommand() { + super("load"); + + setDefaultExecutor(this::usage); + + Argument extension = ArgumentType.DynamicStringArray("extensionName"); + + setArgumentCallback(this::gameModeCallback, extension); + + addSyntax(this::execute, extension); + } + + private void usage(CommandSender sender, Arguments arguments) { + sender.sendMessage("Usage: /load "); + } + + private void execute(CommandSender sender, Arguments arguments) { + String name = join(arguments.getStringArray("extensionName")); + sender.sendMessage("extensionFile = "+name+"...."); + + ExtensionManager extensionManager = MinecraftServer.getExtensionManager(); + Path extensionFolder = extensionManager.getExtensionFolder().toPath().toAbsolutePath(); + Path extensionJar = extensionFolder.resolve(name); + if(!extensionJar.toAbsolutePath().startsWith(extensionFolder)) { + sender.sendMessage("File name '"+name+"' does not represent a file inside the extensions folder. Will not load"); + return; + } + + try { + extensionManager.loadDynamicExtension(extensionJar.toFile()); + sender.sendMessage("Extension loaded!"); + } catch (Exception e) { + e.printStackTrace(); + sender.sendMessage("Failed to load extension: "+e.getMessage()); + } + } + + private void gameModeCallback(CommandSender sender, String extension, int error) { + sender.sendMessage("'" + extension + "' is not a valid extension name!"); + } + + private String join(String[] extensionNameParts) { + StringBuilder b = new StringBuilder(); + for (int i = 0; i < extensionNameParts.length; i++) { + String s = extensionNameParts[i]; + if(i != 0) { + b.append(" "); + } + b.append(s); + } + return b.toString(); + } +} diff --git a/src/test/java/demo/commands/UnloadExtensionCommand.java b/src/test/java/demo/commands/UnloadExtensionCommand.java new file mode 100644 index 000000000..b2bd247eb --- /dev/null +++ b/src/test/java/demo/commands/UnloadExtensionCommand.java @@ -0,0 +1,76 @@ +package demo.commands; + +import net.minestom.server.MinecraftServer; +import net.minestom.server.command.CommandSender; +import net.minestom.server.command.builder.Arguments; +import net.minestom.server.command.builder.Command; +import net.minestom.server.command.builder.arguments.Argument; +import net.minestom.server.command.builder.arguments.ArgumentType; +import net.minestom.server.extensions.Extension; +import net.minestom.server.extensions.ExtensionManager; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; + +public class UnloadExtensionCommand extends Command { + public UnloadExtensionCommand() { + super("unload"); + + setDefaultExecutor(this::usage); + + Argument extension = ArgumentType.DynamicStringArray("extensionName"); + + setArgumentCallback(this::gameModeCallback, extension); + + addSyntax(this::execute, extension); + } + + private void usage(CommandSender sender, Arguments arguments) { + sender.sendMessage("Usage: /unload "); + } + + private void execute(CommandSender sender, Arguments arguments) { + String name = join(arguments.getStringArray("extensionName")); + sender.sendMessage("extensionName = "+name+"...."); + + ExtensionManager extensionManager = MinecraftServer.getExtensionManager(); + Extension ext = extensionManager.getExtension(name); + if(ext != null) { + try { + extensionManager.unloadExtension(name); + } catch (Throwable t) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + t.printStackTrace(); + t.printStackTrace(new PrintStream(baos)); + baos.flush(); + baos.close(); + String contents = new String(baos.toByteArray(), StandardCharsets.UTF_8); + contents.lines().forEach(sender::sendMessage); + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + sender.sendMessage("Extension '"+name+"' does not exist."); + } + } + + private void gameModeCallback(CommandSender sender, String extension, int error) { + sender.sendMessage("'" + extension + "' is not a valid extension name!"); + } + + private String join(String[] extensionNameParts) { + StringBuilder b = new StringBuilder(); + for (int i = 0; i < extensionNameParts.length; i++) { + String s = extensionNameParts[i]; + if(i != 0) { + b.append(" "); + } + b.append(s); + } + return b.toString(); + } +}