Merge pull request #129 from Minestom/improve-extension-system

Improve extension system
This commit is contained in:
TheMode 2021-03-02 18:22:21 +01:00 committed by GitHub
commit 29a8542d3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 970 additions and 45 deletions

View File

@ -105,6 +105,9 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.2'
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.6.2')
// Only here to ensure J9 module support for extensions and our classloaders
testCompileOnly "org.mockito:mockito-core:2.28.2"
// Netty
api 'io.netty:netty-handler:4.1.59.Final'
api 'io.netty:netty-codec:4.1.59.Final'

View File

@ -1,11 +1,13 @@
package net.minestom.server;
import net.minestom.server.extensions.ExtensionManager;
import net.minestom.server.extras.selfmodification.MinestomRootClassLoader;
import net.minestom.server.extras.selfmodification.mixins.MixinCodeModifier;
import net.minestom.server.extras.selfmodification.mixins.MixinServiceMinestom;
import org.spongepowered.asm.launch.MixinBootstrap;
import org.spongepowered.asm.launch.platform.CommandLineOptions;
import org.spongepowered.asm.mixin.Mixins;
import org.spongepowered.asm.service.ServiceNotAvailableError;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@ -20,7 +22,14 @@ public final class Bootstrap {
try {
ClassLoader classLoader = MinestomRootClassLoader.getInstance();
startMixin(args);
MinestomRootClassLoader.getInstance().addCodeModifier(new MixinCodeModifier());
try {
MinestomRootClassLoader.getInstance().addCodeModifier(new MixinCodeModifier());
} catch (RuntimeException e) {
e.printStackTrace();
System.err.println("Failed to add MixinCodeModifier, mixins will not be injected. Check the log entries above to debug.");
}
ExtensionManager.loadCodeModifiersEarly();
MixinServiceMinestom.gotoPreinitPhase();
// ensure extensions are loaded when starting the server
@ -44,7 +53,16 @@ public final class Bootstrap {
// hacks required to pass custom arguments
Method start = MixinBootstrap.class.getDeclaredMethod("start");
start.setAccessible(true);
if (!((boolean) start.invoke(null))) {
try {
if (!((boolean) start.invoke(null))) {
return;
}
} catch (ServiceNotAvailableError e) {
e.printStackTrace();
System.err.println("Failed to load Mixin, see error above.");
System.err.println("It is possible you simply have two files with identical names inside your server jar. " +
"Check your META-INF/services directory inside your Minestom implementation and merge files with identical names inside META-INF/services.");
return;
}

View File

@ -783,6 +783,7 @@ public final class MinecraftServer {
public static void stopCleanly() {
stopping = true;
LOGGER.info("Stopping Minestom server.");
extensionManager.unloadAllExtensions();
updateManager.stop();
schedulerManager.shutdown();
connectionManager.shutdown();

View File

@ -106,6 +106,7 @@ public class Entity implements Viewable, EventHandler, DataContainer, Permission
// Events
private final Map<Class<? extends Event>, Collection<EventCallback>> eventCallbacks = new ConcurrentHashMap<>();
private final Map<String, Collection<EventCallback<?>>> extensionCallbacks = new ConcurrentHashMap<>();
protected Metadata metadata = new Metadata(this);
protected EntityMeta entityMeta;
@ -706,6 +707,12 @@ public class Entity implements Viewable, EventHandler, DataContainer, Permission
return eventCallbacks;
}
@NotNull
@Override
public Collection<EventCallback<?>> getExtensionCallbacks(String extension) {
return extensionCallbacks.computeIfAbsent(extension, e -> new CopyOnWriteArrayList<>());
}
/**
* Each entity has an unique id (server-wide) which will change after a restart.
*

View File

@ -6,6 +6,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Object containing all the global event listeners.
@ -14,10 +15,17 @@ public final class GlobalEventHandler implements EventHandler {
// Events
private final Map<Class<? extends Event>, Collection<EventCallback>> eventCallbacks = new ConcurrentHashMap<>();
private final Map<String, Collection<EventCallback<?>>> extensionCallbacks = new ConcurrentHashMap<>();
@NotNull
@Override
public Map<Class<? extends Event>, Collection<EventCallback>> getEventCallbacksMap() {
return eventCallbacks;
}
@NotNull
@Override
public Collection<EventCallback<?>> getExtensionCallbacks(String extension) {
return extensionCallbacks.computeIfAbsent(extension, e -> new CopyOnWriteArrayList<>());
}
}

View File

@ -6,19 +6,21 @@ import net.minestom.server.event.CancellableEvent;
import net.minestom.server.event.Event;
import net.minestom.server.event.EventCallback;
import net.minestom.server.event.GlobalEventHandler;
import net.minestom.server.extensions.IExtensionObserver;
import net.minestom.server.extras.selfmodification.MinestomRootClassLoader;
import net.minestom.server.instance.Instance;
import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.stream.Stream;
/**
* Represents an element which can have {@link Event} listeners assigned to it.
*/
public interface EventHandler {
public interface EventHandler extends IExtensionObserver {
/**
* Gets a {@link Map} containing all the listeners assigned to a specific {@link Event} type.
@ -28,6 +30,15 @@ public interface EventHandler {
@NotNull
Map<Class<? extends Event>, Collection<EventCallback>> getEventCallbacksMap();
/**
* Gets a {@link Collection} containing all the listeners assigned to a specific extension (represented by its name).
* Used to unload all callbacks when the extension is unloaded
*
* @return a {@link Collection} with all the listeners
*/
@NotNull
Collection<EventCallback<?>> getExtensionCallbacks(String extension);
/**
* Adds a new event callback for the specified type {@code eventClass}.
*
@ -37,6 +48,12 @@ public interface EventHandler {
* @return true if the callback collection changed as a result of the call
*/
default <E extends Event> boolean addEventCallback(@NotNull Class<E> eventClass, @NotNull EventCallback<E> eventCallback) {
String extensionSource = MinestomRootClassLoader.findExtensionObjectOwner(eventCallback);
if(extensionSource != null) {
MinecraftServer.getExtensionManager().getExtension(extensionSource).observe(this);
getExtensionCallbacks(extensionSource).add(eventCallback);
};
Collection<EventCallback> callbacks = getEventCallbacks(eventClass);
return callbacks.add(eventCallback);
}
@ -51,6 +68,11 @@ public interface EventHandler {
*/
default <E extends Event> boolean removeEventCallback(@NotNull Class<E> eventClass, @NotNull EventCallback<E> eventCallback) {
Collection<EventCallback> callbacks = getEventCallbacks(eventClass);
String extensionSource = MinestomRootClassLoader.findExtensionObjectOwner(eventCallback);
if(extensionSource != null) {
getExtensionCallbacks(extensionSource).remove(eventCallback);
};
return callbacks.remove(eventCallback);
}
@ -126,10 +148,33 @@ public interface EventHandler {
}
}
/**
* Remove all event callbacks owned by the given extension
* @param extension the extension to remove callbacks from
*/
default void removeCallbacksOwnedByExtension(String extension) {
Collection<EventCallback<?>> extensionCallbacks = getExtensionCallbacks(extension);
for(EventCallback<?> callback : extensionCallbacks) {
// try to remove this callback from all callback collections
// we do this because we do not have information about the event class at this point
for(Collection<EventCallback> eventCallbacks : getEventCallbacksMap().values()) {
eventCallbacks.remove(callback);
}
}
extensionCallbacks.clear();
}
private <E extends Event> void runEvent(@NotNull Collection<EventCallback> eventCallbacks, @NotNull E event) {
for (EventCallback<E> eventCallback : eventCallbacks) {
eventCallback.run(event);
}
}
@Override
default void onExtensionUnload(String extensionName) {
removeCallbacksOwnedByExtension(extensionName);
}
}

View File

@ -23,6 +23,8 @@ final class DiscoveredExtension {
private String[] codeModifiers;
private String[] dependencies;
private ExternalDependencies externalDependencies;
private List<String> missingCodeModifiers = new LinkedList<>();
private boolean failedToLoadMixin = false;
transient List<URL> files = new LinkedList<>();
transient LoadStatus loadStatus = LoadStatus.LOAD_SUCCESS;
transient private File originalJar;
@ -138,6 +140,22 @@ final class DiscoveredExtension {
}
public void addMissingCodeModifier(String codeModifierClass) {
missingCodeModifiers.add(codeModifierClass);
}
public void setFailedToLoadMixinFlag() {
failedToLoadMixin = true;
}
public List<String> getMissingCodeModifiers() {
return missingCodeModifiers;
}
public boolean hasFailedToLoadMixin() {
return failedToLoadMixin;
}
enum LoadStatus {
LOAD_SUCCESS("Actually, it did not fail. This message should not have been printed."),
MISSING_DEPENDENCIES("Missing dependencies, check your logs."),

View File

@ -3,8 +3,12 @@ package net.minestom.server.extensions;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import java.util.ArrayList;
import java.util.List;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
public abstract class Extension {
// Set by reflection
@ -14,6 +18,15 @@ public abstract class Extension {
@SuppressWarnings("unused")
private Logger logger;
/**
* Observers that will be notified of events related to this extension.
* Kept as WeakReference because entities can be observers, but could become candidate to be garbage-collected while
* this extension holds a reference to it. A WeakReference makes sure this extension does not prevent the memory
* from being cleaned up.
*/
private Set<WeakReference<IExtensionObserver>> observers = Collections.newSetFromMap(new ConcurrentHashMap<>());
private ReferenceQueue<IExtensionObserver> observerReferenceQueue = new ReferenceQueue<>();
protected Extension() {
}
@ -55,12 +68,54 @@ public abstract class Extension {
return logger;
}
/**
* Adds a new observer to this extension.
* Will be kept as a WeakReference.
* @param observer
*/
public void observe(IExtensionObserver observer) {
observers.add(new WeakReference<>(observer, observerReferenceQueue));
}
/**
* Calls some action on all valid observers of this extension
* @param action code to execute on each observer
*/
public void triggerChange(Consumer<IExtensionObserver> action) {
for(WeakReference<IExtensionObserver> weakObserver : observers) {
IExtensionObserver observer = weakObserver.get();
if(observer != null) {
action.accept(observer);
}
}
}
/**
* If this extension registers code modifiers and/or mixins, are they loaded correctly?
*/
public boolean areCodeModifiersAllLoadedCorrectly() {
return !getDescription().failedToLoadMixin && getDescription().getMissingCodeModifiers().isEmpty();
}
/**
* Removes all expired reference to observers
*
* @see #observers
*/
public void cleanupObservers() {
Reference<? extends IExtensionObserver> ref;
while((ref = observerReferenceQueue.poll()) != null) {
observers.remove(ref);
}
}
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 List<String> missingCodeModifiers = new LinkedList<>();
private final boolean failedToLoadMixin;
private final DiscoveredExtension origin;
ExtensionDescription(@NotNull String name, @NotNull String version, @NotNull List<String> authors, @NotNull DiscoveredExtension origin) {
@ -68,6 +123,8 @@ public abstract class Extension {
this.version = version;
this.authors = authors;
this.origin = origin;
failedToLoadMixin = origin.hasFailedToLoadMixin();
missingCodeModifiers.addAll(origin.getMissingCodeModifiers());
}
@NotNull
@ -94,5 +151,15 @@ public abstract class Extension {
DiscoveredExtension getOrigin() {
return origin;
}
@NotNull
public List<String> getMissingCodeModifiers() {
return missingCodeModifiers;
}
@NotNull
public boolean hasFailedToLoadMixin() {
return failedToLoadMixin;
}
}
}

View File

@ -2,17 +2,22 @@ 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;
@ -27,10 +32,12 @@ 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);
private final static String INDEV_CLASSES_FOLDER = "minestom.extension.indevfolder.classes";
private final static String INDEV_RESOURCES_FOLDER = "minestom.extension.indevfolder.resources";
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();
private final Map<String, MinestomExtensionClassLoader> extensionLoaders = new HashMap<>();
@ -118,6 +125,13 @@ public class ExtensionManager {
MinecraftServer.getExceptionManager().handleException(e);
}
}
// periodically cleanup observers
MinecraftServer.getSchedulerManager().buildTask(() -> {
for(Extension ext : extensionList) {
ext.cleanupObservers();
}
}).repeat(1L, TimeUnit.MINUTE).schedule();
}
private void setupClassLoader(@NotNull DiscoveredExtension discoveredExtension) {
@ -235,16 +249,19 @@ public class ExtensionManager {
@NotNull
private List<DiscoveredExtension> discoverExtensions() {
List<DiscoveredExtension> extensions = new LinkedList<>();
for (File file : extensionFolder.listFiles()) {
if (file.isDirectory()) {
continue;
}
if (!file.getName().endsWith(".jar")) {
continue;
}
DiscoveredExtension extension = discoverFromJar(file);
if (extension != null && extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) {
extensions.add(extension);
File[] fileList = extensionFolder.listFiles();
if(fileList != null) {
for (File file : fileList) {
if (file.isDirectory()) {
continue;
}
if (!file.getName().endsWith(".jar")) {
continue;
}
DiscoveredExtension extension = discoverFromJar(file);
if (extension != null && extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) {
extensions.add(extension);
}
}
}
@ -397,13 +414,13 @@ public class ExtensionManager {
for (var artifact : externalDependencies.artifacts) {
var resolved = getter.get(artifact, dependenciesFolder);
addDependencyFile(resolved.getContentsLocation(), ext);
addDependencyFile(resolved, ext);
LOGGER.trace("Dependency of extension {}: {}", ext.getName(), resolved);
}
for (var dependencyName : ext.getDependencies()) {
var resolved = getter.get(dependencyName, dependenciesFolder);
addDependencyFile(resolved.getContentsLocation(), ext);
addDependencyFile(resolved, ext);
LOGGER.trace("Dependency of extension {}: {}", ext.getName(), resolved);
}
} catch (Exception e) {
@ -415,9 +432,19 @@ public class ExtensionManager {
}
}
private void addDependencyFile(URL dependency, DiscoveredExtension extension) {
extension.files.add(dependency);
LOGGER.trace("Added dependency {} to extension {} classpath", dependency.toExternalForm(), extension.getName());
private void addDependencyFile(ResolvedDependency dependency, 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());
}
}
/**
@ -429,7 +456,7 @@ public class ExtensionManager {
@NotNull
public MinestomExtensionClassLoader newClassLoader(@NotNull DiscoveredExtension extension, @NotNull URL[] urls) {
MinestomRootClassLoader root = MinestomRootClassLoader.getInstance();
MinestomExtensionClassLoader loader = new MinestomExtensionClassLoader(extension.getName(), urls, root);
MinestomExtensionClassLoader loader = new MinestomExtensionClassLoader(extension.getName(), extension.getEntrypoint(), urls, root);
if (extension.getDependencies().length == 0) {
// orphaned extension, we can insert it directly
root.addChild(loader);
@ -469,7 +496,7 @@ public class ExtensionManager {
}
@NotNull
public Map<String, URLClassLoader> getExtensionLoaders() {
public Map<String, MinestomExtensionClassLoader> getExtensionLoaders() {
return new HashMap<>(extensionLoaders);
}
@ -485,19 +512,40 @@ public class ExtensionManager {
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()) {
modifiableClassLoader.loadModifier(extension.files.toArray(new File[0]), codeModifierClass);
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();
Mixins.addConfiguration(mixinConfigFile);
LOGGER.info("Found mixin in extension {}: {}", extension.getName(), mixinConfigFile);
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) {
MinecraftServer.getExceptionManager().handleException(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()
@ -511,6 +559,11 @@ public class ExtensionManager {
private void unload(Extension ext) {
ext.preTerminate();
ext.terminate();
// remove callbacks for this extension
String extensionName = ext.getDescription().getName();
ext.triggerChange(observer -> observer.onExtensionUnload(extensionName));
// TODO: more callback types
ext.postTerminate();
ext.unload();
@ -664,4 +717,46 @@ public class ExtensionManager {
public void shutdown() {
this.extensionList.forEach(this::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) {
manager.setupClassLoader(e);
}
// setup code modifiers and mixins
manager.setupCodeModifiers(discovered, MinestomRootClassLoader.getInstance());
// setup is done, remove all extension classloaders
for(MinestomExtensionClassLoader extensionLoader : manager.getExtensionLoaders().values()) {
MinestomRootClassLoader.getInstance().removeChildInHierarchy(extensionLoader);
}
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);
}
}
}
}

View File

@ -0,0 +1,14 @@
package net.minestom.server.extensions;
/**
* Observes events related to extensions
*/
public interface IExtensionObserver {
/**
* Called before unloading an extension (that is, right after Extension#terminate and right before Extension#unload)
* @param extensionName the name of the extension that is being unloaded
*/
void onExtensionUnload(String extensionName);
}

View File

@ -10,9 +10,34 @@ public class MinestomExtensionClassLoader extends HierarchyClassLoader {
*/
private final MinestomRootClassLoader root;
public MinestomExtensionClassLoader(String name, URL[] urls, MinestomRootClassLoader root) {
super(name, urls, root);
/**
* Main of the main class of the extension linked to this classloader
*/
private final String mainClassName;
public MinestomExtensionClassLoader(String extensionName, String mainClassName, URL[] urls, MinestomRootClassLoader root) {
super(extensionName, urls, root);
this.root = root;
this.mainClassName = mainClassName;
}
/**
* Returns the name of the extension linked to this classloader
* @return the name of the extension linked to this classloader
*/
public String getExtensionName() {
// simply calls ClassLoader#getName as the extension name is used to name this classloader
// this method is simply for ease-of-use
return getName();
}
/**
* Returns the main class name linked to the extension responsible for this classloader.
* Used by the root classloader to let extensions load themselves in a dev environment.
* @return the main class name linked to the extension responsible for this classloader
*/
public String getMainClassName() {
return mainClassName;
}
@Override
@ -72,4 +97,16 @@ public class MinestomExtensionClassLoader extends HierarchyClassLoader {
super.finalize();
System.err.println("Class loader "+getName()+" finalized.");
}
/**
* Is the given class name the name of the entry point of one the extensions from this classloader chain?
* @param name the class name to check
* @return whether the given class name the name of the entry point of one the extensions from this classloader chain
* @see MinestomRootClassLoader#loadBytes(String, boolean) for more information
*/
protected boolean isMainExtensionClass(String name) {
if(mainClassName.equals(name))
return true;
return children.stream().anyMatch(c -> c.isMainExtensionClass(name));
}
}

View File

@ -1,7 +1,9 @@
package net.minestom.server.extras.selfmodification;
import net.minestom.server.MinecraftServer;
import net.minestom.server.extensions.ExtensionManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
@ -65,9 +67,21 @@ public class MinestomRootClassLoader extends HierarchyClassLoader {
// TODO: priorities?
private final List<CodeModifier> modifiers = new LinkedList<>();
/**
* Whether Minestom detected that it is running in a dev environment.
* Determined by the existence of the system property {@link ExtensionManager#INDEV_CLASSES_FOLDER}
*/
private boolean inDevEnvironment = false;
/**
* List of already loaded code modifier class names. This prevents loading the same class twice.
*/
private final Set<String> alreadyLoadedCodeModifiers = new HashSet<>();
private MinestomRootClassLoader(ClassLoader parent) {
super("Minestom Root ClassLoader", extractURLsFromClasspath(), parent);
asmClassLoader = newChild(new URL[0]);
inDevEnvironment = System.getProperty(ExtensionManager.INDEV_CLASSES_FOLDER) != null;
}
public static MinestomRootClassLoader getInstance() {
@ -179,6 +193,22 @@ public class MinestomRootClassLoader extends HierarchyClassLoader {
if (name == null)
throw new ClassNotFoundException();
String path = name.replace(".", "/") + ".class";
if(inDevEnvironment) {
// check if the class to load is the entry point of the extension
boolean isMainExtensionClass = false;
for(MinestomExtensionClassLoader c : children) {
if(c.isMainExtensionClass(name)) {
isMainExtensionClass = true;
break;
}
}
if(isMainExtensionClass) { // entry point of the extension, force load through extension classloader
throw new ClassNotFoundException("The class "+name+" is the entry point of an extension. " +
"Because we are in a dev environment, we force its load through its extension classloader, " +
"even though the root classloader has access.");
}
}
InputStream input = getResourceAsStream(path);
if (input == null) {
throw new ClassNotFoundException("Could not find resource " + path);
@ -245,12 +275,17 @@ public class MinestomRootClassLoader extends HierarchyClassLoader {
return URLClassLoader.newInstance(urls, this);
}
public void loadModifier(File[] originFiles, String codeModifierClass) {
URL[] urls = new URL[originFiles.length];
/**
* Loads a code modifier.
* @param urls
* @param codeModifierClass
* @return whether the modifier has been loaded. Returns 'true' even if the code modifier is already loaded before calling this method
*/
public boolean loadModifier(URL[] urls, String codeModifierClass) {
if(alreadyLoadedCodeModifiers.contains(codeModifierClass)) {
return true;
}
try {
for (int i = 0; i < originFiles.length; i++) {
urls[i] = originFiles[i].toURI().toURL();
}
URLClassLoader loader = newChild(urls);
Class<?> modifierClass = loader.loadClass(codeModifierClass);
if (CodeModifier.class.isAssignableFrom(modifierClass)) {
@ -258,11 +293,18 @@ public class MinestomRootClassLoader extends HierarchyClassLoader {
synchronized (modifiers) {
LOGGER.warn("Added Code modifier: {}", modifier);
addCodeModifier(modifier);
alreadyLoadedCodeModifiers.add(codeModifierClass);
}
}
} catch (MalformedURLException | ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) {
MinecraftServer.getExceptionManager().handleException(e);
return true;
} catch (ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) {
if(MinecraftServer.getExceptionManager() != null) {
MinecraftServer.getExceptionManager().handleException(e);
} else {
e.printStackTrace();
}
}
return false;
}
public void addCodeModifier(CodeModifier modifier) {
@ -279,4 +321,24 @@ public class MinestomRootClassLoader extends HierarchyClassLoader {
public List<CodeModifier> getModifiers() {
return modifiers;
}
/**
* Tries to know which extension created this object, based on the classloader of the object. This can only check that the class of the object has been loaded
* by an extension.
*
* While not perfect, this should detect any callback created via extension code.
* It is possible this current version of the implementation might struggle with callbacks created through external
* libraries, but as libraries are loaded separately for each extension, this *should not*(tm) be a problem.
*
* @param obj the object to get the extension of
* @return <code>null</code> if no extension has been found, otherwise the extension name
*/
@Nullable
public static String findExtensionObjectOwner(@NotNull Object obj) {
ClassLoader cl = obj.getClass().getClassLoader();
if(cl instanceof MinestomExtensionClassLoader) {
return ((MinestomExtensionClassLoader) cl).getExtensionName();
}
return null;
}
}

View File

@ -41,6 +41,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;
@ -79,6 +80,7 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
private long lastTickAge = System.currentTimeMillis();
private final Map<Class<? extends Event>, Collection<EventCallback>> eventCallbacks = new ConcurrentHashMap<>();
private final Map<String, Collection<EventCallback<?>>> extensionCallbacks = new ConcurrentHashMap<>();
// Entities present in this instance
protected final Set<Entity> entities = new CopyOnWriteArraySet<>();
@ -864,6 +866,12 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
return eventCallbacks;
}
@NotNull
@Override
public Collection<EventCallback<?>> getExtensionCallbacks(String extension) {
return extensionCallbacks.computeIfAbsent(extension, e -> new CopyOnWriteArrayList<>());
}
// UNSAFE METHODS (need most of time to be synchronized)
/**

View File

@ -4,15 +4,17 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectCollection;
import net.minestom.server.MinecraftServer;
import net.minestom.server.extensions.Extension;
import net.minestom.server.extensions.IExtensionObserver;
import net.minestom.server.utils.thread.MinestomThread;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
* An object which manages all the {@link Task}'s.
@ -24,7 +26,7 @@ import java.util.concurrent.atomic.AtomicInteger;
* <p>
* Shutdown tasks are built with {@link #buildShutdownTask(Runnable)} and are executed, as the name implies, when the server stops.
*/
public final class SchedulerManager {
public final class SchedulerManager implements IExtensionObserver {
private static boolean instanced;
// A counter for all normal tasks
@ -40,6 +42,16 @@ public final class SchedulerManager {
// All the registered shutdown tasks (task id = task)
protected final Int2ObjectMap<Task> shutdownTasks;
/**
* Tasks scheduled through extensions
*/
private final Map<String, List<Task>> extensionTasks = new ConcurrentHashMap<>();
/**
* Shutdown tasks scheduled through extensions
*/
private final Map<String, List<Task>> extensionShutdownTasks = new ConcurrentHashMap<>();
/**
* Default constructor
*/
@ -169,4 +181,62 @@ public final class SchedulerManager {
public ScheduledExecutorService getTimerExecutionService() {
return timerExecutionService;
}
/**
* Called when a Task from an extension is scheduled.
* @param owningExtension the name of the extension which scheduled the task
* @param task the task that has been scheduled
*/
void onScheduleFromExtension(String owningExtension, Task task) {
List<Task> scheduledForThisExtension = extensionTasks.computeIfAbsent(owningExtension, s -> new CopyOnWriteArrayList<>());
scheduledForThisExtension.add(task);
Extension ext = MinecraftServer.getExtensionManager().getExtension(owningExtension);
ext.observe(this);
}
/**
* Called when a Task from an extension is scheduled for server shutdown.
* @param owningExtension the name of the extension which scheduled the task
* @param task the task that has been scheduled
*/
void onScheduleShutdownFromExtension(String owningExtension, Task task) {
List<Task> scheduledForThisExtension = extensionShutdownTasks.computeIfAbsent(owningExtension, s -> new CopyOnWriteArrayList<>());
scheduledForThisExtension.add(task);
Extension ext = MinecraftServer.getExtensionManager().getExtension(owningExtension);
ext.observe(this);
}
/**
* Unschedules all non-transient tasks ({@link Task#isTransient()}) from this scheduler. Tasks are allowed to complete
* @param extension the name of the extension to unschedule tasks from
* @see Task#isTransient()
*/
public void removeExtensionTasksOnUnload(String extension) {
List<Task> scheduledForThisExtension = extensionTasks.get(extension);
if(scheduledForThisExtension != null) {
List<Task> toCancel = scheduledForThisExtension.stream()
.filter(t -> !t.isTransient())
.collect(Collectors.toList());
toCancel.forEach(Task::cancel);
scheduledForThisExtension.removeAll(toCancel);
}
List<Task> shutdownScheduledForThisExtension = extensionShutdownTasks.get(extension);
if(shutdownScheduledForThisExtension != null) {
List<Task> toCancel = shutdownScheduledForThisExtension.stream()
.filter(t -> !t.isTransient())
.collect(Collectors.toList());
toCancel.forEach(Task::cancel);
shutdownScheduledForThisExtension.removeAll(toCancel);
shutdownTasks.values().removeAll(toCancel);
}
}
@Override
public void onExtensionUnload(String extensionName) {
removeExtensionTasksOnUnload(extensionName);
}
}

View File

@ -3,6 +3,7 @@ package net.minestom.server.timer;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import net.minestom.server.MinecraftServer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.concurrent.ScheduledFuture;
@ -27,6 +28,16 @@ public class Task implements Runnable {
private final long delay;
// Repeat value for the task execution
private final long repeat;
/** Extension which owns this task, or null if none */
private final String owningExtension;
/**
* If this task is owned by an extension, should it survive the unloading of said extension?
* May be useful for delay tasks, but it can prevent the extension classes from being unloaded, and preventing a full
* reload of that extension.
*/
private final boolean isTransient;
// Task completion/execution
private ScheduledFuture<?> future;
// The thread of the task
@ -41,13 +52,15 @@ public class Task implements Runnable {
* @param delay The time to delay
* @param repeat The time until the repetition
*/
public Task(@NotNull SchedulerManager schedulerManager, @NotNull Runnable runnable, boolean shutdown, long delay, long repeat) {
public Task(@NotNull SchedulerManager schedulerManager, @NotNull Runnable runnable, boolean shutdown, long delay, long repeat, boolean isTransient, @Nullable String owningExtension) {
this.schedulerManager = schedulerManager;
this.runnable = runnable;
this.shutdown = shutdown;
this.id = shutdown ? this.schedulerManager.getShutdownCounterIdentifier() : this.schedulerManager.getCounterIdentifier();
this.delay = delay;
this.repeat = repeat;
this.isTransient = isTransient;
this.owningExtension = owningExtension;
}
/**
@ -87,6 +100,9 @@ public class Task implements Runnable {
* Sets up the task for correct execution.
*/
public void schedule() {
if(owningExtension != null) {
this.schedulerManager.onScheduleFromExtension(owningExtension, this);
}
this.future = this.repeat == 0L ?
this.schedulerManager.getTimerExecutionService().schedule(this, this.delay, TimeUnit.MILLISECONDS) :
this.schedulerManager.getTimerExecutionService().scheduleAtFixedRate(this, this.delay, this.repeat, TimeUnit.MILLISECONDS);
@ -150,6 +166,22 @@ public class Task implements Runnable {
return id == task.id;
}
/**
* If this task is owned by an extension, should it survive the unloading of said extension?
* May be useful for delay tasks, but it can prevent the extension classes from being unloaded, and preventing a full
* reload of that extension.
*/
public boolean isTransient() {
return isTransient;
}
/**
* Extension which owns this task, or null if none
*/
public String getOwningExtension() {
return owningExtension;
}
@Override
public int hashCode() {
return Objects.hash(id);

View File

@ -1,6 +1,7 @@
package net.minestom.server.timer;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import net.minestom.server.extras.selfmodification.MinestomRootClassLoader;
import net.minestom.server.utils.time.TimeUnit;
import org.jetbrains.annotations.NotNull;
@ -18,10 +19,18 @@ public class TaskBuilder {
private final Runnable runnable;
// True if the task planned for the application shutdown
private final boolean shutdown;
/** Extension which owns this task, or null if none */
private final String owningExtension;
// Delay value for the task execution
private long delay;
// Repeat value for the task execution
private long repeat;
/**
* If this task is owned by an extension, should it survive the unloading of said extension?
* May be useful for delay tasks, but it can prevent the extension classes from being unloaded, and preventing a full
* reload of that extension.
*/
private boolean isTransient;
/**
* Creates a task builder.
@ -46,6 +55,8 @@ public class TaskBuilder {
this.schedulerManager = schedulerManager;
this.runnable = runnable;
this.shutdown = shutdown;
this.isTransient = false;
this.owningExtension = MinestomRootClassLoader.findExtensionObjectOwner(runnable);
}
/**
@ -96,6 +107,14 @@ public class TaskBuilder {
return this;
}
/** If this task is owned by an extension, should it survive the unloading of said extension?
* May be useful for delay tasks, but it can prevent the extension classes from being unloaded, and preventing a full
* reload of that extension. */
public TaskBuilder makeTransient() {
isTransient = true;
return this;
}
/**
* Schedules this {@link Task} for execution.
*
@ -108,12 +127,17 @@ public class TaskBuilder {
this.runnable,
this.shutdown,
this.delay,
this.repeat);
this.repeat,
this.isTransient,
this.owningExtension);
if (this.shutdown) {
Int2ObjectMap<Task> shutdownTasks = this.schedulerManager.shutdownTasks;
synchronized (shutdownTasks) {
shutdownTasks.put(task.getId(), task);
}
if(owningExtension != null) {
this.schedulerManager.onScheduleShutdownFromExtension(owningExtension, task);
}
} else {
Int2ObjectMap<Task> tasks = this.schedulerManager.tasks;
synchronized (tasks) {

View File

@ -0,0 +1,34 @@
package improveextensions;
import net.minestom.server.MinecraftServer;
import net.minestom.server.extensions.Extension;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.world.DimensionType;
import org.junit.jupiter.api.Assertions;
import org.opentest4j.AssertionFailedError;
import java.util.UUID;
/**
* Extensions should be able to use Mixins for classes loaded very early by Minestom (InstanceContainer for instance)
*/
public class DisableEarlyLoad extends Extension {
@Override
public void initialize() {
// force load of InstanceContainer class
InstanceContainer c = new InstanceContainer(UUID.randomUUID(), DimensionType.OVERWORLD, null);
System.out.println(c.toString());
try {
Assertions.assertFalse(MixinIntoMinestomCore.success, "InstanceContainer must NOT have been mixed in with improveextensions.InstanceContainerMixin, because early loading has been disabled");
} catch (AssertionFailedError e) {
e.printStackTrace();
}
MinecraftServer.stopCleanly();
}
@Override
public void terminate() {
getLogger().info("Terminate extension");
}
}

View File

@ -0,0 +1,37 @@
package improveextensions;
import net.minestom.server.MinecraftServer;
import net.minestom.server.extensions.Extension;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.world.DimensionType;
import org.junit.jupiter.api.Assertions;
import org.opentest4j.AssertionFailedError;
import java.util.UUID;
/**
* Extensions should be able to use Mixins for classes loaded very early by Minestom (InstanceContainer for instance)
*/
public class MixinIntoMinestomCore extends Extension {
public static boolean success = false;
@Override
public void initialize() {
// force load of InstanceContainer class
InstanceContainer c = new InstanceContainer(UUID.randomUUID(), DimensionType.OVERWORLD, null);
System.out.println(c.toString());
try {
Assertions.assertTrue(success, "InstanceContainer must have been mixed in with improveextensions.InstanceContainerMixin");
Assertions.assertEquals(1, MinecraftServer.getExtensionManager().getExtensionLoaders().size(), "Only one extension classloader (this extension's) must be active.");
} catch (AssertionFailedError e) {
e.printStackTrace();
}
MinecraftServer.stopCleanly();
}
@Override
public void terminate() {
getLogger().info("Terminate extension");
}
}

View File

@ -0,0 +1,21 @@
package improveextensions;
import net.minestom.server.Bootstrap;
// To launch with VM arguments:
// To test early Mixin injections:
// -Dminestom.extension.indevfolder.classes=build/classes/java/test/ -Dminestom.extension.indevfolder.resources=build/resources/test/improveextensions
// To test disabling early Mixin injections:
// -Dminestom.extension.disable_early_load=true -Dminestom.extension.indevfolder.classes=build/classes/java/test/ -Dminestom.extension.indevfolder.resources=build/resources/test/improveextensions/disableearlyload
// To test extension termination when the server quits:
// -Dminestom.extension.indevfolder.classes=build/classes/java/test/ -Dminestom.extension.indevfolder.resources=build/resources/test/improveextensions/unloadonstop
// To test report of failure when a mixin configuration cannot be loaded, or code modifiers are missing
// -Dminestom.extension.indevfolder.classes=build/classes/java/test/ -Dminestom.extension.indevfolder.resources=build/resources/test/improveextensions/missingmodifiers
public class MixinIntoMinestomCoreLauncher {
public static void main(String[] args) {
Bootstrap.bootstrap("demo.MainDemo", args);
}
}

View File

@ -0,0 +1,43 @@
package improveextensions;
import net.minestom.server.MinecraftServer;
import net.minestom.server.extensions.Extension;
import net.minestom.server.instance.InstanceContainer;
import net.minestom.server.world.DimensionType;
import org.junit.jupiter.api.Assertions;
import org.opentest4j.AssertionFailedError;
import java.util.List;
import java.util.UUID;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Extensions should be able to use Mixins for classes loaded very early by Minestom (InstanceContainer for instance)
*/
public class MixinIntoMinestomCoreWithJava9ModuleOnClasspath extends Extension {
@Override
public void initialize() {
// use Mockito only to ensure J9 modules on the classpath are supported
List mockedList = mock(List.class);
when(mockedList.get(0)).thenReturn("Test");
// force load of InstanceContainer class
InstanceContainer c = new InstanceContainer(UUID.randomUUID(), DimensionType.OVERWORLD, null);
System.out.println(c.toString());
try {
Assertions.assertTrue(MixinIntoMinestomCore.success, "InstanceContainer must have been mixed in with improveextensions.InstanceContainerMixin");
Assertions.assertEquals(1, MinecraftServer.getExtensionManager().getExtensionLoaders().size(), "Only one extension classloader (this extension's) must be active.");
Assertions.assertEquals("Test", mockedList.get(0));
} catch (AssertionFailedError e) {
e.printStackTrace();
}
MinecraftServer.stopCleanly();
}
@Override
public void terminate() {
getLogger().info("Terminate extension");
}
}

View File

@ -0,0 +1,31 @@
package improveextensions.missingmodifiers;
import net.minestom.server.MinecraftServer;
import net.minestom.server.extensions.Extension;
import org.junit.jupiter.api.Assertions;
import org.opentest4j.AssertionFailedError;
public class MissingCodeModifiersExtension extends Extension {
@Override
public void initialize() {
// force load of InstanceContainer class
try {
Assertions.assertFalse(areCodeModifiersAllLoadedCorrectly(), "Mixin configuration could not be loaded and code modifiers are unavailable, the failure should be reported");
Assertions.assertTrue(getDescription().hasFailedToLoadMixin(), "Mixin configuration does not exist and should not be loaded");
Assertions.assertEquals(1, getDescription().getMissingCodeModifiers().size(), "Code modifier does not exist, it should be reported as missing");
Assertions.assertEquals("InvalidCodeModifierClass", getDescription().getMissingCodeModifiers().get(0));
System.out.println("All tests passed.");
} catch (AssertionFailedError e) {
e.printStackTrace();
}
MinecraftServer.stopCleanly();
}
@Override
public void terminate() {
getLogger().info("Terminate extension");
}
}

View File

@ -0,0 +1,19 @@
package improveextensions.mixins;
import improveextensions.MixinIntoMinestomCore;
import net.minestom.server.instance.InstanceContainer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(InstanceContainer.class)
public class InstanceContainerMixin {
@Inject(method = "<init>", at = @At("RETURN"), require = 1)
private void constructorHead(CallbackInfo ci) {
System.out.println("Mixin into InstanceContainerMixin");
MixinIntoMinestomCore.success = true;
}
}

View File

@ -0,0 +1,122 @@
package improveextensions.unloadcallbacks;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.type.monster.EntityZombie;
import net.minestom.server.event.EventCallback;
import net.minestom.server.event.GlobalEventHandler;
import net.minestom.server.event.entity.EntityTickEvent;
import net.minestom.server.event.instance.InstanceTickEvent;
import net.minestom.server.extensions.Extension;
import net.minestom.server.extras.selfmodification.MinestomRootClassLoader;
import net.minestom.server.instance.Instance;
import net.minestom.server.utils.Position;
import net.minestom.server.utils.time.TimeUnit;
import org.junit.jupiter.api.Assertions;
import org.opentest4j.AssertionFailedError;
import java.util.concurrent.atomic.AtomicBoolean;
public class UnloadCallbacksExtension extends Extension {
private boolean ticked1 = false;
private boolean ticked2 = false;
private boolean tickedScheduledNonTransient = false;
private boolean tickedScheduledTransient = false;
private boolean zombieTicked = false;
private boolean instanceTicked = false;
private final EventCallback<InstanceTickEvent> callback = this::onTick;
private void onTick(InstanceTickEvent e) {
ticked1 = true;
}
@Override
public void initialize() {
GlobalEventHandler globalEvents = MinecraftServer.getGlobalEventHandler();
// this callback will be automatically removed when unloading the extension
globalEvents.addEventCallback(InstanceTickEvent.class, callback);
// this one too
globalEvents.addEventCallback(InstanceTickEvent.class, e -> ticked2 = true);
Instance instance = MinecraftServer.getInstanceManager().getInstances().stream().findFirst().orElseThrow();
// add an event callback on an instance
instance.addEventCallback(InstanceTickEvent.class, e -> instanceTicked = true);
instance.loadChunk(0,0);
// add an event callback on an entity
EntityZombie zombie = new EntityZombie(new Position(8,64,8) /* middle of chunk */);
zombie.addEventCallback(EntityTickEvent.class, e -> {
zombieTicked = true;
});
zombie.setInstance(instance);
// this callback will be cancelled
MinecraftServer.getSchedulerManager().buildTask(() -> {
tickedScheduledNonTransient = true;
}).repeat(100L, TimeUnit.MILLISECOND).schedule();
// this callback will NOT be cancelled
MinecraftServer.getSchedulerManager().buildTask(() -> {
tickedScheduledTransient = true;
}).repeat(100L, TimeUnit.MILLISECOND).makeTransient().schedule();
try {
Assertions.assertNotNull(MinestomRootClassLoader.findExtensionObjectOwner(callback));
Assertions.assertEquals("UnloadCallbacksExtension", MinestomRootClassLoader.findExtensionObjectOwner(callback));
} catch (AssertionFailedError e) {
e.printStackTrace();
System.exit(-1);
}
MinecraftServer.getSchedulerManager().buildTask(() -> {
// unload self
MinecraftServer.getExtensionManager().unloadExtension(getDescription().getName());
}).delay(1L, TimeUnit.SECOND).schedule();
}
@Override
public void terminate() {
new Thread(() -> {
try {
// wait for complete termination of this extension
Thread.sleep(10);
ticked1 = false;
ticked2 = false;
tickedScheduledNonTransient = false;
tickedScheduledTransient = false;
instanceTicked = false;
zombieTicked = false;
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
AtomicBoolean executedDelayTaskAfterTerminate = new AtomicBoolean(false);
// because terminate is called just before unscheduling and removing event callbacks,
// the following task will never be executed, because it is not transient
MinecraftServer.getSchedulerManager().buildTask(() -> {
executedDelayTaskAfterTerminate.set(true);
}).delay(100L, TimeUnit.MILLISECOND).schedule();
// this shutdown tasks will not be executed because it is not transient
MinecraftServer.getSchedulerManager().buildShutdownTask(() -> Assertions.fail("This shutdown task should be unloaded when the extension is")).schedule();
MinecraftServer.getSchedulerManager().buildTask(() -> {
// Make sure callbacks are disabled
try {
Assertions.assertFalse(ticked1, "ticked1 should be false because the callback has been unloaded");
Assertions.assertFalse(ticked2, "ticked2 should be false because the callback has been unloaded");
Assertions.assertFalse(tickedScheduledNonTransient, "tickedScheduledNonTransient should be false because the callback has been unloaded");
Assertions.assertFalse(zombieTicked, "zombieTicked should be false because the callback has been unloaded");
Assertions.assertFalse(instanceTicked, "instanceTicked should be false because the callback has been unloaded");
Assertions.assertTrue(tickedScheduledTransient, "tickedScheduledNonTransient should be true because the callback has NOT been unloaded");
Assertions.assertFalse(executedDelayTaskAfterTerminate.get(), "executedDelayTaskAfterTerminate should be false because the callback has been unloaded before executing");
System.out.println("All tests passed.");
} catch (AssertionFailedError e) {
e.printStackTrace();
}
MinecraftServer.stopCleanly(); // TODO: fix deadlock which happens because stopCleanly waits on completion of scheduler tasks
}).delay(1L, TimeUnit.SECOND).makeTransient().schedule();
}
}

View File

@ -0,0 +1,37 @@
package improveextensions.unloadextensiononstop;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.type.monster.EntityZombie;
import net.minestom.server.event.EventCallback;
import net.minestom.server.event.GlobalEventHandler;
import net.minestom.server.event.entity.EntityTickEvent;
import net.minestom.server.event.instance.InstanceTickEvent;
import net.minestom.server.extensions.Extension;
import net.minestom.server.extras.selfmodification.MinestomRootClassLoader;
import net.minestom.server.instance.Instance;
import net.minestom.server.utils.Position;
import net.minestom.server.utils.time.TimeUnit;
import org.junit.jupiter.api.Assertions;
import org.opentest4j.AssertionFailedError;
import java.util.concurrent.atomic.AtomicBoolean;
public class UnloadExtensionOnStop extends Extension {
private boolean terminated = false;
@Override
public void initialize() {
MinecraftServer.getSchedulerManager().buildShutdownTask(() -> {
Assertions.assertTrue(terminated, "Extension should have been terminated on shutdown.");
System.out.println("All tests passed.");
}).makeTransient().schedule();
MinecraftServer.getSchedulerManager().buildTask(MinecraftServer::stopCleanly).makeTransient().delay(1L, TimeUnit.SECOND).schedule();
}
@Override
public void terminate() {
terminated = true;
}
}

View File

@ -0,0 +1,6 @@
{
"entrypoint": "improveextensions.DisableEarlyLoad",
"name": "DisableEarlyLoad",
"codeModifiers": [],
"mixinConfig": "mixins.minestomcore.json"
}

View File

@ -0,0 +1,10 @@
{
"required": true,
"minVersion": "0.8",
"package": "improveextensions.mixins",
"target": "@env(DEFAULT)",
"compatibilityLevel": "JAVA_11",
"mixins": [
"InstanceContainerMixin"
]
}

View File

@ -0,0 +1,6 @@
{
"entrypoint": "improveextensions.MixinIntoMinestomCore",
"name": "MixinIntoMinestomCore",
"codeModifiers": [],
"mixinConfig": "mixins.minestomcore.json"
}

View File

@ -0,0 +1,14 @@
{
"entrypoint": "improveextensions.MixinIntoMinestomCoreWithJava9ModuleOnClasspath",
"name": "MixinIntoMinestomCoreWithJava9ModuleOnClasspath",
"codeModifiers": [],
"mixinConfig": "mixins.minestomcore.json",
"externalDependencies": {
"repositories": [
{"name": "JCentral", "url": "https://jcenter.bintray.com/"}
],
"artifacts": [
"org.mockito:mockito-core:2.28.2"
]
}
}

View File

@ -0,0 +1,10 @@
{
"required": true,
"minVersion": "0.8",
"package": "improveextensions.mixins",
"target": "@env(DEFAULT)",
"compatibilityLevel": "JAVA_11",
"mixins": [
"InstanceContainerMixin"
]
}

View File

@ -0,0 +1,8 @@
{
"entrypoint": "improveextensions.missingmodifiers.MissingCodeModifiersExtension",
"name": "MissingCodeModifiersExtension",
"codeModifiers": [
"InvalidCodeModifierClass"
],
"mixinConfig": "mixins.$invalid$.json"
}

View File

@ -0,0 +1,10 @@
{
"required": true,
"minVersion": "0.8",
"package": "improveextensions.mixins",
"target": "@env(DEFAULT)",
"compatibilityLevel": "JAVA_11",
"mixins": [
"InstanceContainerMixin"
]
}

View File

@ -0,0 +1,4 @@
{
"entrypoint": "improveextensions.unloadcallbacks.UnloadCallbacksExtension",
"name": "UnloadCallbacksExtension"
}

View File

@ -0,0 +1,4 @@
{
"entrypoint": "improveextensions.unloadextensiononstop.UnloadExtensionOnStop",
"name": "UnloadExtensionOnStop"
}