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 { 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 * @param id the ID of the extension
* @return an extension with the given name * @return an extension with the given ID
*/ */
@Nullable @Nullable
public abstract Extension extension(@NonNull String name); public abstract Extension extension(@NonNull String id);
/** /**
* Enables the given {@link Extension}. * 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); commandMap.register(extension.description().id(), "geyserext", pluginCommand);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) { } 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.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; 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.Extension;
import org.geysermc.geyser.api.extension.ExtensionDescription; import org.geysermc.geyser.api.extension.ExtensionDescription;
import org.geysermc.geyser.api.extension.exception.InvalidExtensionException; import org.geysermc.geyser.api.extension.exception.InvalidExtensionException;
@ -39,14 +40,17 @@ import java.nio.file.Path;
public class GeyserExtensionClassLoader extends URLClassLoader { public class GeyserExtensionClassLoader extends URLClassLoader {
private final GeyserExtensionLoader loader; private final GeyserExtensionLoader loader;
private final ExtensionDescription description;
private final Object2ObjectMap<String, Class<?>> classes = new Object2ObjectOpenHashMap<>(); 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); super(new URL[] { path.toUri().toURL() }, parent);
this.loader = loader; this.loader = loader;
this.description = description;
} }
public Extension load(ExtensionDescription description) throws InvalidExtensionException { public Extension load() throws InvalidExtensionException {
try { try {
Class<?> jarClass; Class<?> jarClass;
try { try {
@ -76,22 +80,32 @@ public class GeyserExtensionClassLoader extends URLClassLoader {
} }
protected Class<?> findClass(String name, boolean checkGlobal) throws ClassNotFoundException { 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); Class<?> result = this.classes.get(name);
if (result == null) { if (result == null) {
result = super.findClass(name); // Try to find class in current extension
if (result == null && checkGlobal) { try {
result = this.loader.classByName(name); 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 (result != null) {
// If class is found, cache it
this.loader.setClass(name, result); 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; return result;
} }

View File

@ -66,26 +66,38 @@ public class GeyserExtensionLoader extends ExtensionLoader {
} }
Path parentFile = path.getParent(); 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)) { 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() + "!"); throw new InvalidExtensionException("The folder " + dataFolder + " is not a directory and is the data folder for the extension " + description.name() + "!");
} }
final GeyserExtensionClassLoader loader; final GeyserExtensionClassLoader loader;
try { try {
loader = new GeyserExtensionClassLoader(this, getClass().getClassLoader(), path); loader = new GeyserExtensionClassLoader(this, getClass().getClassLoader(), path, description);
} catch (Throwable e) { } catch (Throwable e) {
throw new InvalidExtensionException(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)); return this.setup(extension, description, dataFolder, new GeyserExtensionEventBus(GeyserImpl.getInstance().eventBus(), extension));
} }
private GeyserExtensionContainer setup(Extension extension, GeyserExtensionDescription description, Path dataFolder, ExtensionEventBus eventBus) { 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); return new GeyserExtensionContainer(extension, dataFolder, description, this, logger, eventBus);
} }
@ -152,7 +164,8 @@ public class GeyserExtensionLoader extends ExtensionLoader {
GeyserExtensionDescription description = this.extensionDescription(path); GeyserExtensionDescription description = this.extensionDescription(path);
String name = description.name(); 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())); GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.extensions.load.duplicate", name, path.toString()));
return; return;
} }
@ -169,8 +182,8 @@ public class GeyserExtensionLoader extends ExtensionLoader {
return; return;
} }
extensions.put(name, path); extensions.put(id, path);
loadedExtensions.put(name, this.loadExtension(path, description)); loadedExtensions.put(id, this.loadExtension(path, description));
} catch (Exception e) { } catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), 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 @Override
public Extension extension(@NonNull String name) { public Extension extension(@NonNull String id) {
return this.extensions.get(name); return this.extensions.get(id);
} }
@Override @Override
@ -83,7 +83,7 @@ public class GeyserExtensionManager extends ExtensionManager {
if (!extension.isEnabled()) { if (!extension.isEnabled()) {
extension.setEnabled(true); extension.setEnabled(true);
GeyserImpl.getInstance().eventBus().register(extension, extension); 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); GeyserImpl.getInstance().eventBus().unregisterAll(extension);
extension.setEnabled(false); 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 @Override
public void register(@NonNull Extension extension) { public void register(@NonNull Extension extension) {
this.extensions.put(extension.name(), extension); this.extensions.put(extension.description().id(), extension);
} }
} }