mirror of
https://github.com/Minestom/Minestom.git
synced 2024-12-31 21:48:08 +01:00
Merge pull request #129 from Minestom/improve-extension-system
Improve extension system
This commit is contained in:
commit
29a8542d3e
@ -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'
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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<>());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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."),
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
34
src/test/java/improveextensions/DisableEarlyLoad.java
Normal file
34
src/test/java/improveextensions/DisableEarlyLoad.java
Normal 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");
|
||||
}
|
||||
}
|
37
src/test/java/improveextensions/MixinIntoMinestomCore.java
Normal file
37
src/test/java/improveextensions/MixinIntoMinestomCore.java
Normal 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");
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"entrypoint": "improveextensions.DisableEarlyLoad",
|
||||
"name": "DisableEarlyLoad",
|
||||
"codeModifiers": [],
|
||||
"mixinConfig": "mixins.minestomcore.json"
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"required": true,
|
||||
"minVersion": "0.8",
|
||||
"package": "improveextensions.mixins",
|
||||
"target": "@env(DEFAULT)",
|
||||
"compatibilityLevel": "JAVA_11",
|
||||
"mixins": [
|
||||
"InstanceContainerMixin"
|
||||
]
|
||||
}
|
6
src/test/resources/improveextensions/extension.json
Normal file
6
src/test/resources/improveextensions/extension.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"entrypoint": "improveextensions.MixinIntoMinestomCore",
|
||||
"name": "MixinIntoMinestomCore",
|
||||
"codeModifiers": [],
|
||||
"mixinConfig": "mixins.minestomcore.json"
|
||||
}
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"required": true,
|
||||
"minVersion": "0.8",
|
||||
"package": "improveextensions.mixins",
|
||||
"target": "@env(DEFAULT)",
|
||||
"compatibilityLevel": "JAVA_11",
|
||||
"mixins": [
|
||||
"InstanceContainerMixin"
|
||||
]
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"entrypoint": "improveextensions.missingmodifiers.MissingCodeModifiersExtension",
|
||||
"name": "MissingCodeModifiersExtension",
|
||||
"codeModifiers": [
|
||||
"InvalidCodeModifierClass"
|
||||
],
|
||||
"mixinConfig": "mixins.$invalid$.json"
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"required": true,
|
||||
"minVersion": "0.8",
|
||||
"package": "improveextensions.mixins",
|
||||
"target": "@env(DEFAULT)",
|
||||
"compatibilityLevel": "JAVA_11",
|
||||
"mixins": [
|
||||
"InstanceContainerMixin"
|
||||
]
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"entrypoint": "improveextensions.unloadcallbacks.UnloadCallbacksExtension",
|
||||
"name": "UnloadCallbacksExtension"
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
"entrypoint": "improveextensions.unloadextensiononstop.UnloadExtensionOnStop",
|
||||
"name": "UnloadExtensionOnStop"
|
||||
}
|
Loading…
Reference in New Issue
Block a user