Allow extensions to load other extension's classes, and store extensions by IDs instead of name (#3946)

- the extensionmanagers `extension` method now takes in a extension id instead of name
- extension folders are now created using extension id's
- Extensions can load classes from other extensions now
- Added warning about external class loading
- Wherever applicable: store extensions internally by id instead of name
This commit is contained in:
chris 2023-10-01 07:17:53 +02:00 committed by GitHub
parent 1d75d084a7
commit 34ff8c1217
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 56 additions and 29 deletions

View File

@ -36,13 +36,13 @@ import java.util.Collection;
public abstract class ExtensionManager {
/**
* Gets an extension with the given name.
* Gets an extension by the given ID.
*
* @param name the name of the extension
* @return an extension with the given name
* @param id the ID of the extension
* @return an extension with the given ID
*/
@Nullable
public abstract Extension extension(@NonNull String name);
public abstract Extension extension(@NonNull String id);
/**
* Enables the given {@link Extension}.

View File

@ -194,7 +194,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
commandMap.register(extension.description().id(), "geyserext", pluginCommand);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
this.geyserLogger.error("Failed to construct PluginCommand for extension " + extension.description().name(), ex);
this.geyserLogger.error("Failed to construct PluginCommand for extension " + extension.name(), ex);
}
}
}

View File

@ -27,6 +27,7 @@ package org.geysermc.geyser.extension;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.extension.Extension;
import org.geysermc.geyser.api.extension.ExtensionDescription;
import org.geysermc.geyser.api.extension.exception.InvalidExtensionException;
@ -39,14 +40,17 @@ import java.nio.file.Path;
public class GeyserExtensionClassLoader extends URLClassLoader {
private final GeyserExtensionLoader loader;
private final ExtensionDescription description;
private final Object2ObjectMap<String, Class<?>> classes = new Object2ObjectOpenHashMap<>();
private boolean warnedForExternalClassAccess;
public GeyserExtensionClassLoader(GeyserExtensionLoader loader, ClassLoader parent, Path path) throws MalformedURLException {
public GeyserExtensionClassLoader(GeyserExtensionLoader loader, ClassLoader parent, Path path, ExtensionDescription description) throws MalformedURLException {
super(new URL[] { path.toUri().toURL() }, parent);
this.loader = loader;
this.description = description;
}
public Extension load(ExtensionDescription description) throws InvalidExtensionException {
public Extension load() throws InvalidExtensionException {
try {
Class<?> jarClass;
try {
@ -76,22 +80,32 @@ public class GeyserExtensionClassLoader extends URLClassLoader {
}
protected Class<?> findClass(String name, boolean checkGlobal) throws ClassNotFoundException {
if (name.startsWith("org.geysermc.geyser.") || name.startsWith("net.minecraft.")) {
throw new ClassNotFoundException(name);
}
Class<?> result = this.classes.get(name);
if (result == null) {
result = super.findClass(name);
if (result == null && checkGlobal) {
result = this.loader.classByName(name);
// Try to find class in current extension
try {
result = super.findClass(name);
} catch (ClassNotFoundException ignored) {
// If class is not found in current extension, check in the global class loader
// This is used for classes that are not in the extension, but are in other extensions
if (checkGlobal) {
if (!warnedForExternalClassAccess) {
GeyserImpl.getInstance().getLogger().warning("Extension " + this.description.name() + " loads class " + name + " from an external source. " +
"This can change at any time and break the extension, additionally to potentially causing unexpected behaviour!");
warnedForExternalClassAccess = true;
}
result = this.loader.classByName(name);
}
}
if (result != null) {
// If class is found, cache it
this.loader.setClass(name, result);
this.classes.put(name, result);
} else {
// If class is not found, throw exception
throw new ClassNotFoundException(name);
}
this.classes.put(name, result);
}
return result;
}

View File

@ -66,26 +66,38 @@ public class GeyserExtensionLoader extends ExtensionLoader {
}
Path parentFile = path.getParent();
Path dataFolder = parentFile.resolve(description.name());
// Extension folders used to be created by name; this changes them to the ID
Path oldDataFolder = parentFile.resolve(description.name());
Path dataFolder = parentFile.resolve(description.id());
if (Files.exists(oldDataFolder) && Files.isDirectory(oldDataFolder) && !oldDataFolder.equals(dataFolder)) {
try {
Files.move(oldDataFolder, dataFolder, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new InvalidExtensionException("Failed to move data folder for extension " + description.name(), e);
}
}
if (Files.exists(dataFolder) && !Files.isDirectory(dataFolder)) {
throw new InvalidExtensionException("The folder " + dataFolder + " is not a directory and is the data folder for the extension " + description.name() + "!");
}
final GeyserExtensionClassLoader loader;
try {
loader = new GeyserExtensionClassLoader(this, getClass().getClassLoader(), path);
loader = new GeyserExtensionClassLoader(this, getClass().getClassLoader(), path, description);
} catch (Throwable e) {
throw new InvalidExtensionException(e);
}
this.classLoaders.put(description.name(), loader);
this.classLoaders.put(description.id(), loader);
final Extension extension = loader.load(description);
final Extension extension = loader.load();
return this.setup(extension, description, dataFolder, new GeyserExtensionEventBus(GeyserImpl.getInstance().eventBus(), extension));
}
private GeyserExtensionContainer setup(Extension extension, GeyserExtensionDescription description, Path dataFolder, ExtensionEventBus eventBus) {
GeyserExtensionLogger logger = new GeyserExtensionLogger(GeyserImpl.getInstance().getLogger(), description.name());
GeyserExtensionLogger logger = new GeyserExtensionLogger(GeyserImpl.getInstance().getLogger(), description.id());
return new GeyserExtensionContainer(extension, dataFolder, description, this, logger, eventBus);
}
@ -152,7 +164,8 @@ public class GeyserExtensionLoader extends ExtensionLoader {
GeyserExtensionDescription description = this.extensionDescription(path);
String name = description.name();
if (extensions.containsKey(name) || extensionManager.extension(name) != null) {
String id = description.id();
if (extensions.containsKey(id) || extensionManager.extension(id) != null) {
GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.extensions.load.duplicate", name, path.toString()));
return;
}
@ -169,8 +182,8 @@ public class GeyserExtensionLoader extends ExtensionLoader {
return;
}
extensions.put(name, path);
loadedExtensions.put(name, this.loadExtension(path, description));
extensions.put(id, path);
loadedExtensions.put(id, this.loadExtension(path, description));
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), e);
}

View File

@ -52,8 +52,8 @@ public class GeyserExtensionManager extends ExtensionManager {
}
@Override
public Extension extension(@NonNull String name) {
return this.extensions.get(name);
public Extension extension(@NonNull String id) {
return this.extensions.get(id);
}
@Override
@ -83,7 +83,7 @@ public class GeyserExtensionManager extends ExtensionManager {
if (!extension.isEnabled()) {
extension.setEnabled(true);
GeyserImpl.getInstance().eventBus().register(extension, extension);
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.enable.success", extension.description().name()));
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.enable.success", extension.name()));
}
}
@ -98,7 +98,7 @@ public class GeyserExtensionManager extends ExtensionManager {
GeyserImpl.getInstance().eventBus().unregisterAll(extension);
extension.setEnabled(false);
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.disable.success", extension.description().name()));
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.disable.success", extension.name()));
}
}
@ -121,6 +121,6 @@ public class GeyserExtensionManager extends ExtensionManager {
@Override
public void register(@NonNull Extension extension) {
this.extensions.put(extension.name(), extension);
this.extensions.put(extension.description().id(), extension);
}
}