mirror of
https://github.com/Minestom/Minestom.git
synced 2025-03-13 07:09:51 +01:00
850 lines
37 KiB
Java
850 lines
37 KiB
Java
package net.minestom.server.extensions;
|
|
|
|
import com.google.gson.Gson;
|
|
import net.minestom.dependencies.DependencyGetter;
|
|
import net.minestom.dependencies.ResolvedDependency;
|
|
import net.minestom.dependencies.maven.MavenRepository;
|
|
import net.minestom.server.MinecraftServer;
|
|
import net.minestom.server.extras.selfmodification.MinestomExtensionClassLoader;
|
|
import net.minestom.server.extras.selfmodification.MinestomRootClassLoader;
|
|
import net.minestom.server.ping.ResponseDataConsumer;
|
|
import net.minestom.server.utils.time.TimeUnit;
|
|
import net.minestom.server.utils.validate.Check;
|
|
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.spongepowered.asm.mixin.throwables.MixinError;
|
|
import org.spongepowered.asm.mixin.throwables.MixinException;
|
|
import org.spongepowered.asm.service.ServiceNotAvailableError;
|
|
|
|
import java.io.*;
|
|
import java.lang.reflect.Constructor;
|
|
import java.lang.reflect.Field;
|
|
import java.lang.reflect.InvocationTargetException;
|
|
import java.net.URL;
|
|
import java.util.*;
|
|
import java.util.stream.Collectors;
|
|
import java.util.zip.ZipFile;
|
|
|
|
public class ExtensionManager {
|
|
|
|
public final static String DISABLE_EARLY_LOAD_SYSTEM_KEY = "minestom.extension.disable_early_load";
|
|
|
|
public final static Logger LOGGER = LoggerFactory.getLogger(ExtensionManager.class);
|
|
|
|
public final static String INDEV_CLASSES_FOLDER = "minestom.extension.indevfolder.classes";
|
|
public final static String INDEV_RESOURCES_FOLDER = "minestom.extension.indevfolder.resources";
|
|
private final static Gson GSON = new Gson();
|
|
|
|
// LinkedHashMaps are HashMaps that preserve order
|
|
private final Map<String, Extension> extensions = new LinkedHashMap<>();
|
|
private final Map<String, Extension> immutableExtensions = Collections.unmodifiableMap(extensions);
|
|
|
|
private final File extensionFolder = new File("extensions");
|
|
private final File dependenciesFolder = new File(extensionFolder, ".libs");
|
|
private boolean loaded;
|
|
|
|
// Option
|
|
private boolean loadOnStartup = true;
|
|
|
|
public ExtensionManager() {
|
|
}
|
|
|
|
/**
|
|
* Gets if the extensions should be loaded during startup.
|
|
* <p>
|
|
* Default value is 'true'.
|
|
*
|
|
* @return true if extensions are loaded in {@link net.minestom.server.MinecraftServer#start(String, int, ResponseDataConsumer)}
|
|
*/
|
|
public boolean shouldLoadOnStartup() {
|
|
return loadOnStartup;
|
|
}
|
|
|
|
/**
|
|
* Used to specify if you want extensions to be loaded and initialized during startup.
|
|
* <p>
|
|
* Only useful before the server start.
|
|
*
|
|
* @param loadOnStartup true to load extensions on startup, false to do nothing
|
|
*/
|
|
public void setLoadOnStartup(boolean loadOnStartup) {
|
|
this.loadOnStartup = loadOnStartup;
|
|
}
|
|
|
|
/**
|
|
* Loads all extensions in the extension folder into this server.
|
|
* <br><br>
|
|
*
|
|
* Pipeline:
|
|
* <br>
|
|
* Finds all .jar files in the extensions folder.
|
|
* <br>
|
|
* Per each jar:
|
|
* <br>
|
|
* Turns its extension.json into a DiscoveredExtension object.
|
|
* <br>
|
|
* Verifies that all properties of extension.json are correctly set.
|
|
* <br><br>
|
|
*
|
|
* It then sorts all those jars by their load order (making sure that an extension's dependencies load before it)
|
|
* <br>
|
|
* Note: Cyclic dependencies will stop both extensions from being loaded.
|
|
* <br><br>
|
|
*
|
|
* Afterwards, it loads all external dependencies and adds them to the extension's files
|
|
* <br><br>
|
|
*
|
|
* Then removes any invalid extensions (Invalid being its Load Status isn't SUCCESS)
|
|
* <br><br>
|
|
*
|
|
* After that, it set its classloaders so each extension is self-contained,
|
|
* <br><br>
|
|
*
|
|
* Removes invalid extensions again,
|
|
* <br><br>
|
|
*
|
|
* and loads all of those extensions into Minestom
|
|
* <br>
|
|
* (Extension fields are set via reflection after each extension is verified, then loaded.)
|
|
* <br><br>
|
|
*
|
|
* If the extension successfully loads, add it to the global extension Map (Name to Extension)
|
|
* <br><br>
|
|
*
|
|
* And finally make a scheduler to clean observers per extension.
|
|
*/
|
|
public void loadExtensions() {
|
|
Check.stateCondition(loaded, "Extensions are already loaded!");
|
|
this.loaded = true;
|
|
|
|
// Initialize folders
|
|
{
|
|
// Make extensions folder if necessary
|
|
if (!extensionFolder.exists()) {
|
|
if (!extensionFolder.mkdirs()) {
|
|
LOGGER.error("Could not find or create the extension folder, extensions will not be loaded!");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Make dependencies folder if necessary
|
|
if (!dependenciesFolder.exists()) {
|
|
if (!dependenciesFolder.mkdirs()) {
|
|
LOGGER.error("Could not find nor create the extension dependencies folder, extensions will not be loaded!");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Periodically cleanup observers
|
|
MinecraftServer.getSchedulerManager().buildTask(() -> {
|
|
for (Extension ext : extensions.values()) {
|
|
ext.cleanupObservers();
|
|
}
|
|
}).repeat(1L, TimeUnit.MINUTE).schedule();
|
|
|
|
// Load extensions
|
|
{
|
|
// Get all extensions and order them accordingly.
|
|
List<DiscoveredExtension> discoveredExtensions = discoverExtensions();
|
|
|
|
// Don't waste resources on doing extra actions if there is nothing to do.
|
|
if (discoveredExtensions.isEmpty()) return;
|
|
|
|
discoveredExtensions = generateLoadOrder(discoveredExtensions);
|
|
loadDependencies(discoveredExtensions);
|
|
|
|
// remove invalid extensions
|
|
discoveredExtensions.removeIf(ext -> ext.loadStatus != DiscoveredExtension.LoadStatus.LOAD_SUCCESS);
|
|
|
|
// set class loaders for all extensions.
|
|
for (DiscoveredExtension discoveredExtension : discoveredExtensions) {
|
|
try {
|
|
discoveredExtension.setMinestomExtensionClassLoader(discoveredExtension.makeClassLoader());
|
|
} catch (Exception e) {
|
|
discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.FAILED_TO_SETUP_CLASSLOADER;
|
|
MinecraftServer.getExceptionManager().handleException(e);
|
|
LOGGER.error("Failed to load extension {}", discoveredExtension.getName());
|
|
LOGGER.error("Failed to load extension", e);
|
|
}
|
|
}
|
|
|
|
// remove invalid extensions
|
|
discoveredExtensions.removeIf(ext -> ext.loadStatus != DiscoveredExtension.LoadStatus.LOAD_SUCCESS);
|
|
setupCodeModifiers(discoveredExtensions);
|
|
|
|
// Load the extensions
|
|
for (DiscoveredExtension discoveredExtension : discoveredExtensions) {
|
|
try {
|
|
loadExtension(discoveredExtension);
|
|
} catch (Exception e) {
|
|
discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.LOAD_FAILED;
|
|
LOGGER.error("Failed to load extension {}", discoveredExtension.getName());
|
|
MinecraftServer.getExceptionManager().handleException(e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads an extension into Minestom.
|
|
*
|
|
* @param discoveredExtension The extension. Make sure to verify its integrity, set its class loader, and its files.
|
|
*
|
|
* @return An extension object made from this DiscoveredExtension
|
|
*/
|
|
@Nullable
|
|
private Extension loadExtension(@NotNull DiscoveredExtension discoveredExtension) {
|
|
// Create Extension (authors, version etc.)
|
|
String extensionName = discoveredExtension.getName();
|
|
String mainClass = discoveredExtension.getEntrypoint();
|
|
|
|
MinestomExtensionClassLoader loader = discoveredExtension.getMinestomExtensionClassLoader();
|
|
|
|
if (extensions.containsKey(extensionName.toLowerCase())) {
|
|
LOGGER.error("An extension called '{}' has already been registered.", extensionName);
|
|
return null;
|
|
}
|
|
|
|
Class<?> jarClass;
|
|
try {
|
|
jarClass = Class.forName(mainClass, true, loader);
|
|
} catch (ClassNotFoundException e) {
|
|
LOGGER.error("Could not find main class '{}' in extension '{}'.", mainClass, extensionName, e);
|
|
return null;
|
|
}
|
|
|
|
Class<? extends Extension> extensionClass;
|
|
try {
|
|
extensionClass = jarClass.asSubclass(Extension.class);
|
|
} catch (ClassCastException e) {
|
|
LOGGER.error("Main class '{}' in '{}' does not extend the 'Extension' superclass.", mainClass, extensionName, e);
|
|
return null;
|
|
}
|
|
|
|
Constructor<? extends Extension> 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) {
|
|
LOGGER.error("Main class '{}' in '{}' does not define a no-args constructor.", mainClass, extensionName, e);
|
|
return null;
|
|
}
|
|
Extension extension = null;
|
|
try {
|
|
extension = constructor.newInstance();
|
|
} catch (InstantiationException e) {
|
|
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) {
|
|
LOGGER.error(
|
|
"While instantiating the main class '{}' in '{}' an exception was thrown.",
|
|
mainClass,
|
|
extensionName,
|
|
e.getTargetException()
|
|
);
|
|
return null;
|
|
}
|
|
|
|
// Set extension origin to its DiscoveredExtension
|
|
try {
|
|
Field originField = Extension.class.getDeclaredField("origin");
|
|
originField.setAccessible(true);
|
|
originField.set(extension, discoveredExtension);
|
|
} catch (IllegalAccessException e) {
|
|
// We made it accessible, should not occur
|
|
} catch (NoSuchFieldException e) {
|
|
LOGGER.error("Main class '{}' in '{}' has no description field.", mainClass, extensionName, e);
|
|
return null;
|
|
}
|
|
|
|
// Set logger
|
|
try {
|
|
Field loggerField = Extension.class.getDeclaredField("logger");
|
|
loggerField.setAccessible(true);
|
|
loggerField.set(extension, LoggerFactory.getLogger(extensionClass));
|
|
} catch (IllegalAccessException e) {
|
|
// We made it accessible, should not occur
|
|
MinecraftServer.getExceptionManager().handleException(e);
|
|
} catch (NoSuchFieldException e) {
|
|
// This should also not occur (unless someone changed the logger in Extension superclass).
|
|
LOGGER.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 dependencyName : discoveredExtension.getDependencies()) {
|
|
Extension dependency = extensions.get(dependencyName.toLowerCase());
|
|
if (dependency == null) {
|
|
LOGGER.warn("Dependency {} of {} is null? This means the extension has been loaded without its dependency, which could cause issues later.", dependencyName, discoveredExtension.getName());
|
|
} else {
|
|
dependency.getDependents().add(discoveredExtension.getName());
|
|
}
|
|
}
|
|
|
|
// add to a linked hash map, as they preserve order
|
|
extensions.put(extensionName.toLowerCase(), extension);
|
|
|
|
return extension;
|
|
}
|
|
|
|
/**
|
|
* Get all extensions from the extensions folder and make them discovered.
|
|
*
|
|
* It skims the extension folder, discovers and verifies each extension, and returns those created DiscoveredExtensions.
|
|
*
|
|
* @return A list of discovered extensions from this folder.
|
|
*/
|
|
@NotNull
|
|
private List<DiscoveredExtension> discoverExtensions() {
|
|
List<DiscoveredExtension> extensions = new LinkedList<>();
|
|
|
|
File[] fileList = extensionFolder.listFiles();
|
|
|
|
if (fileList != null) {
|
|
// Loop through all files in extension folder
|
|
for (File file : fileList) {
|
|
|
|
// Ignore folders
|
|
if (file.isDirectory()) {
|
|
continue;
|
|
}
|
|
|
|
// Ignore non .jar files
|
|
if (!file.getName().endsWith(".jar")) {
|
|
continue;
|
|
}
|
|
|
|
DiscoveredExtension extension = discoverFromJar(file);
|
|
if (extension != null && extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) {
|
|
extensions.add(extension);
|
|
}
|
|
}
|
|
}
|
|
|
|
// this allows developers to have their extension discovered while working on it, without having to build a jar and put in the extension folder
|
|
if (System.getProperty(INDEV_CLASSES_FOLDER) != null && System.getProperty(INDEV_RESOURCES_FOLDER) != null) {
|
|
LOGGER.info("Found indev folders for extension. Adding to list of discovered extensions.");
|
|
final String extensionClasses = System.getProperty(INDEV_CLASSES_FOLDER);
|
|
final String extensionResources = System.getProperty(INDEV_RESOURCES_FOLDER);
|
|
try (InputStreamReader reader = new InputStreamReader(new FileInputStream(new File(extensionResources, "extension.json")))) {
|
|
DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class);
|
|
extension.files.add(new File(extensionClasses).toURI().toURL());
|
|
extension.files.add(new File(extensionResources).toURI().toURL());
|
|
|
|
// Verify integrity and ensure defaults
|
|
DiscoveredExtension.verifyIntegrity(extension);
|
|
|
|
if (extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) {
|
|
extensions.add(extension);
|
|
}
|
|
} catch (IOException e) {
|
|
MinecraftServer.getExceptionManager().handleException(e);
|
|
}
|
|
}
|
|
return extensions;
|
|
}
|
|
|
|
/**
|
|
* Grabs a discovered extension from a jar.
|
|
*
|
|
* @param file The jar to grab it from (a .jar is a formatted .zip file)
|
|
*
|
|
* @return The created DiscoveredExtension.
|
|
*/
|
|
@Nullable
|
|
private DiscoveredExtension discoverFromJar(@NotNull File file) {
|
|
try (ZipFile f = new ZipFile(file);
|
|
InputStreamReader reader = new InputStreamReader(f.getInputStream(f.getEntry("extension.json")))) {
|
|
|
|
// Initialize DiscoveredExtension from GSON.
|
|
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) {
|
|
MinecraftServer.getExceptionManager().handleException(e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@NotNull
|
|
private List<DiscoveredExtension> generateLoadOrder(@NotNull List<DiscoveredExtension> discoveredExtensions) {
|
|
// Extension --> Extensions it depends on.
|
|
Map<DiscoveredExtension, List<DiscoveredExtension>> dependencyMap = new HashMap<>();
|
|
|
|
// Put dependencies in dependency map
|
|
{
|
|
Map<String, DiscoveredExtension> extensionMap = new HashMap<>();
|
|
|
|
// go through all the discovered extensions and assign their name in a map.
|
|
for (DiscoveredExtension discoveredExtension : discoveredExtensions) {
|
|
extensionMap.put(discoveredExtension.getName().toLowerCase(), discoveredExtension);
|
|
}
|
|
|
|
allExtensions: // go through all the discovered extensions and get their dependencies as extensions
|
|
for (DiscoveredExtension discoveredExtension : discoveredExtensions) {
|
|
|
|
List<DiscoveredExtension> dependencies = new ArrayList<>(discoveredExtension.getDependencies().length);
|
|
|
|
// Map the dependencies into DiscoveredExtensions.
|
|
for (String dependencyName : discoveredExtension.getDependencies()) {
|
|
|
|
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 (extensions.containsKey(dependencyName.toLowerCase())) {
|
|
|
|
dependencies.add(extensions.get(dependencyName.toLowerCase()).getOrigin());
|
|
continue; // Go to the next loop in this dependency loop, this iteration is done.
|
|
|
|
} else {
|
|
|
|
// dependency isn't loaded, move on.
|
|
LOGGER.error("Extension {} requires an extension called {}.", discoveredExtension.getName(), dependencyName);
|
|
LOGGER.error("However the extension {} could not be found.", dependencyName);
|
|
LOGGER.error("Therefore {} will not be loaded.", discoveredExtension.getName());
|
|
discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.MISSING_DEPENDENCIES;
|
|
continue allExtensions; // the above labeled loop will go to the next extension as this dependency is invalid.
|
|
|
|
}
|
|
}
|
|
// This will add null for an unknown-extension
|
|
dependencies.add(dependencyExtension);
|
|
|
|
}
|
|
|
|
dependencyMap.put(
|
|
discoveredExtension,
|
|
dependencies
|
|
);
|
|
|
|
}
|
|
}
|
|
|
|
// List containing the load order.
|
|
LinkedList<DiscoveredExtension> sortedList = new LinkedList<>();
|
|
|
|
// TODO actually have to read this
|
|
{
|
|
// entries with empty lists
|
|
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 -> isLoaded(entry.getValue())).collect(Collectors.toList())
|
|
).isEmpty()
|
|
) {
|
|
// Get all "loadable" (not actually being loaded!) extensions and put them in the sorted list.
|
|
for (Map.Entry<DiscoveredExtension, List<DiscoveredExtension>> 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.
|
|
for (var dependencies : dependencyMap.values()) {
|
|
dependencies.remove(entry.getKey());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if there are cyclic extensions.
|
|
if (!dependencyMap.isEmpty()) {
|
|
LOGGER.error("Minestom found {} cyclic extensions.", dependencyMap.size());
|
|
LOGGER.error("Cyclic extensions depend on each other and can therefore not be loaded.");
|
|
for (Map.Entry<DiscoveredExtension, List<DiscoveredExtension>> 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;
|
|
}
|
|
|
|
/**
|
|
* Checks if this list of extensions are loaded
|
|
* @param extensions The list of extensions to check against.
|
|
* @return If all of these extensions are loaded.
|
|
*/
|
|
private boolean isLoaded(@NotNull List<DiscoveredExtension> extensions) {
|
|
return
|
|
extensions.isEmpty() // Don't waste CPU on checking an empty array
|
|
// Make sure the internal extensions list contains all of these.
|
|
|| extensions.stream().allMatch(ext -> this.extensions.containsKey(ext.getName().toLowerCase()));
|
|
}
|
|
|
|
private void loadDependencies(@NotNull List<DiscoveredExtension> extensions) {
|
|
List<DiscoveredExtension> allLoadedExtensions = new LinkedList<>(extensions);
|
|
|
|
for (Extension extension : immutableExtensions.values())
|
|
allLoadedExtensions.add(extension.getOrigin());
|
|
|
|
ExtensionDependencyResolver extensionDependencyResolver = new ExtensionDependencyResolver(allLoadedExtensions);
|
|
|
|
for (DiscoveredExtension discoveredExtension : extensions) {
|
|
try {
|
|
DependencyGetter getter = new DependencyGetter();
|
|
DiscoveredExtension.ExternalDependencies externalDependencies = discoveredExtension.getExternalDependencies();
|
|
List<MavenRepository> repoList = new LinkedList<>();
|
|
for (var repository : externalDependencies.repositories) {
|
|
|
|
if (repository.name == null) {
|
|
throw new IllegalStateException("Missing 'name' element in repository object.");
|
|
}
|
|
|
|
if (repository.name.isEmpty()) {
|
|
throw new IllegalStateException("Invalid 'name' element in repository object.");
|
|
}
|
|
|
|
if (repository.url == null) {
|
|
throw new IllegalStateException("Missing 'url' element in repository object.");
|
|
}
|
|
|
|
if (repository.url.isEmpty()) {
|
|
throw new IllegalStateException("Invalid 'url' element in repository object.");
|
|
}
|
|
|
|
repoList.add(new MavenRepository(repository.name, repository.url));
|
|
}
|
|
|
|
getter.addMavenResolver(repoList);
|
|
getter.addResolver(extensionDependencyResolver);
|
|
|
|
for (String artifact : externalDependencies.artifacts) {
|
|
var resolved = getter.get(artifact, dependenciesFolder);
|
|
addDependencyFile(resolved, discoveredExtension);
|
|
LOGGER.trace("Dependency of extension {}: {}", discoveredExtension.getName(), resolved);
|
|
}
|
|
|
|
for (String dependencyName : discoveredExtension.getDependencies()) {
|
|
var resolved = getter.get(dependencyName, dependenciesFolder);
|
|
addDependencyFile(resolved, discoveredExtension);
|
|
LOGGER.trace("Dependency of extension {}: {}", discoveredExtension.getName(), resolved);
|
|
}
|
|
} catch (Exception e) {
|
|
discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.MISSING_DEPENDENCIES;
|
|
LOGGER.error("Failed to load dependencies for extension {}", discoveredExtension.getName());
|
|
LOGGER.error("Extension '{}' will not be loaded", discoveredExtension.getName());
|
|
LOGGER.error("This is the exception", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void addDependencyFile(@NotNull ResolvedDependency dependency, @NotNull DiscoveredExtension extension) {
|
|
URL location = dependency.getContentsLocation();
|
|
extension.files.add(location);
|
|
LOGGER.trace("Added dependency {} to extension {} classpath", location.toExternalForm(), extension.getName());
|
|
|
|
// recurse to add full dependency tree
|
|
if (!dependency.getSubdependencies().isEmpty()) {
|
|
LOGGER.trace("Dependency {} has subdependencies, adding...", location.toExternalForm());
|
|
for (ResolvedDependency sub : dependency.getSubdependencies()) {
|
|
addDependencyFile(sub, extension);
|
|
}
|
|
LOGGER.trace("Dependency {} has had its subdependencies added.", location.toExternalForm());
|
|
}
|
|
}
|
|
|
|
@NotNull
|
|
public File getExtensionFolder() {
|
|
return extensionFolder;
|
|
}
|
|
|
|
@NotNull
|
|
public Collection<Extension> getExtensions() {
|
|
return immutableExtensions.values();
|
|
}
|
|
|
|
@Nullable
|
|
public Extension getExtension(@NotNull String name) {
|
|
return extensions.get(name.toLowerCase());
|
|
}
|
|
|
|
public boolean hasExtension(@NotNull String name) {
|
|
return extensions.containsKey(name);
|
|
}
|
|
|
|
/**
|
|
* Extensions are allowed to apply Mixin transformers, the magic happens here.
|
|
*/
|
|
private void setupCodeModifiers(@NotNull List<DiscoveredExtension> extensions) {
|
|
final ClassLoader cl = getClass().getClassLoader();
|
|
if (!(cl instanceof MinestomRootClassLoader)) {
|
|
LOGGER.warn("Current class loader is not a MinestomOverwriteClassLoader, but {}. " +
|
|
"This disables code modifiers (Mixin support is therefore disabled). " +
|
|
"This can be fixed by starting your server using Bootstrap#bootstrap (optional).", cl);
|
|
return;
|
|
}
|
|
MinestomRootClassLoader modifiableClassLoader = (MinestomRootClassLoader) cl;
|
|
setupCodeModifiers(extensions, modifiableClassLoader);
|
|
}
|
|
|
|
private void setupCodeModifiers(@NotNull List<DiscoveredExtension> extensions, MinestomRootClassLoader modifiableClassLoader) {
|
|
LOGGER.info("Start loading code modifiers...");
|
|
for (DiscoveredExtension extension : extensions) {
|
|
try {
|
|
for (String codeModifierClass : extension.getCodeModifiers()) {
|
|
boolean loaded = modifiableClassLoader.loadModifier(extension.files.toArray(new URL[0]), codeModifierClass);
|
|
if(!loaded) {
|
|
extension.addMissingCodeModifier(codeModifierClass);
|
|
}
|
|
}
|
|
if (!extension.getMixinConfig().isEmpty()) {
|
|
final String mixinConfigFile = extension.getMixinConfig();
|
|
try {
|
|
Mixins.addConfiguration(mixinConfigFile);
|
|
LOGGER.info("Found mixin in extension {}: {}", extension.getName(), mixinConfigFile);
|
|
} catch (ServiceNotAvailableError | MixinError | MixinException e) {
|
|
if(MinecraftServer.getExceptionManager() != null) {
|
|
MinecraftServer.getExceptionManager().handleException(e);
|
|
} else {
|
|
e.printStackTrace();
|
|
}
|
|
LOGGER.error("Could not load Mixin configuration: "+mixinConfigFile);
|
|
extension.setFailedToLoadMixinFlag();
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
if(MinecraftServer.getExceptionManager() != null) {
|
|
MinecraftServer.getExceptionManager().handleException(e);
|
|
} else {
|
|
e.printStackTrace();
|
|
}
|
|
LOGGER.error("Failed to load code modifier for extension in files: " +
|
|
extension.files
|
|
.stream()
|
|
.map(URL::toExternalForm)
|
|
.collect(Collectors.joining(", ")), e);
|
|
}
|
|
}
|
|
LOGGER.info("Done loading code modifiers.");
|
|
}
|
|
|
|
private void unload(@NotNull Extension ext) {
|
|
ext.preTerminate();
|
|
ext.terminate();
|
|
// remove callbacks for this extension
|
|
String extensionName = ext.getOrigin().getName();
|
|
ext.triggerChange(observer -> observer.onExtensionUnload(extensionName));
|
|
// TODO: more callback types
|
|
|
|
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 : extensions.values()) {
|
|
e.getDependents().remove(ext.getOrigin().getName());
|
|
}
|
|
|
|
String id = ext.getOrigin().getName().toLowerCase();
|
|
// remove from loaded extensions
|
|
extensions.remove(id);
|
|
|
|
// remove class loader, required to reload the classes
|
|
MinestomExtensionClassLoader classloader = ext.getOrigin().removeMinestomExtensionClassLoader();
|
|
try {
|
|
// close resources
|
|
classloader.close();
|
|
} catch (IOException e) {
|
|
MinecraftServer.getExceptionManager().handleException(e);
|
|
}
|
|
MinestomRootClassLoader.getInstance().removeChildInHierarchy(classloader);
|
|
}
|
|
|
|
public boolean reload(@NotNull String extensionName) {
|
|
Extension ext = extensions.get(extensionName.toLowerCase());
|
|
if (ext == null) {
|
|
throw new IllegalArgumentException("Extension " + extensionName + " is not currently loaded.");
|
|
}
|
|
|
|
File originalJar = ext.getOrigin().getOriginalJar();
|
|
if (originalJar == null) {
|
|
LOGGER.error("Cannot reload extension {} that is not from a .jar file!", extensionName);
|
|
return false;
|
|
}
|
|
|
|
LOGGER.info("Reload extension {} from jar file {}", extensionName, originalJar.getAbsolutePath());
|
|
List<String> dependents = new LinkedList<>(ext.getDependents()); // copy dependents list
|
|
List<File> originalJarsOfDependents = new LinkedList<>();
|
|
|
|
for (String dependentID : dependents) {
|
|
Extension dependentExt = extensions.get(dependentID.toLowerCase());
|
|
File dependentOriginalJar = dependentExt.getOrigin().getOriginalJar();
|
|
originalJarsOfDependents.add(dependentOriginalJar);
|
|
if (dependentOriginalJar == null) {
|
|
LOGGER.error("Cannot reload extension {} that is not from a .jar file!", dependentID);
|
|
return false;
|
|
}
|
|
|
|
LOGGER.info("Unloading dependent extension {} (because it depends on {})", dependentID, extensionName);
|
|
unload(dependentExt);
|
|
}
|
|
|
|
LOGGER.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
|
|
List<DiscoveredExtension> extensionsToReload = new LinkedList<>();
|
|
LOGGER.info("Rediscover extension {} from jar {}", extensionName, originalJar.getAbsolutePath());
|
|
DiscoveredExtension rediscoveredExtension = discoverFromJar(originalJar);
|
|
extensionsToReload.add(rediscoveredExtension);
|
|
|
|
for (File dependentJar : originalJarsOfDependents) {
|
|
// rediscover dependent extension to reload
|
|
LOGGER.info("Rediscover dependent extension (depends on {}) from jar {}", extensionName, dependentJar.getAbsolutePath());
|
|
extensionsToReload.add(discoverFromJar(dependentJar));
|
|
}
|
|
|
|
// ensure correct order of dependencies
|
|
loadExtensionList(extensionsToReload);
|
|
|
|
return true;
|
|
}
|
|
|
|
public boolean loadDynamicExtension(@NotNull File jarFile) throws FileNotFoundException {
|
|
if (!jarFile.exists()) {
|
|
throw new FileNotFoundException("File '" + jarFile.getAbsolutePath() + "' does not exists. Cannot load extension.");
|
|
}
|
|
|
|
LOGGER.info("Discover dynamic extension from jar {}", jarFile.getAbsolutePath());
|
|
DiscoveredExtension discoveredExtension = discoverFromJar(jarFile);
|
|
List<DiscoveredExtension> extensionsToLoad = Collections.singletonList(discoveredExtension);
|
|
return loadExtensionList(extensionsToLoad);
|
|
}
|
|
|
|
private boolean loadExtensionList(@NotNull List<DiscoveredExtension> extensionsToLoad) {
|
|
// ensure correct order of dependencies
|
|
LOGGER.debug("Reorder extensions to ensure proper load order");
|
|
extensionsToLoad = generateLoadOrder(extensionsToLoad);
|
|
loadDependencies(extensionsToLoad);
|
|
|
|
// setup new classloaders for the extensions to reload
|
|
for (DiscoveredExtension toReload : extensionsToLoad) {
|
|
LOGGER.debug("Setting up classloader for extension {}", toReload.getName());
|
|
toReload.setMinestomExtensionClassLoader(toReload.makeClassLoader());
|
|
}
|
|
|
|
// 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(extensionsToLoad);
|
|
|
|
List<Extension> newExtensions = new LinkedList<>();
|
|
for (DiscoveredExtension toReload : extensionsToLoad) {
|
|
// reload extensions
|
|
LOGGER.info("Actually load extension {}", toReload.getName());
|
|
Extension loadedExtension = loadExtension(toReload);
|
|
if (loadedExtension != null) {
|
|
newExtensions.add(loadedExtension);
|
|
}
|
|
}
|
|
|
|
if (newExtensions.isEmpty()) {
|
|
LOGGER.error("No extensions to load, skipping callbacks");
|
|
return false;
|
|
}
|
|
|
|
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(@NotNull 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.getDependents()); // copy dependents list
|
|
|
|
for (String dependentID : dependents) {
|
|
Extension dependentExt = extensions.get(dependentID.toLowerCase());
|
|
LOGGER.info("Unloading dependent extension {} (because it depends on {})", dependentID, extensionName);
|
|
unload(dependentExt);
|
|
}
|
|
|
|
LOGGER.info("Unloading extension {}", extensionName);
|
|
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() {
|
|
for (Extension extension : getExtensions()) {
|
|
extension.unload();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads code modifiers early, that is before <code>MinecraftServer.init()</code> is called.
|
|
*/
|
|
public static void loadCodeModifiersEarly() {
|
|
// allow users to disable early code modifier load
|
|
if ("true".equalsIgnoreCase(System.getProperty(DISABLE_EARLY_LOAD_SYSTEM_KEY))) {
|
|
return;
|
|
}
|
|
LOGGER.info("Early load of code modifiers from extensions.");
|
|
ExtensionManager manager = new ExtensionManager();
|
|
|
|
// discover extensions that are present
|
|
List<DiscoveredExtension> discovered = manager.discoverExtensions();
|
|
|
|
// setup extension class loaders, so that Mixin can load the json configuration file correctly
|
|
for (DiscoveredExtension e : discovered) {
|
|
e.setMinestomExtensionClassLoader(e.makeClassLoader());
|
|
}
|
|
|
|
// setup code modifiers and mixins
|
|
manager.setupCodeModifiers(discovered, MinestomRootClassLoader.getInstance());
|
|
|
|
// setup is done, remove all extension classloaders
|
|
for (Extension extension : manager.getExtensions()) {
|
|
MinestomRootClassLoader.getInstance().removeChildInHierarchy(extension.getOrigin().getMinestomExtensionClassLoader());
|
|
}
|
|
LOGGER.info("Early load of code modifiers from extensions done!");
|
|
}
|
|
|
|
/**
|
|
* Unloads all extensions
|
|
*/
|
|
public void unloadAllExtensions() {
|
|
// copy names, as the extensions map will be modified via the calls to unload
|
|
Set<String> extensionNames = new HashSet<>(extensions.keySet());
|
|
for (String ext : extensionNames) {
|
|
if (extensions.containsKey(ext)) { // is still loaded? Because extensions can depend on one another, it might have already been unloaded
|
|
unloadExtension(ext);
|
|
}
|
|
}
|
|
}
|
|
}
|