package de.geolykt.starloader.mod; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.UncheckedIOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.net.URLClassLoader; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import org.jetbrains.annotations.ApiStatus.AvailableSince; import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.ApiStatus.ScheduledForRemoval; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spongepowered.asm.mixin.Mixins; import org.stianloader.picoresolve.DependencyLayer; import org.stianloader.picoresolve.DependencyLayer.DependencyEdge; import org.stianloader.picoresolve.DependencyLayer.DependencyLayerElement; import org.stianloader.picoresolve.GAV; import org.stianloader.picoresolve.MavenResolver; import org.stianloader.picoresolve.Scope; import org.stianloader.picoresolve.exclusion.ExclusionContainer; import org.stianloader.picoresolve.repo.MavenRepository; import org.stianloader.picoresolve.repo.RepositoryAttachedValue; import org.stianloader.picoresolve.version.MavenVersion; import net.minestom.server.extras.selfmodification.MinestomExtensionClassLoader; import net.minestom.server.extras.selfmodification.MinestomRootClassLoader; import de.geolykt.starloader.mod.DiscoveredExtension.ExternalDependencies; import de.geolykt.starloader.mod.DiscoveredExtension.ExternalDependencyArtifact; import de.geolykt.starloader.mod.DiscoveredExtension.ExternalRepository; import de.geolykt.starloader.mod.DiscoveredExtension.LoadStatus; import de.geolykt.starloader.mod.Extension.ExtensionDescription; import de.geolykt.starloader.transformers.ASMTransformer; import de.geolykt.starloader.transformers.ReversibleAccessSetterTransformer; import de.geolykt.starloader.util.JavaInterop; import de.geolykt.starloader.util.MirroringURIMavenRepository; public class ExtensionManager { /** * The description of the extension that is currently being loaded. * This is internally used to set the description of an extension before the constructor runs * without having to rely on massive hacks using the sun Unsafe. */ static final ThreadLocal CURRENTLY_LOADED_EXTENSION = new ThreadLocal<>(); @Internal public static final Logger LOGGER = LoggerFactory.getLogger(ExtensionManager.class); /** * Whether to dump ALL data retrieved using {@link MirroringURIMavenRepository} as-is. * * @since 4.0.0-a20240601 */ @AvailableSince("4.0.0-a20240601") private static final boolean MIRROR_MAVEN_REQUESTS = Boolean.getBoolean("org.stianloader.sll.log.MIRROR_MAVEN_REQUESTS"); private final Map extensionClassloaders = new HashMap<>(); @NotNull private final List extensionList = new CopyOnWriteArrayList<>(); private final Map extensions = new ConcurrentHashMap<>(); @NotNull private final List immutableExtensionListView = Collections.unmodifiableList(this.extensionList); private boolean loaded; @NotNull @AvailableSince("4.0.0-a20240601") private final Path mavenCacheDir; @Deprecated @ScheduledForRemoval(inVersion = "5.0.0") public ExtensionManager() { this(Paths.get(".picoresolve-cache")); } public ExtensionManager(@NotNull Path mavenCacheDir) { this.mavenCacheDir = mavenCacheDir; } public void loadExtensions(List<@NotNull ? extends ExtensionPrototype> extensionCandidates) { if (this.loaded) { throw new IllegalStateException("Extensions are already loaded!"); } this.loaded = true; List discoveredExtensions = this.discoverExtensions(extensionCandidates); discoveredExtensions = this.generateLoadOrder(discoveredExtensions); // remove invalid extensions assert discoveredExtensions != null; discoveredExtensions.removeIf(ext -> ext.getLoadStatus() != DiscoveredExtension.LoadStatus.LOAD_SUCCESS); for (DiscoveredExtension extension : discoveredExtensions) { if (extension.loader != null) { try { extension.loader.close(); } catch (IOException e) { ExtensionManager.LOGGER.warn("Unable to close leftover classloader for extension {}", extension.getName(), e); } extension.loader = null; } if (extension.getLoadStatus() == LoadStatus.LOAD_SUCCESS) { extension.loader = this.newClassLoader(extension); } } this.setupAccessWideners(discoveredExtensions); this.setupCodeModifiers(discoveredExtensions); // remove invalid extensions discoveredExtensions.removeIf(ext -> ext.getLoadStatus() != DiscoveredExtension.LoadStatus.LOAD_SUCCESS); for (DiscoveredExtension discoveredExtension : discoveredExtensions) { if (discoveredExtension == null) { continue; } try { this.attemptSingleLoad(discoveredExtension); } catch (Exception e) { discoveredExtension.setLoadStatus(DiscoveredExtension.LoadStatus.LOAD_FAILED); e.printStackTrace(); ExtensionManager.LOGGER.error("Failed to load extension {}", discoveredExtension.getName(), e); } } } @SuppressWarnings("resource") @Nullable private Extension attemptSingleLoad(@NotNull DiscoveredExtension discoveredExtension) { // Create ExtensionDescription (authors, version etc.) final String extensionName = discoveredExtension.getName(); String mainClass = discoveredExtension.getEntrypoint(); Extension.ExtensionDescription extensionDescription = new Extension.ExtensionDescription( extensionName, discoveredExtension.getVersion(), Arrays.asList(discoveredExtension.getAuthors()), discoveredExtension ); MinestomExtensionClassLoader loader = discoveredExtension.loader; if (this.extensions.containsKey(extensionName.toLowerCase(Locale.ROOT))) { ExtensionManager.LOGGER.error("An extension called '{}' has already been registered.", extensionName); return null; } Class jarClass; try { jarClass = loader.loadClassAsChild(mainClass.replace('/', '.'), true); if (jarClass.getClassLoader() != loader) { throw new ClassNotFoundException("Class " + jarClass.getName() + " is loaded by classloader \"" + JavaInterop.getClassloaderName(jarClass.getClassLoader()) + "\", but expected it to be loaded by \"" + JavaInterop.getClassloaderName(loader) + "\""); } } catch (ClassNotFoundException e) { ExtensionManager.LOGGER.error("Could not find main class '{}' in extension '{}' with associated URLs '{}'.", mainClass, extensionName, extensionDescription.getOrigin().files, e); return null; } Class extensionClass; try { extensionClass = jarClass.asSubclass(Extension.class); } catch (ClassCastException e) { ExtensionManager.LOGGER.error("Main class '{}' in '{}' does not extend the 'Extension' superclass. Instead it directly extends '{}' from classloader '{}'", mainClass, extensionName, jarClass.getSuperclass().getName(), JavaInterop.getClassloaderName(jarClass.getSuperclass().getClassLoader()), e); return null; } Constructor 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) { ExtensionManager.LOGGER.error("Main class '{}' in '{}' does not define a no-args constructor.", mainClass, extensionName, e); return null; } Extension extension = null; try { ExtensionManager.CURRENTLY_LOADED_EXTENSION.set(extensionDescription); extension = constructor.newInstance(); } catch (InstantiationException e) { ExtensionManager.LOGGER.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) { ExtensionManager.LOGGER.error("While instantiating the main class '{}' in '{}' an exception was thrown.", mainClass, extensionName, e.getTargetException()); return null; } finally { ExtensionManager.CURRENTLY_LOADED_EXTENSION.set(null); } // 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) { LOGGER.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()); } } this.extensionList.add(extension); // add to a list, as lists preserve order this.extensions.put(extensionName.toLowerCase(), extension); return extension; } @NotNull private List discoverExtensions(List<@NotNull ? extends ExtensionPrototype> extensionCandidates) { List extensions = new LinkedList<>(); for (ExtensionPrototype prototype : extensionCandidates) { if (prototype.enabled) { DiscoveredExtension extension = this.discoverFromPrototype(prototype); if (extension == null) { ExtensionManager.LOGGER.debug("Ignoring prototype {} as no extension could be discovered from it's registered URLs.", prototype); } else if (extension.getLoadStatus() == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) { extensions.add(extension); } else { ExtensionManager.LOGGER.debug("Ignoring prototype {} as discovered failed (load status = " + extension.getLoadStatus() + ").", prototype); } } else { ExtensionManager.LOGGER.trace("Ignoring prototype {} as it is disabled.", prototype); } } return extensions; } @Nullable private DiscoveredExtension discoverFromPrototype(@NotNull ExtensionPrototype prototype) { List urls = prototype.originURLs; URLClassLoader discardLoader = MinestomRootClassLoader.getInstance().newChild(urls.toArray(new @NotNull URL[0])); try { URL resource = discardLoader.findResource("extension.json"); if (resource == null) { throw new IOException("Extension does not have an extension.json file: " + urls); } try (InputStream is = resource.openStream()) { DiscoveredExtension extension = DiscoveredExtension.fromJSON(is, prototype); extension.files.addAll(urls); // Verify integrity and ensure defaults DiscoveredExtension.verifyIntegrity(extension); discardLoader.close(); return extension; } } catch (IOException e) { try { discardLoader.close(); } catch (IOException e2) { UncheckedIOException unchecked = new UncheckedIOException("Cannot close temporary discard classloader", e2); unchecked.addSuppressed(e); throw unchecked; } e.printStackTrace(); return null; } catch (Throwable t) { try { discardLoader.close(); } catch (IOException e) { UncheckedIOException unchecked = new UncheckedIOException("Cannot close temporary discard classloader", e); unchecked.addSuppressed(t); throw unchecked; } throw t; } } @Nullable private List generateLoadOrder(@NotNull List discoveredExtensions) { // Do some mapping so we can map strings to extensions. Map extensionMap = new HashMap<>(); Map> dependencyMap = new HashMap<>(); for (DiscoveredExtension discoveredExtension : discoveredExtensions) { extensionMap.put(discoveredExtension.getName().toLowerCase(), discoveredExtension); } for (DiscoveredExtension discoveredExtension : discoveredExtensions) { @SuppressWarnings("null") List 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 (this.extensions.containsKey(dependencyName.toLowerCase(Locale.ROOT))) { return this.extensions.get(dependencyName.toLowerCase(Locale.ROOT)).getDescription().getOrigin(); } ExtensionManager.LOGGER.error("Extension {} requires an extension called {}.", discoveredExtension.getName(), dependencyName); ExtensionManager.LOGGER.error("However the extension {} could not be found.", dependencyName); ExtensionManager.LOGGER.error("Therefore {} will not be loaded.", discoveredExtension.getName()); discoveredExtension.setLoadStatus(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 sortedList = new LinkedList<>(); // entries with empty lists List>> 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> 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()) { LOGGER.error("SLL found {} cyclic mods.", dependencyMap.size()); LOGGER.error("Cyclic mods depend on each other and can therefore not be loaded."); for (Map.Entry> entry : dependencyMap.entrySet()) { DiscoveredExtension discoveredExtension = entry.getKey(); LOGGER.error("{} could not be loaded, as it depends on: {}.", discoveredExtension.getName(), entry.getValue().stream().map(DiscoveredExtension::getName).collect(Collectors.joining(", "))); } } return sortedList; } private boolean areAllDependenciesLoaded(List dependencies) { return dependencies.isEmpty() || dependencies.stream().allMatch(ext -> this.extensions.containsKey(ext.getName().toLowerCase())); } /** * Creates a new class loader for the given extension. * Will add the new loader as a child of all its dependencies' loaders. * * @param extension The extension that should own the classloader * @since 4.0.0-20230701 */ @SuppressWarnings("resource") @NotNull public MinestomExtensionClassLoader newClassLoader(@NotNull DiscoveredExtension extension) { MinestomRootClassLoader root = MinestomRootClassLoader.getInstance(); MinestomExtensionClassLoader loader = new MinestomExtensionClassLoader(extension.getName(), extension.files.toArray(new URL[0]), root); ExternalDependencies dependencies = extension.getExternalDependencies(); resolveDependencies: if (dependencies != null) { // Resolve maven dependencies List dependencyEdges = new ArrayList<>(); for (ExternalDependencyArtifact artifact : dependencies.getArtifacts()) { dependencyEdges.add(new DependencyEdge(artifact.getGroup(), artifact.getArtifact(), artifact.getClassifier(), artifact.getExtension(), artifact.getVersion(), Scope.RUNTIME, artifact.getExclusions())); } if (dependencyEdges.isEmpty()) { break resolveDependencies; } MavenResolver resolver = new MavenResolver(this.mavenCacheDir); for (ExternalRepository repo : dependencies.getRepositories()) { Path mirrorOut = (repo.isMirrorable() && ExtensionManager.MIRROR_MAVEN_REQUESTS) ? this.mavenCacheDir.resolve(".mirror-out") : null; if (!repo.isMirrorOnly() || ExtensionManager.MIRROR_MAVEN_REQUESTS) { resolver.addRepository(new MirroringURIMavenRepository(repo.getName(), URI.create(repo.getUrl()), mirrorOut)); } } GAV virtualGAV = new GAV("sll-virtual-dependency", extension.getName(), MavenVersion.parse(extension.getVersion())); DependencyLayerElement virtualElement = new DependencyLayerElement(virtualGAV, null, null, ExclusionContainer.empty(), dependencyEdges); DependencyLayer layer = new DependencyLayer(null, Collections.singletonList(virtualElement)); Executor executor = Objects.requireNonNull(ForkJoinPool.commonPool()); try { resolver.resolveAllChildren(layer, executor).get(60, TimeUnit.SECONDS); } catch (CompletionException | ExecutionException e) { ExtensionManager.LOGGER.error("Unable to fetch remote dependencies of extension {} v{}.", extension.getName(), extension.getVersion()); throw new RuntimeException("Unable to fetch remote dependencies of extension " + extension.getName() + ", version " + extension.getVersion(), e); } catch (TimeoutException | InterruptedException e) { ExtensionManager.LOGGER.error("Unable to fetch remote dependencies of extension {} v{} (timed out. Extensions have a maximum of 60 seconds to resolve remove dependencies).", extension.getName(), extension.getVersion()); throw new RuntimeException("Unable to fetch remote dependencies of extension " + extension.getName() + ", version " + extension.getVersion() + ": Timed out", e); } CompletableFuture>> combined = CompletableFuture.completedFuture(ConcurrentHashMap.newKeySet()); List dependencyGAVs = new ArrayList<>(); for (layer = layer.getChild(); layer != null; layer = layer.getChild()) { for (DependencyLayerElement element : layer.elements) { dependencyGAVs.add(element.gav); CompletableFuture> download = resolver.download(element.gav, element.classifier, element.type, executor); combined = combined.thenCombine(download, (set, downloadResult) -> { set.add(downloadResult); return set; }); } } Set> paths; try { paths = combined.get(60, TimeUnit.SECONDS); } catch (CompletionException | ExecutionException e) { ExtensionManager.LOGGER.error("Unable to download remote dependencies of extension {} v{}. Requested GAVs: {}", extension.getName(), extension.getVersion(), dependencyGAVs); throw new RuntimeException("Unable to download remote dependencies of extension " + extension.getName() + ", version " + extension.getVersion(), e); } catch (TimeoutException | InterruptedException e) { ExtensionManager.LOGGER.error("Unable to download remote dependencies of extension {} v{} (timed out. Extensions have a maximum of 60 seconds to resolve remove dependencies). Requested GAVs: {}", extension.getName(), extension.getVersion(), dependencyGAVs); throw new RuntimeException("Unable to download remote dependencies of extension " + extension.getName() + ", version " + extension.getVersion() + ": Timed out", e); } for (RepositoryAttachedValue value : paths) { String repoId = "null (cached in maven local, but without a known maven repository)"; MavenRepository repo = value.getRepository(); if (repo != null) { repoId = repo.getRepositoryId(); } ExtensionManager.LOGGER.debug("Adding {} to the classpath of extension '{}' v{}. The dependency was found in repository '{}'", value.getValue(), extension.getName(), extension.getVersion(), repoId); try { loader.addURL(value.getValue().toUri().toURL()); } catch (MalformedURLException e) { ExtensionManager.LOGGER.warn("Failed to add {} to the classpath of extension '{}' v{}. The dependency was found in repository '{}'", value.getValue(), extension.getName(), extension.getVersion(), repoId, e); } } } 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 missedOne = false; for (String dependency : extension.getDependencies()) { MinestomExtensionClassLoader parent = this.extensionClassloaders.get(dependency.toLowerCase(Locale.ROOT)); if (parent != null) { parent.addChild(loader); } else { missedOne = true; } } if (missedOne) { ExtensionManager.LOGGER.error("Could not load extension '{}' as it was not possible to find any of the following parents in the classloader hierarchy: {}. Following classloaders are currently registered: {}", extension.getName(), Arrays.toString(extension.getDependencies()), this.extensionClassloaders.keySet()); throw new RuntimeException("Could not load extension " + extension.getName() + " as it was not possible find any of the following parents inside classloader hierarchy (this indicates a likely issue with SLL internals): " + Arrays.toString(extension.getDependencies())); } } this.extensionClassloaders.put(extension.getName().toLowerCase(Locale.ROOT), loader); return loader; } @NotNull public List getExtensions() { return this.immutableExtensionListView; } @Nullable public Extension getExtension(@NotNull String name) { return this.extensions.get(name.toLowerCase()); } @SuppressWarnings("deprecation") private void setupAccessWideners(List extensionsToLoad) { for (DiscoveredExtension extension : extensionsToLoad) { if (extension.getLoadStatus() != LoadStatus.LOAD_SUCCESS || extension.getAccessWidener().equals("")) { continue; } URL entry = extension.loader.findResource(extension.getAccessWidener()); if (entry == null) { ExtensionManager.LOGGER.warn("Unable to find the access widener file for extension {}!", extension.getName()); continue; } try (InputStream awFile = entry.openStream()) { MinestomRootClassLoader.getInstance().readAccessWidener(awFile); } catch (IOException e) { e.printStackTrace(); ExtensionManager.LOGGER.warn("Failed to set up an access widener for {}!", extension.getName()); } } ReversibleAccessSetterTransformer transformer = null; for (ASMTransformer asmTransformer : MinestomRootClassLoader.getInstance().getTransformers()) { if (asmTransformer instanceof ReversibleAccessSetterTransformer) { transformer = (ReversibleAccessSetterTransformer) asmTransformer; break; } } if (transformer == null) { transformer = new ReversibleAccessSetterTransformer(); MinestomRootClassLoader.getInstance().addASMTransformer(transformer); } for (DiscoveredExtension extension : extensionsToLoad) { if (extension.getLoadStatus() != LoadStatus.LOAD_SUCCESS || extension.getReversibleAccessSetter().isEmpty()) { continue; } URL entry = extension.loader.findResource(extension.getReversibleAccessSetter()); if (entry == null) { ExtensionManager.LOGGER.warn("Unable to find the reversible access setter file for extension {}!", extension.getName()); continue; } try (InputStream rasFile = entry.openStream(); InputStreamReader isr = new InputStreamReader(rasFile, StandardCharsets.UTF_8); BufferedReader br = new BufferedReader(isr)) { transformer.getReverseContext().read(extension.getName(), br, true); } catch (IOException e) { e.printStackTrace(); ExtensionManager.LOGGER.warn("Failed to set up the reversed reversible access setter for {}!", extension.getName()); continue; } try (InputStream rasFile = entry.openStream(); InputStreamReader isr = new InputStreamReader(rasFile, StandardCharsets.UTF_8); BufferedReader br = new BufferedReader(isr)) { transformer.getMainContext().read(extension.getName(), br, false); } catch (IOException e) { e.printStackTrace(); ExtensionManager.LOGGER.warn("Failed to set up the standard reversible access setter for {}!", extension.getName()); continue; } } } /** * Extensions are allowed to apply Mixin transformers, the magic happens here. */ private void setupCodeModifiers(@NotNull List extensions) { // FIXME code modifiers persist (particularly asm transformers and mixins) even though the extension never started correctly ClassLoader cl = getClass().getClassLoader(); if (!(cl instanceof MinestomRootClassLoader)) { // The extension manager class is excluded from the root classloader, so yeah... cl = MinestomRootClassLoader.getInstance(); } @SuppressWarnings("resource") MinestomRootClassLoader modifiableClassLoader = (MinestomRootClassLoader) cl; ExtensionManager.LOGGER.info("Start loading code modifiers..."); for (DiscoveredExtension extension : extensions) { try { for (String codeModifierClass : extension.getCodeModifiers()) { modifiableClassLoader.loadModifier(extension.loader, codeModifierClass); } if (!extension.getMixinConfig().isEmpty()) { final String mixinConfigFile = extension.getMixinConfig(); Mixins.addConfiguration(mixinConfigFile); ExtensionManager.LOGGER.info("Found mixin in extension {}: {}", extension.getName(), mixinConfigFile); } } catch (Exception e) { ExtensionManager.LOGGER.error("Failed to load code modifier for extension in files: {}", extension.files, e); } } ExtensionManager.LOGGER.info("Done loading code modifiers."); } @SuppressWarnings({ "resource", "deprecation" }) 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 : this.extensionList) { e.getDescription().getDependents().remove(ext.getDescription().getName()); } String id = ext.getDescription().getName().toLowerCase(Locale.ROOT); // remove from loaded extensions this.extensions.remove(id); this.extensionList.remove(ext); // remove class loader, required to reload the classes MinestomExtensionClassLoader classloader = this.extensionClassloaders.remove(id); try { // close resources if (classloader != null) { classloader.close(); } } catch (IOException e) { e.printStackTrace(); } MinestomExtensionClassLoader modifierClassloader = ext.getDescription().getOrigin().loader; if (modifierClassloader != null) { try { modifierClassloader.close(); } catch (IOException e) { ExtensionManager.LOGGER.error("Unable to close extension codemodifier classloader", e); } ext.getDescription().getOrigin().loader = null; } } public void reload(String extensionName) { Extension ext = this.extensions.get(extensionName.toLowerCase()); if (ext == null) { throw new IllegalArgumentException("Extension " + extensionName + " is not currently loaded."); } ExtensionManager.LOGGER.info("Reload extension {}", extensionName); List dependents = new LinkedList<>(ext.getDescription().getDependents()); // copy dependents list List<@NotNull ExtensionPrototype> dependentsPrototypes = new LinkedList<>(); for (String dependentID : dependents) { Extension dependentExt = extensions.get(dependentID.toLowerCase()); dependentsPrototypes.add(dependentExt.getDescription().getOrigin().getSourcePrototype()); ExtensionManager.LOGGER.info("Unloading dependent extension {} (because it depends on {})", dependentID, extensionName); this.unload(dependentExt); } ExtensionManager.LOGGER.info("Unloading extension {}", extensionName); this.unload(ext); // Hint: 'ext' and its dependents should no longer be referenced from now on // re-discover extension to reload. We allow dependency changes, so we need to fully reload it List extensionsToReload = new LinkedList<>(); ExtensionManager.LOGGER.info("Rediscovering extension {}", extensionName); DiscoveredExtension rediscoveredExtension = this.discoverFromPrototype(ext.getDescription().getOrigin().getSourcePrototype()); extensionsToReload.add(rediscoveredExtension); for (ExtensionPrototype dependentPrototype : dependentsPrototypes) { // re-discover dependent extension to reload ExtensionManager.LOGGER.info("Rediscover dependent extension prototype '{}' (depends on {})", dependentPrototype, extensionName); extensionsToReload.add(this.discoverFromPrototype(dependentPrototype)); } // ensure correct order of dependencies this.loadExtensionList(extensionsToReload); } private boolean loadExtensionList(@NotNull List extensionsToLoad) { // ensure correct order of dependencies this.generateLoadOrder(extensionsToLoad); for (DiscoveredExtension extension : extensionsToLoad) { if (extension.loader != null) { try { extension.loader.close(); } catch (IOException e) { ExtensionManager.LOGGER.warn("Unable to close leftover classloader for extension {}", extension.getName(), e); } extension.loader = null; } if (extension.getLoadStatus() == LoadStatus.LOAD_SUCCESS) { extension.loader = this.newClassLoader(extension); } } this.setupAccessWideners(extensionsToLoad); // setup code modifiers for these extensions // TODO: it is possible that the new modifiers cannot be applied (because the targeted classes are already loaded), should we issue a warning? // If so, how? this.setupCodeModifiers(extensionsToLoad); List newExtensions = new LinkedList<>(); for (DiscoveredExtension toReload : extensionsToLoad) { // reload extensions ExtensionManager.LOGGER.info("Actually load extension {}", toReload.getName()); Extension loadedExtension = this.attemptSingleLoad(toReload); if (loadedExtension != null) { newExtensions.add(loadedExtension); } } if (newExtensions.isEmpty()) { ExtensionManager.LOGGER.error("No extensions to load, skipping callbacks"); return false; } ExtensionManager.LOGGER.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 dependents = new LinkedList<>(ext.getDescription().getDependents()); // copy dependents list for (String dependentID : dependents) { Extension dependentExt = this.extensions.get(dependentID.toLowerCase()); ExtensionManager.LOGGER.info("Unloading dependent extension {} (because it depends on {})", dependentID, extensionName); this.unload(dependentExt); } ExtensionManager.LOGGER.info("Unloading extension {}", extensionName); this.unload(ext); // call GC to try to get rid of classes and classloader System.gc(); } /** * Shutdowns all the extensions by unloading them. */ public void shutdown() { this.extensionList.forEach(this::unload); } }