mirror of
https://github.com/Minestom/Minestom.git
synced 2025-01-03 23:17:48 +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'
|
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.2'
|
||||||
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine: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
|
// Netty
|
||||||
api 'io.netty:netty-handler:4.1.59.Final'
|
api 'io.netty:netty-handler:4.1.59.Final'
|
||||||
api 'io.netty:netty-codec:4.1.59.Final'
|
api 'io.netty:netty-codec:4.1.59.Final'
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
package net.minestom.server;
|
package net.minestom.server;
|
||||||
|
|
||||||
|
import net.minestom.server.extensions.ExtensionManager;
|
||||||
import net.minestom.server.extras.selfmodification.MinestomRootClassLoader;
|
import net.minestom.server.extras.selfmodification.MinestomRootClassLoader;
|
||||||
import net.minestom.server.extras.selfmodification.mixins.MixinCodeModifier;
|
import net.minestom.server.extras.selfmodification.mixins.MixinCodeModifier;
|
||||||
import net.minestom.server.extras.selfmodification.mixins.MixinServiceMinestom;
|
import net.minestom.server.extras.selfmodification.mixins.MixinServiceMinestom;
|
||||||
import org.spongepowered.asm.launch.MixinBootstrap;
|
import org.spongepowered.asm.launch.MixinBootstrap;
|
||||||
import org.spongepowered.asm.launch.platform.CommandLineOptions;
|
import org.spongepowered.asm.launch.platform.CommandLineOptions;
|
||||||
import org.spongepowered.asm.mixin.Mixins;
|
import org.spongepowered.asm.mixin.Mixins;
|
||||||
|
import org.spongepowered.asm.service.ServiceNotAvailableError;
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
@ -20,7 +22,14 @@ public final class Bootstrap {
|
|||||||
try {
|
try {
|
||||||
ClassLoader classLoader = MinestomRootClassLoader.getInstance();
|
ClassLoader classLoader = MinestomRootClassLoader.getInstance();
|
||||||
startMixin(args);
|
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();
|
MixinServiceMinestom.gotoPreinitPhase();
|
||||||
// ensure extensions are loaded when starting the server
|
// ensure extensions are loaded when starting the server
|
||||||
@ -44,7 +53,16 @@ public final class Bootstrap {
|
|||||||
// hacks required to pass custom arguments
|
// hacks required to pass custom arguments
|
||||||
Method start = MixinBootstrap.class.getDeclaredMethod("start");
|
Method start = MixinBootstrap.class.getDeclaredMethod("start");
|
||||||
start.setAccessible(true);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -783,6 +783,7 @@ public final class MinecraftServer {
|
|||||||
public static void stopCleanly() {
|
public static void stopCleanly() {
|
||||||
stopping = true;
|
stopping = true;
|
||||||
LOGGER.info("Stopping Minestom server.");
|
LOGGER.info("Stopping Minestom server.");
|
||||||
|
extensionManager.unloadAllExtensions();
|
||||||
updateManager.stop();
|
updateManager.stop();
|
||||||
schedulerManager.shutdown();
|
schedulerManager.shutdown();
|
||||||
connectionManager.shutdown();
|
connectionManager.shutdown();
|
||||||
|
@ -106,6 +106,7 @@ public class Entity implements Viewable, EventHandler, DataContainer, Permission
|
|||||||
|
|
||||||
// Events
|
// Events
|
||||||
private final Map<Class<? extends Event>, Collection<EventCallback>> eventCallbacks = new ConcurrentHashMap<>();
|
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 Metadata metadata = new Metadata(this);
|
||||||
protected EntityMeta entityMeta;
|
protected EntityMeta entityMeta;
|
||||||
@ -706,6 +707,12 @@ public class Entity implements Viewable, EventHandler, DataContainer, Permission
|
|||||||
return eventCallbacks;
|
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.
|
* 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.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object containing all the global event listeners.
|
* Object containing all the global event listeners.
|
||||||
@ -14,10 +15,17 @@ public final class GlobalEventHandler implements EventHandler {
|
|||||||
|
|
||||||
// Events
|
// Events
|
||||||
private final Map<Class<? extends Event>, Collection<EventCallback>> eventCallbacks = new ConcurrentHashMap<>();
|
private final Map<Class<? extends Event>, Collection<EventCallback>> eventCallbacks = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, Collection<EventCallback<?>>> extensionCallbacks = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@Override
|
@Override
|
||||||
public Map<Class<? extends Event>, Collection<EventCallback>> getEventCallbacksMap() {
|
public Map<Class<? extends Event>, Collection<EventCallback>> getEventCallbacksMap() {
|
||||||
return eventCallbacks;
|
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.Event;
|
||||||
import net.minestom.server.event.EventCallback;
|
import net.minestom.server.event.EventCallback;
|
||||||
import net.minestom.server.event.GlobalEventHandler;
|
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.instance.Instance;
|
||||||
import net.minestom.server.utils.validate.Check;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an element which can have {@link Event} listeners assigned to it.
|
* 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.
|
* Gets a {@link Map} containing all the listeners assigned to a specific {@link Event} type.
|
||||||
@ -28,6 +30,15 @@ public interface EventHandler {
|
|||||||
@NotNull
|
@NotNull
|
||||||
Map<Class<? extends Event>, Collection<EventCallback>> getEventCallbacksMap();
|
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}.
|
* 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
|
* @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) {
|
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);
|
Collection<EventCallback> callbacks = getEventCallbacks(eventClass);
|
||||||
return callbacks.add(eventCallback);
|
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) {
|
default <E extends Event> boolean removeEventCallback(@NotNull Class<E> eventClass, @NotNull EventCallback<E> eventCallback) {
|
||||||
Collection<EventCallback> callbacks = getEventCallbacks(eventClass);
|
Collection<EventCallback> callbacks = getEventCallbacks(eventClass);
|
||||||
|
String extensionSource = MinestomRootClassLoader.findExtensionObjectOwner(eventCallback);
|
||||||
|
if(extensionSource != null) {
|
||||||
|
getExtensionCallbacks(extensionSource).remove(eventCallback);
|
||||||
|
};
|
||||||
|
|
||||||
return callbacks.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) {
|
private <E extends Event> void runEvent(@NotNull Collection<EventCallback> eventCallbacks, @NotNull E event) {
|
||||||
for (EventCallback<E> eventCallback : eventCallbacks) {
|
for (EventCallback<E> eventCallback : eventCallbacks) {
|
||||||
eventCallback.run(event);
|
eventCallback.run(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default void onExtensionUnload(String extensionName) {
|
||||||
|
removeCallbacksOwnedByExtension(extensionName);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,8 @@ final class DiscoveredExtension {
|
|||||||
private String[] codeModifiers;
|
private String[] codeModifiers;
|
||||||
private String[] dependencies;
|
private String[] dependencies;
|
||||||
private ExternalDependencies externalDependencies;
|
private ExternalDependencies externalDependencies;
|
||||||
|
private List<String> missingCodeModifiers = new LinkedList<>();
|
||||||
|
private boolean failedToLoadMixin = false;
|
||||||
transient List<URL> files = new LinkedList<>();
|
transient List<URL> files = new LinkedList<>();
|
||||||
transient LoadStatus loadStatus = LoadStatus.LOAD_SUCCESS;
|
transient LoadStatus loadStatus = LoadStatus.LOAD_SUCCESS;
|
||||||
transient private File originalJar;
|
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 {
|
enum LoadStatus {
|
||||||
LOAD_SUCCESS("Actually, it did not fail. This message should not have been printed."),
|
LOAD_SUCCESS("Actually, it did not fail. This message should not have been printed."),
|
||||||
MISSING_DEPENDENCIES("Missing dependencies, check your logs."),
|
MISSING_DEPENDENCIES("Missing dependencies, check your logs."),
|
||||||
|
@ -3,8 +3,12 @@ package net.minestom.server.extensions;
|
|||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.lang.ref.Reference;
|
||||||
import java.util.List;
|
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 {
|
public abstract class Extension {
|
||||||
// Set by reflection
|
// Set by reflection
|
||||||
@ -14,6 +18,15 @@ public abstract class Extension {
|
|||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private Logger logger;
|
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() {
|
protected Extension() {
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -55,12 +68,54 @@ public abstract class Extension {
|
|||||||
return logger;
|
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 {
|
public static class ExtensionDescription {
|
||||||
private final String name;
|
private final String name;
|
||||||
private final String version;
|
private final String version;
|
||||||
private final List<String> authors;
|
private final List<String> authors;
|
||||||
private final List<String> dependents = new ArrayList<>();
|
private final List<String> dependents = new ArrayList<>();
|
||||||
|
private final List<String> missingCodeModifiers = new LinkedList<>();
|
||||||
|
private final boolean failedToLoadMixin;
|
||||||
private final DiscoveredExtension origin;
|
private final DiscoveredExtension origin;
|
||||||
|
|
||||||
ExtensionDescription(@NotNull String name, @NotNull String version, @NotNull List<String> authors, @NotNull 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.version = version;
|
||||||
this.authors = authors;
|
this.authors = authors;
|
||||||
this.origin = origin;
|
this.origin = origin;
|
||||||
|
failedToLoadMixin = origin.hasFailedToLoadMixin();
|
||||||
|
missingCodeModifiers.addAll(origin.getMissingCodeModifiers());
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
@ -94,5 +151,15 @@ public abstract class Extension {
|
|||||||
DiscoveredExtension getOrigin() {
|
DiscoveredExtension getOrigin() {
|
||||||
return origin;
|
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 com.google.gson.Gson;
|
||||||
import net.minestom.dependencies.DependencyGetter;
|
import net.minestom.dependencies.DependencyGetter;
|
||||||
|
import net.minestom.dependencies.ResolvedDependency;
|
||||||
import net.minestom.dependencies.maven.MavenRepository;
|
import net.minestom.dependencies.maven.MavenRepository;
|
||||||
import net.minestom.server.MinecraftServer;
|
import net.minestom.server.MinecraftServer;
|
||||||
import net.minestom.server.extras.selfmodification.MinestomExtensionClassLoader;
|
import net.minestom.server.extras.selfmodification.MinestomExtensionClassLoader;
|
||||||
import net.minestom.server.extras.selfmodification.MinestomRootClassLoader;
|
import net.minestom.server.extras.selfmodification.MinestomRootClassLoader;
|
||||||
import net.minestom.server.ping.ResponseDataConsumer;
|
import net.minestom.server.ping.ResponseDataConsumer;
|
||||||
|
import net.minestom.server.utils.time.TimeUnit;
|
||||||
import net.minestom.server.utils.validate.Check;
|
import net.minestom.server.utils.validate.Check;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.spongepowered.asm.mixin.Mixins;
|
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.io.*;
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
@ -27,10 +32,12 @@ import java.util.zip.ZipFile;
|
|||||||
|
|
||||||
public class ExtensionManager {
|
public class ExtensionManager {
|
||||||
|
|
||||||
|
public final static String DISABLE_EARLY_LOAD_SYSTEM_KEY = "minestom.extension.disable_early_load";
|
||||||
|
|
||||||
public final static Logger LOGGER = LoggerFactory.getLogger(ExtensionManager.class);
|
public final static Logger LOGGER = LoggerFactory.getLogger(ExtensionManager.class);
|
||||||
|
|
||||||
private final static String INDEV_CLASSES_FOLDER = "minestom.extension.indevfolder.classes";
|
public 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_RESOURCES_FOLDER = "minestom.extension.indevfolder.resources";
|
||||||
private final static Gson GSON = new Gson();
|
private final static Gson GSON = new Gson();
|
||||||
|
|
||||||
private final Map<String, MinestomExtensionClassLoader> extensionLoaders = new HashMap<>();
|
private final Map<String, MinestomExtensionClassLoader> extensionLoaders = new HashMap<>();
|
||||||
@ -118,6 +125,13 @@ public class ExtensionManager {
|
|||||||
MinecraftServer.getExceptionManager().handleException(e);
|
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) {
|
private void setupClassLoader(@NotNull DiscoveredExtension discoveredExtension) {
|
||||||
@ -235,16 +249,19 @@ public class ExtensionManager {
|
|||||||
@NotNull
|
@NotNull
|
||||||
private List<DiscoveredExtension> discoverExtensions() {
|
private List<DiscoveredExtension> discoverExtensions() {
|
||||||
List<DiscoveredExtension> extensions = new LinkedList<>();
|
List<DiscoveredExtension> extensions = new LinkedList<>();
|
||||||
for (File file : extensionFolder.listFiles()) {
|
File[] fileList = extensionFolder.listFiles();
|
||||||
if (file.isDirectory()) {
|
if(fileList != null) {
|
||||||
continue;
|
for (File file : fileList) {
|
||||||
}
|
if (file.isDirectory()) {
|
||||||
if (!file.getName().endsWith(".jar")) {
|
continue;
|
||||||
continue;
|
}
|
||||||
}
|
if (!file.getName().endsWith(".jar")) {
|
||||||
DiscoveredExtension extension = discoverFromJar(file);
|
continue;
|
||||||
if (extension != null && extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) {
|
}
|
||||||
extensions.add(extension);
|
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) {
|
for (var artifact : externalDependencies.artifacts) {
|
||||||
var resolved = getter.get(artifact, dependenciesFolder);
|
var resolved = getter.get(artifact, dependenciesFolder);
|
||||||
addDependencyFile(resolved.getContentsLocation(), ext);
|
addDependencyFile(resolved, ext);
|
||||||
LOGGER.trace("Dependency of extension {}: {}", ext.getName(), resolved);
|
LOGGER.trace("Dependency of extension {}: {}", ext.getName(), resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var dependencyName : ext.getDependencies()) {
|
for (var dependencyName : ext.getDependencies()) {
|
||||||
var resolved = getter.get(dependencyName, dependenciesFolder);
|
var resolved = getter.get(dependencyName, dependenciesFolder);
|
||||||
addDependencyFile(resolved.getContentsLocation(), ext);
|
addDependencyFile(resolved, ext);
|
||||||
LOGGER.trace("Dependency of extension {}: {}", ext.getName(), resolved);
|
LOGGER.trace("Dependency of extension {}: {}", ext.getName(), resolved);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -415,9 +432,19 @@ public class ExtensionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addDependencyFile(URL dependency, DiscoveredExtension extension) {
|
private void addDependencyFile(ResolvedDependency dependency, DiscoveredExtension extension) {
|
||||||
extension.files.add(dependency);
|
URL location = dependency.getContentsLocation();
|
||||||
LOGGER.trace("Added dependency {} to extension {} classpath", dependency.toExternalForm(), extension.getName());
|
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
|
@NotNull
|
||||||
public MinestomExtensionClassLoader newClassLoader(@NotNull DiscoveredExtension extension, @NotNull URL[] urls) {
|
public MinestomExtensionClassLoader newClassLoader(@NotNull DiscoveredExtension extension, @NotNull URL[] urls) {
|
||||||
MinestomRootClassLoader root = MinestomRootClassLoader.getInstance();
|
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) {
|
if (extension.getDependencies().length == 0) {
|
||||||
// orphaned extension, we can insert it directly
|
// orphaned extension, we can insert it directly
|
||||||
root.addChild(loader);
|
root.addChild(loader);
|
||||||
@ -469,7 +496,7 @@ public class ExtensionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public Map<String, URLClassLoader> getExtensionLoaders() {
|
public Map<String, MinestomExtensionClassLoader> getExtensionLoaders() {
|
||||||
return new HashMap<>(extensionLoaders);
|
return new HashMap<>(extensionLoaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -485,19 +512,40 @@ public class ExtensionManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
MinestomRootClassLoader modifiableClassLoader = (MinestomRootClassLoader) cl;
|
MinestomRootClassLoader modifiableClassLoader = (MinestomRootClassLoader) cl;
|
||||||
|
setupCodeModifiers(extensions, modifiableClassLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupCodeModifiers(@NotNull List<DiscoveredExtension> extensions, MinestomRootClassLoader modifiableClassLoader) {
|
||||||
LOGGER.info("Start loading code modifiers...");
|
LOGGER.info("Start loading code modifiers...");
|
||||||
for (DiscoveredExtension extension : extensions) {
|
for (DiscoveredExtension extension : extensions) {
|
||||||
try {
|
try {
|
||||||
for (String codeModifierClass : extension.getCodeModifiers()) {
|
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()) {
|
if (!extension.getMixinConfig().isEmpty()) {
|
||||||
final String mixinConfigFile = extension.getMixinConfig();
|
final String mixinConfigFile = extension.getMixinConfig();
|
||||||
Mixins.addConfiguration(mixinConfigFile);
|
try {
|
||||||
LOGGER.info("Found mixin in extension {}: {}", extension.getName(), mixinConfigFile);
|
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) {
|
} 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: " +
|
LOGGER.error("Failed to load code modifier for extension in files: " +
|
||||||
extension.files
|
extension.files
|
||||||
.stream()
|
.stream()
|
||||||
@ -511,6 +559,11 @@ public class ExtensionManager {
|
|||||||
private void unload(Extension ext) {
|
private void unload(Extension ext) {
|
||||||
ext.preTerminate();
|
ext.preTerminate();
|
||||||
ext.terminate();
|
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.postTerminate();
|
||||||
ext.unload();
|
ext.unload();
|
||||||
|
|
||||||
@ -664,4 +717,46 @@ public class ExtensionManager {
|
|||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
this.extensionList.forEach(this::unload);
|
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;
|
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.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
|
@Override
|
||||||
@ -72,4 +97,16 @@ public class MinestomExtensionClassLoader extends HierarchyClassLoader {
|
|||||||
super.finalize();
|
super.finalize();
|
||||||
System.err.println("Class loader "+getName()+" finalized.");
|
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;
|
package net.minestom.server.extras.selfmodification;
|
||||||
|
|
||||||
import net.minestom.server.MinecraftServer;
|
import net.minestom.server.MinecraftServer;
|
||||||
|
import net.minestom.server.extensions.ExtensionManager;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.objectweb.asm.ClassReader;
|
import org.objectweb.asm.ClassReader;
|
||||||
import org.objectweb.asm.ClassWriter;
|
import org.objectweb.asm.ClassWriter;
|
||||||
import org.objectweb.asm.tree.ClassNode;
|
import org.objectweb.asm.tree.ClassNode;
|
||||||
@ -65,9 +67,21 @@ public class MinestomRootClassLoader extends HierarchyClassLoader {
|
|||||||
// TODO: priorities?
|
// TODO: priorities?
|
||||||
private final List<CodeModifier> modifiers = new LinkedList<>();
|
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) {
|
private MinestomRootClassLoader(ClassLoader parent) {
|
||||||
super("Minestom Root ClassLoader", extractURLsFromClasspath(), parent);
|
super("Minestom Root ClassLoader", extractURLsFromClasspath(), parent);
|
||||||
asmClassLoader = newChild(new URL[0]);
|
asmClassLoader = newChild(new URL[0]);
|
||||||
|
inDevEnvironment = System.getProperty(ExtensionManager.INDEV_CLASSES_FOLDER) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MinestomRootClassLoader getInstance() {
|
public static MinestomRootClassLoader getInstance() {
|
||||||
@ -179,6 +193,22 @@ public class MinestomRootClassLoader extends HierarchyClassLoader {
|
|||||||
if (name == null)
|
if (name == null)
|
||||||
throw new ClassNotFoundException();
|
throw new ClassNotFoundException();
|
||||||
String path = name.replace(".", "/") + ".class";
|
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);
|
InputStream input = getResourceAsStream(path);
|
||||||
if (input == null) {
|
if (input == null) {
|
||||||
throw new ClassNotFoundException("Could not find resource " + path);
|
throw new ClassNotFoundException("Could not find resource " + path);
|
||||||
@ -245,12 +275,17 @@ public class MinestomRootClassLoader extends HierarchyClassLoader {
|
|||||||
return URLClassLoader.newInstance(urls, this);
|
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 {
|
try {
|
||||||
for (int i = 0; i < originFiles.length; i++) {
|
|
||||||
urls[i] = originFiles[i].toURI().toURL();
|
|
||||||
}
|
|
||||||
URLClassLoader loader = newChild(urls);
|
URLClassLoader loader = newChild(urls);
|
||||||
Class<?> modifierClass = loader.loadClass(codeModifierClass);
|
Class<?> modifierClass = loader.loadClass(codeModifierClass);
|
||||||
if (CodeModifier.class.isAssignableFrom(modifierClass)) {
|
if (CodeModifier.class.isAssignableFrom(modifierClass)) {
|
||||||
@ -258,11 +293,18 @@ public class MinestomRootClassLoader extends HierarchyClassLoader {
|
|||||||
synchronized (modifiers) {
|
synchronized (modifiers) {
|
||||||
LOGGER.warn("Added Code modifier: {}", modifier);
|
LOGGER.warn("Added Code modifier: {}", modifier);
|
||||||
addCodeModifier(modifier);
|
addCodeModifier(modifier);
|
||||||
|
alreadyLoadedCodeModifiers.add(codeModifierClass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (MalformedURLException | ClassNotFoundException | InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) {
|
return true;
|
||||||
MinecraftServer.getExceptionManager().handleException(e);
|
} 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) {
|
public void addCodeModifier(CodeModifier modifier) {
|
||||||
@ -279,4 +321,24 @@ public class MinestomRootClassLoader extends HierarchyClassLoader {
|
|||||||
public List<CodeModifier> getModifiers() {
|
public List<CodeModifier> getModifiers() {
|
||||||
return modifiers;
|
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.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@ -79,6 +80,7 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
|
|||||||
private long lastTickAge = System.currentTimeMillis();
|
private long lastTickAge = System.currentTimeMillis();
|
||||||
|
|
||||||
private final Map<Class<? extends Event>, Collection<EventCallback>> eventCallbacks = new ConcurrentHashMap<>();
|
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
|
// Entities present in this instance
|
||||||
protected final Set<Entity> entities = new CopyOnWriteArraySet<>();
|
protected final Set<Entity> entities = new CopyOnWriteArraySet<>();
|
||||||
@ -864,6 +866,12 @@ public abstract class Instance implements BlockModifier, EventHandler, DataConta
|
|||||||
return eventCallbacks;
|
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)
|
// 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.ints.Int2ObjectOpenHashMap;
|
||||||
import it.unimi.dsi.fastutil.objects.ObjectCollection;
|
import it.unimi.dsi.fastutil.objects.ObjectCollection;
|
||||||
import net.minestom.server.MinecraftServer;
|
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 net.minestom.server.utils.thread.MinestomThread;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.List;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.*;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object which manages all the {@link Task}'s.
|
* An object which manages all the {@link Task}'s.
|
||||||
@ -24,7 +26,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
* <p>
|
* <p>
|
||||||
* Shutdown tasks are built with {@link #buildShutdownTask(Runnable)} and are executed, as the name implies, when the server stops.
|
* 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;
|
private static boolean instanced;
|
||||||
// A counter for all normal tasks
|
// A counter for all normal tasks
|
||||||
@ -40,6 +42,16 @@ public final class SchedulerManager {
|
|||||||
// All the registered shutdown tasks (task id = task)
|
// All the registered shutdown tasks (task id = task)
|
||||||
protected final Int2ObjectMap<Task> shutdownTasks;
|
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
|
* Default constructor
|
||||||
*/
|
*/
|
||||||
@ -169,4 +181,62 @@ public final class SchedulerManager {
|
|||||||
public ScheduledExecutorService getTimerExecutionService() {
|
public ScheduledExecutorService getTimerExecutionService() {
|
||||||
return timerExecutionService;
|
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 it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||||
import net.minestom.server.MinecraftServer;
|
import net.minestom.server.MinecraftServer;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
@ -27,6 +28,16 @@ public class Task implements Runnable {
|
|||||||
private final long delay;
|
private final long delay;
|
||||||
// Repeat value for the task execution
|
// Repeat value for the task execution
|
||||||
private final long repeat;
|
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
|
// Task completion/execution
|
||||||
private ScheduledFuture<?> future;
|
private ScheduledFuture<?> future;
|
||||||
// The thread of the task
|
// The thread of the task
|
||||||
@ -41,13 +52,15 @@ public class Task implements Runnable {
|
|||||||
* @param delay The time to delay
|
* @param delay The time to delay
|
||||||
* @param repeat The time until the repetition
|
* @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.schedulerManager = schedulerManager;
|
||||||
this.runnable = runnable;
|
this.runnable = runnable;
|
||||||
this.shutdown = shutdown;
|
this.shutdown = shutdown;
|
||||||
this.id = shutdown ? this.schedulerManager.getShutdownCounterIdentifier() : this.schedulerManager.getCounterIdentifier();
|
this.id = shutdown ? this.schedulerManager.getShutdownCounterIdentifier() : this.schedulerManager.getCounterIdentifier();
|
||||||
this.delay = delay;
|
this.delay = delay;
|
||||||
this.repeat = repeat;
|
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.
|
* Sets up the task for correct execution.
|
||||||
*/
|
*/
|
||||||
public void schedule() {
|
public void schedule() {
|
||||||
|
if(owningExtension != null) {
|
||||||
|
this.schedulerManager.onScheduleFromExtension(owningExtension, this);
|
||||||
|
}
|
||||||
this.future = this.repeat == 0L ?
|
this.future = this.repeat == 0L ?
|
||||||
this.schedulerManager.getTimerExecutionService().schedule(this, this.delay, TimeUnit.MILLISECONDS) :
|
this.schedulerManager.getTimerExecutionService().schedule(this, this.delay, TimeUnit.MILLISECONDS) :
|
||||||
this.schedulerManager.getTimerExecutionService().scheduleAtFixedRate(this, this.delay, this.repeat, 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;
|
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
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(id);
|
return Objects.hash(id);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.minestom.server.timer;
|
package net.minestom.server.timer;
|
||||||
|
|
||||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||||
|
import net.minestom.server.extras.selfmodification.MinestomRootClassLoader;
|
||||||
import net.minestom.server.utils.time.TimeUnit;
|
import net.minestom.server.utils.time.TimeUnit;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
@ -18,10 +19,18 @@ public class TaskBuilder {
|
|||||||
private final Runnable runnable;
|
private final Runnable runnable;
|
||||||
// True if the task planned for the application shutdown
|
// True if the task planned for the application shutdown
|
||||||
private final boolean shutdown;
|
private final boolean shutdown;
|
||||||
|
/** Extension which owns this task, or null if none */
|
||||||
|
private final String owningExtension;
|
||||||
// Delay value for the task execution
|
// Delay value for the task execution
|
||||||
private long delay;
|
private long delay;
|
||||||
// Repeat value for the task execution
|
// Repeat value for the task execution
|
||||||
private long repeat;
|
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.
|
* Creates a task builder.
|
||||||
@ -46,6 +55,8 @@ public class TaskBuilder {
|
|||||||
this.schedulerManager = schedulerManager;
|
this.schedulerManager = schedulerManager;
|
||||||
this.runnable = runnable;
|
this.runnable = runnable;
|
||||||
this.shutdown = shutdown;
|
this.shutdown = shutdown;
|
||||||
|
this.isTransient = false;
|
||||||
|
this.owningExtension = MinestomRootClassLoader.findExtensionObjectOwner(runnable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -96,6 +107,14 @@ public class TaskBuilder {
|
|||||||
return this;
|
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.
|
* Schedules this {@link Task} for execution.
|
||||||
*
|
*
|
||||||
@ -108,12 +127,17 @@ public class TaskBuilder {
|
|||||||
this.runnable,
|
this.runnable,
|
||||||
this.shutdown,
|
this.shutdown,
|
||||||
this.delay,
|
this.delay,
|
||||||
this.repeat);
|
this.repeat,
|
||||||
|
this.isTransient,
|
||||||
|
this.owningExtension);
|
||||||
if (this.shutdown) {
|
if (this.shutdown) {
|
||||||
Int2ObjectMap<Task> shutdownTasks = this.schedulerManager.shutdownTasks;
|
Int2ObjectMap<Task> shutdownTasks = this.schedulerManager.shutdownTasks;
|
||||||
synchronized (shutdownTasks) {
|
synchronized (shutdownTasks) {
|
||||||
shutdownTasks.put(task.getId(), task);
|
shutdownTasks.put(task.getId(), task);
|
||||||
}
|
}
|
||||||
|
if(owningExtension != null) {
|
||||||
|
this.schedulerManager.onScheduleShutdownFromExtension(owningExtension, task);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Int2ObjectMap<Task> tasks = this.schedulerManager.tasks;
|
Int2ObjectMap<Task> tasks = this.schedulerManager.tasks;
|
||||||
synchronized (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