From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Owen1212055 <23108066+Owen1212055@users.noreply.github.com> Date: Wed, 6 Jul 2022 23:00:31 -0400 Subject: [PATCH] Paper Plugins diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java index b3a58bf4b654e336826dc04da9e2f80ff8b9a9a7..156334e3876d966fedc91d18da29f562395ab182 100644 --- a/src/main/java/io/papermc/paper/command/PaperCommand.java +++ b/src/main/java/io/papermc/paper/command/PaperCommand.java @@ -4,6 +4,7 @@ import io.papermc.paper.command.subcommands.EntityCommand; import io.papermc.paper.command.subcommands.HeapDumpCommand; import io.papermc.paper.command.subcommands.ReloadCommand; import io.papermc.paper.command.subcommands.VersionCommand; +import io.papermc.paper.command.subcommands.DumpPluginsCommand; import it.unimi.dsi.fastutil.Pair; import java.util.ArrayList; import java.util.Arrays; @@ -40,6 +41,7 @@ public final class PaperCommand extends Command { commands.put(Set.of("entity"), new EntityCommand()); commands.put(Set.of("reload"), new ReloadCommand()); commands.put(Set.of("version"), new VersionCommand()); + commands.put(Set.of("dumpplugins"), new DumpPluginsCommand()); return commands.entrySet().stream() .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) diff --git a/src/main/java/io/papermc/paper/command/PaperCommands.java b/src/main/java/io/papermc/paper/command/PaperCommands.java index 6a00f3d38da8107825ab1d405f337fd077b09f72..d44d0074446c1c54e87dc8078dff7fef1d92f343 100644 --- a/src/main/java/io/papermc/paper/command/PaperCommands.java +++ b/src/main/java/io/papermc/paper/command/PaperCommands.java @@ -23,5 +23,6 @@ public final class PaperCommands { COMMANDS.forEach((s, command) -> { server.server.getCommandMap().register(s, "Paper", command); }); + server.server.getCommandMap().register("bukkit", new PaperPluginsCommand()); } } diff --git a/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..72096a66a4046633de73a12f5a043ac6dff169b1 --- /dev/null +++ b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java @@ -0,0 +1,207 @@ +package io.papermc.paper.command; + +import com.google.common.collect.Lists; +import io.leangen.geantyref.GenericTypeReflector; +import io.leangen.geantyref.TypeToken; +import io.papermc.paper.plugin.configuration.PluginMeta; +import io.papermc.paper.plugin.entrypoint.Entrypoint; +import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; +import io.papermc.paper.plugin.provider.PluginProvider; +import io.papermc.paper.plugin.provider.ProviderStatus; +import io.papermc.paper.plugin.provider.ProviderStatusHolder; +import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent; +import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.command.defaults.BukkitCommand; +import org.bukkit.craftbukkit.util.CraftMagicNumbers; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.TreeMap; + +public class PaperPluginsCommand extends BukkitCommand { + + private static final TextColor INFO_COLOR = TextColor.color(52, 159, 218); + + // TODO: LINK? + private static final Component SERVER_PLUGIN_INFO = Component.text("ℹ What is a server plugin?", INFO_COLOR) + .append(asPlainComponents(""" + Server plugins can add new behavior to your server! + You can find new plugins on Paper's plugin repository, Hangar. + + + """)); + + private static final Component SERVER_INITIALIZER_INFO = Component.text("ℹ What is a server initializer?", INFO_COLOR) + .append(asPlainComponents(""" + Server initializers are ran before your server + starts and are provided by paper plugins. + """)); + + private static final Component LEGACY_PLUGIN_INFO = Component.text("ℹ What is a legacy plugin?", INFO_COLOR) + .append(asPlainComponents(""" + A legacy plugin is a plugin that was made on + very old unsupported versions of the game. + + It is encouraged that you replace this plugin, + as they might not work in the future and may cause + performance issues. + """)); + + private static final Component LEGACY_PLUGIN_STAR = Component.text('*', TextColor.color(255, 212, 42)).hoverEvent(LEGACY_PLUGIN_INFO); + private static final Component INFO_ICON_START = Component.text("ℹ ", INFO_COLOR); + private static final Component PAPER_HEADER = Component.text("Paper Plugins:", TextColor.color(2, 136, 209)); + private static final Component BUKKIT_HEADER = Component.text("Bukkit Plugins:", TextColor.color(237, 129, 6)); + private static final Component PLUGIN_TICK = Component.text("- ", NamedTextColor.DARK_GRAY); + private static final Component PLUGIN_TICK_EMPTY = Component.text(" "); + + private static final Type JAVA_PLUGIN_PROVIDER_TYPE = new TypeToken>() {}.getType(); + + public PaperPluginsCommand() { + super("plugins"); + this.description = "Gets a list of plugins running on the server"; + this.usageMessage = "/plugins"; + this.setPermission("bukkit.command.plugins"); + this.setAliases(Arrays.asList("pl")); + } + + private static List formatProviders(TreeMap> plugins) { + List components = new ArrayList<>(plugins.size()); + for (PluginProvider entry : plugins.values()) { + components.add(formatProvider(entry)); + } + + boolean isFirst = true; + List formattedSublists = new ArrayList<>(); + /* + Split up the plugin list for each 10 plugins to get size down + + Plugin List: + - Plugin 1, Plugin 2, .... Plugin 10, + Plugin 11, Plugin 12 ... Plugin 20, + */ + for (List componentSublist : Lists.partition(components, 10)) { + Component component = Component.space(); + if (isFirst) { + component = component.append(PLUGIN_TICK); + isFirst = false; + } else { + component = PLUGIN_TICK_EMPTY; + //formattedSublists.add(Component.empty()); // Add an empty line, the auto chat wrapping and this makes it quite jarring. + } + + formattedSublists.add(component.append(Component.join(JoinConfiguration.commas(true), componentSublist))); + } + + return formattedSublists; + } + + private static Component formatProvider(PluginProvider provider) { + TextComponent.Builder builder = Component.text(); + if (provider instanceof SpigotPluginProvider spigotPluginProvider && CraftMagicNumbers.isLegacy(spigotPluginProvider.getMeta())) { + builder.append(LEGACY_PLUGIN_STAR); + } + + String name = provider.getMeta().getName(); + Component pluginName = Component.text(name, fromStatus(provider)) + .clickEvent(ClickEvent.runCommand("/version " + name)); + + builder.append(pluginName); + + return builder.build(); + } + + private static Component asPlainComponents(String strings) { + net.kyori.adventure.text.TextComponent.Builder builder = Component.text(); + for (String string : strings.split("\n")) { + builder.append(Component.newline()); + builder.append(Component.text(string, NamedTextColor.WHITE)); + } + + return builder.build(); + } + + private static TextColor fromStatus(PluginProvider provider) { + if (provider instanceof ProviderStatusHolder statusHolder && statusHolder.getLastProvidedStatus() != null) { + ProviderStatus status = statusHolder.getLastProvidedStatus(); + + // Handle enabled/disabled game plugins + if (status == ProviderStatus.INITIALIZED && GenericTypeReflector.isSuperType(JAVA_PLUGIN_PROVIDER_TYPE, provider.getClass())) { + Plugin plugin = Bukkit.getPluginManager().getPlugin(provider.getMeta().getName()); + // Plugin doesn't exist? Could be due to it being removed. + if (plugin == null) { + return NamedTextColor.RED; + } + + return plugin.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED; + } + + return switch (status) { + case INITIALIZED -> NamedTextColor.GREEN; + case ERRORED -> NamedTextColor.RED; + }; + } else if (provider instanceof PaperPluginParent.PaperServerPluginProvider serverPluginProvider && serverPluginProvider.shouldSkipCreation()) { + // Paper plugins will be skipped if their provider is skipped due to their initializer failing. + // Show them as red + return NamedTextColor.RED; + } else { + // Separated for future logic choice, but this indicated a provider that failed to load due to + // dependency issues or what not. + return NamedTextColor.RED; + } + } + + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String currentAlias, @NotNull String[] args) { + if (!this.testPermission(sender)) return true; + + TreeMap> paperPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + TreeMap> spigotPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + + for (PluginProvider provider : LaunchEntryPointHandler.INSTANCE.get(Entrypoint.PLUGIN).getRegisteredProviders()) { + PluginMeta configuration = provider.getMeta(); + + if (provider instanceof SpigotPluginProvider) { + spigotPlugins.put(configuration.getDisplayName(), provider); + } else if (provider instanceof PaperPluginParent.PaperServerPluginProvider) { + paperPlugins.put(configuration.getDisplayName(), provider); + } + } + + Component infoMessage = Component.text("Server Plugins (%s):".formatted(paperPlugins.size() + spigotPlugins.size()), NamedTextColor.WHITE); + //.append(INFO_ICON_START.hoverEvent(SERVER_PLUGIN_INFO)); TODO: Add docs + + sender.sendMessage(infoMessage); + sender.sendMessage(PAPER_HEADER); + for (Component component : formatProviders(paperPlugins)) { + sender.sendMessage(component); + } + sender.sendMessage(BUKKIT_HEADER); + for (Component component : formatProviders(spigotPlugins)) { + sender.sendMessage(component); + } + + return true; + } + + @NotNull + @Override + public List tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException { + return Collections.emptyList(); + } + +} diff --git a/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java b/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..254854646b748e5bb47657625315ced51b22887f --- /dev/null +++ b/src/main/java/io/papermc/paper/command/subcommands/DumpPluginsCommand.java @@ -0,0 +1,201 @@ +package io.papermc.paper.command.subcommands; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.internal.Streams; +import com.google.gson.stream.JsonWriter; +import io.papermc.paper.command.PaperSubcommand; +import io.papermc.paper.plugin.entrypoint.Entrypoint; +import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; +import io.papermc.paper.plugin.entrypoint.classloader.group.LockingClassLoaderGroup; +import io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage; +import io.papermc.paper.plugin.entrypoint.classloader.group.SimpleListPluginClassLoaderGroup; +import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; +import io.papermc.paper.plugin.entrypoint.strategy.ModernPluginLoadingStrategy; +import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration; +import io.papermc.paper.plugin.manager.PaperPluginManagerImpl; +import io.papermc.paper.plugin.provider.PluginProvider; +import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; +import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage; +import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup; +import io.papermc.paper.plugin.storage.ConfiguredProviderStorage; +import io.papermc.paper.plugin.storage.ProviderStorage; +import net.minecraft.server.MinecraftServer; +import org.bukkit.command.CommandSender; +import org.bukkit.plugin.Plugin; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +import java.io.PrintStream; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.GREEN; +import static net.kyori.adventure.text.format.NamedTextColor.RED; + +@DefaultQualifier(NonNull.class) +public final class DumpPluginsCommand implements PaperSubcommand { + @Override + public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { + this.dumpPlugins(sender, args); + return true; + } + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss"); + + private void dumpPlugins(final CommandSender sender, final String[] args) { + Path parent = Path.of("debug"); + Path path = parent.resolve("plugin-info" + FORMATTER.format(LocalDateTime.now()) + ".txt"); + try { + Files.createDirectories(parent); + Files.createFile(path); + sender.sendMessage(text("Writing plugin information to " + path, GREEN)); + + final JsonObject data = this.writeDebug(); + + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.setIndent(" "); + jsonWriter.setLenient(false); + Streams.write(data, jsonWriter); + + try (PrintStream out = new PrintStream(Files.newOutputStream(path), false, StandardCharsets.UTF_8)) { + out.print(stringWriter); + } + sender.sendMessage(text("Successfully written plugin debug information!", GREEN)); + } catch (Throwable e) { + sender.sendMessage(text("Failed to write plugin information! See the console for more info.", RED)); + MinecraftServer.LOGGER.warn("Error occurred while dumping plugin info", e); + } + } + + private JsonObject writeDebug() { + JsonObject root = new JsonObject(); + if (ConfiguredProviderStorage.LEGACY_PLUGIN_LOADING) { + root.addProperty("legacy-loading-strategy", true); + } + + this.writeProviders(root); + this.writePlugins(root); + this.writeClassloaders(root); + + return root; + } + + private void writeProviders(JsonObject root) { + JsonObject rootProviders = new JsonObject(); + root.add("providers", rootProviders); + + for (Map.Entry, ProviderStorage> entry : LaunchEntryPointHandler.INSTANCE.getStorage().entrySet()) { + JsonObject entrypoint = new JsonObject(); + + JsonArray providers = new JsonArray(); + entrypoint.add("providers", providers); + + List> pluginProviders = new ArrayList<>(); + for (PluginProvider provider : entry.getValue().getRegisteredProviders()) { + JsonObject providerObj = new JsonObject(); + providerObj.addProperty("name", provider.getMeta().getName()); + providerObj.addProperty("version", provider.getMeta().getVersion()); + providerObj.addProperty("dependencies", provider.getMeta().getPluginDependencies().toString()); + providerObj.addProperty("soft-dependencies", provider.getMeta().getPluginSoftDependencies().toString()); + providerObj.addProperty("load-before", provider.getMeta().getLoadBeforePlugins().toString()); + + + providers.add(providerObj); + pluginProviders.add((PluginProvider) provider); + } + + JsonArray loadOrder = new JsonArray(); + entrypoint.add("load-order", loadOrder); + + ModernPluginLoadingStrategy modernPluginLoadingStrategy = new ModernPluginLoadingStrategy<>(new ProviderConfiguration<>() { + @Override + public void applyContext(PluginProvider provider, DependencyContext dependencyContext) { + } + + @Override + public boolean load(PluginProvider provider, Object provided) { + loadOrder.add(provider.getMeta().getName()); + return false; + } + + @Override + public List requiredDependencies(PluginProvider provider) { + return provider.getMeta().getPluginDependencies(); + } + + @Override + public List optionalDependencies(PluginProvider provider) { + return provider.getMeta().getPluginSoftDependencies(); + } + + @Override + public List loadBeforeDependencies(PluginProvider provider) { + return provider.getMeta().getLoadBeforePlugins(); + } + }); + modernPluginLoadingStrategy.loadProviders(pluginProviders); + + rootProviders.add(entry.getKey().getDebugName(), entrypoint); + } + } + + private void writePlugins(JsonObject root) { + JsonArray rootPlugins = new JsonArray(); + root.add("plugins", rootPlugins); + + for (Plugin plugin : PaperPluginManagerImpl.getInstance().getPlugins()) { + rootPlugins.add(plugin.toString()); + } + } + + private void writeClassloaders(JsonObject root) { + JsonObject classLoadersRoot = new JsonObject(); + root.add("classloaders", classLoadersRoot); + + PaperPluginClassLoaderStorage storage = (PaperPluginClassLoaderStorage) PaperClassLoaderStorage.instance(); + classLoadersRoot.addProperty("global", storage.getGlobalGroup().toString()); + classLoadersRoot.addProperty("dependency_graph", PaperPluginManagerImpl.getInstance().getInstanceManagerGraph().toString()); + + JsonArray array = new JsonArray(); + classLoadersRoot.add("children", array); + for (PluginClassLoaderGroup group : storage.getGroups()) { + array.add(this.writeClassloader(group)); + } + } + + private JsonObject writeClassloader(PluginClassLoaderGroup group) { + JsonObject classLoadersRoot = new JsonObject(); + if (group instanceof SimpleListPluginClassLoaderGroup listGroup) { + JsonArray array = new JsonArray(); + classLoadersRoot.addProperty("main", listGroup.toString()); + classLoadersRoot.add("children", array); + for (ConfiguredPluginClassLoader innerGroup : listGroup.getClassLoaders()) { + array.add(this.writeClassloader(innerGroup)); + } + + } else if (group instanceof LockingClassLoaderGroup locking) { + // Unwrap + return this.writeClassloader(locking.getParent()); + } else { + classLoadersRoot.addProperty("raw", group.toString()); + } + + return classLoadersRoot; + } + + private JsonElement writeClassloader(ConfiguredPluginClassLoader innerGroup) { + return new JsonPrimitive(innerGroup.toString()); + } +} diff --git a/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java new file mode 100644 index 0000000000000000000000000000000000000000..9926eb59b83abffffb578356f148f045edc027cb --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java @@ -0,0 +1,88 @@ +package io.papermc.paper.plugin; + +import com.mojang.logging.LogUtils; +import io.papermc.paper.configuration.PaperConfigurations; +import joptsimple.OptionSet; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class PluginInitializerManager { + + private static final Logger LOGGER = LogUtils.getLogger(); + private static PluginInitializerManager impl; + private final Path pluginDirectory; + private final Path updateDirectory; + + PluginInitializerManager(final Path pluginDirectory, final Path updateDirectory) { + this.pluginDirectory = pluginDirectory; + this.updateDirectory = updateDirectory; + } + + private static PluginInitializerManager parse(@NotNull final OptionSet minecraftOptionSet) throws Exception { + // We have to load the bukkit configuration inorder to get the update folder location. + final File configFileLocationBukkit = (File) minecraftOptionSet.valueOf("bukkit-settings"); + + final Path pluginDirectory = ((File) minecraftOptionSet.valueOf("plugins")).toPath(); + + final YamlConfiguration configuration = PaperConfigurations.loadLegacyConfigFile(configFileLocationBukkit); + + final String updateDirectoryName = configuration.getString("settings.update-folder", "update"); + if (updateDirectoryName.isBlank()) { + return new PluginInitializerManager(pluginDirectory, null); + } + + final Path resolvedUpdateDirectory = pluginDirectory.resolve(updateDirectoryName); + if (!Files.isDirectory(resolvedUpdateDirectory)) { + LOGGER.error("Misconfigured update directory!"); + LOGGER.error(("Your configured update directory (%s) in bukkit.yml is pointing to a non-directory path. " + + "Disabling auto updating functionality.").formatted(resolvedUpdateDirectory)); + return new PluginInitializerManager(pluginDirectory, null); + } + + boolean isSameFile; + try { + isSameFile = Files.isSameFile(resolvedUpdateDirectory, pluginDirectory); + } catch (final IOException e) { + LOGGER.error("Misconfigured update directory!"); + LOGGER.error("Failed to compare update/plugin directory", e); + return new PluginInitializerManager(pluginDirectory, null); + } + + if (isSameFile) { + LOGGER.error("Misconfigured update directory!"); + LOGGER.error(("Your configured update directory (%s) in bukkit.yml is pointing to the same location as the plugin directory (%s). " + + "Disabling auto updating functionality.").formatted(resolvedUpdateDirectory, pluginDirectory)); + + return new PluginInitializerManager(pluginDirectory, null); + } + + return new PluginInitializerManager(pluginDirectory, resolvedUpdateDirectory); + } + + public static PluginInitializerManager init(final OptionSet optionSet) throws Exception { + impl = parse(optionSet); + return impl; + } + + public static PluginInitializerManager instance() { + return impl; + } + + @NotNull + public Path pluginDirectoryPath() { + return pluginDirectory; + } + + @Nullable + public Path pluginUpdatePath() { + return updateDirectory; + } + +} diff --git a/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContextImpl.java b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContextImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..8249a7024537fccd99735b92abb1368e6647b5ae --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/bootstrap/PluginProviderContextImpl.java @@ -0,0 +1,40 @@ +package io.papermc.paper.plugin.bootstrap; + +import io.papermc.paper.plugin.PluginInitializerManager; +import io.papermc.paper.plugin.configuration.PluginMeta; +import io.papermc.paper.plugin.provider.PluginProvider; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; +import java.util.logging.Logger; + +public record PluginProviderContextImpl(PluginMeta config, Path dataFolder, + Logger logger) implements PluginProviderContext { + + public static PluginProviderContextImpl of(PluginMeta config, Logger logger) { + Path dataFolder = PluginInitializerManager.instance().pluginDirectoryPath().resolve(config.getDisplayName()); + + return new PluginProviderContextImpl(config, dataFolder, logger); + } + + public static PluginProviderContextImpl of(PluginProvider provider, Path pluginFolder) { + Path dataFolder = pluginFolder.resolve(provider.getMeta().getDisplayName()); + + return new PluginProviderContextImpl(provider.getMeta(), dataFolder, provider.getLogger()); + } + + @Override + public @NotNull PluginMeta getConfiguration() { + return this.config; + } + + @Override + public @NotNull Path getDataDirectory() { + return this.dataFolder; + } + + @Override + public @NotNull Logger getLogger() { + return this.logger; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/Entrypoint.java b/src/main/java/io/papermc/paper/plugin/entrypoint/Entrypoint.java new file mode 100644 index 0000000000000000000000000000000000000000..125008ac7db8b9f3fb57c49f8e4facc4ad4bb136 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/Entrypoint.java @@ -0,0 +1,25 @@ +package io.papermc.paper.plugin.entrypoint; + +import io.papermc.paper.plugin.bootstrap.PluginBootstrap; +import org.bukkit.plugin.java.JavaPlugin; + +/** + * Used to mark a certain place that {@link EntrypointHandler} will register {@link io.papermc.paper.plugin.provider.PluginProvider} under. + * Used for loading only certain providers at a certain time. + * @param provider type + */ +public final class Entrypoint { + + public static final Entrypoint BOOTSTRAPPER = new Entrypoint<>("bootstrapper"); + public static final Entrypoint PLUGIN = new Entrypoint<>("plugin"); + + private final String debugName; + + private Entrypoint(String debugName) { + this.debugName = debugName; + } + + public String getDebugName() { + return debugName; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/EntrypointHandler.java b/src/main/java/io/papermc/paper/plugin/entrypoint/EntrypointHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..b38e1e0f3d3055086f51bb191fd4b60ecf32d016 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/EntrypointHandler.java @@ -0,0 +1,14 @@ +package io.papermc.paper.plugin.entrypoint; + +import io.papermc.paper.plugin.provider.PluginProvider; + +/** + * Represents a register that will register providers at a certain {@link Entrypoint}, + * where then when the given {@link Entrypoint} is registered those will be loaded. + */ +public interface EntrypointHandler { + + void register(Entrypoint entrypoint, PluginProvider provider); + + void enter(Entrypoint entrypoint); +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java b/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..61a67971a41527c0e3b614bf48d2bc8eabd443b5 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/LaunchEntryPointHandler.java @@ -0,0 +1,60 @@ +package io.papermc.paper.plugin.entrypoint; + +import io.papermc.paper.plugin.provider.PluginProvider; +import io.papermc.paper.plugin.storage.BootstrapProviderStorage; +import io.papermc.paper.plugin.storage.ProviderStorage; +import io.papermc.paper.plugin.storage.ServerPluginProviderStorage; +import org.jetbrains.annotations.ApiStatus; + +import java.util.HashMap; +import java.util.Map; + +/** + * Used by the server to register/load plugin bootstrappers and plugins. + */ +public class LaunchEntryPointHandler implements EntrypointHandler { + + public static final LaunchEntryPointHandler INSTANCE = new LaunchEntryPointHandler(); + private final Map, ProviderStorage> storage = new HashMap<>(); + + LaunchEntryPointHandler() { + this.storage.put(Entrypoint.BOOTSTRAPPER, new BootstrapProviderStorage()); + this.storage.put(Entrypoint.PLUGIN, new ServerPluginProviderStorage()); + } + + // Utility + public static void enterBootstrappers() { + LaunchEntryPointHandler.INSTANCE.enter(Entrypoint.BOOTSTRAPPER); + } + + @Override + public void enter(Entrypoint entrypoint) { + ProviderStorage storage = this.storage.get(entrypoint); + if (storage == null) { + throw new IllegalArgumentException("No storage registered for entrypoint %s.".formatted(entrypoint)); + } + + storage.enter(); + } + + @Override + public void register(Entrypoint entrypoint, PluginProvider provider) { + ProviderStorage storage = this.get(entrypoint); + if (storage == null) { + throw new IllegalArgumentException("No storage registered for entrypoint %s.".formatted(entrypoint)); + } + + storage.register(provider); + } + + @SuppressWarnings("unchecked") + public ProviderStorage get(Entrypoint entrypoint) { + return (ProviderStorage) this.storage.get(entrypoint); + } + + // Debug only + @ApiStatus.Internal + public Map, ProviderStorage> getStorage() { + return storage; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/ClassloaderBytecodeModifier.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/ClassloaderBytecodeModifier.java new file mode 100644 index 0000000000000000000000000000000000000000..93b5196a960f3efbe0d28f5527ea2752426213ce --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/ClassloaderBytecodeModifier.java @@ -0,0 +1,22 @@ +package io.papermc.paper.plugin.entrypoint.classloader; + +import io.papermc.paper.plugin.configuration.PluginMeta; +import net.kyori.adventure.util.Services; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public interface ClassloaderBytecodeModifier { + + static ClassloaderBytecodeModifier bytecodeModifier() { + return Provider.INSTANCE; + } + + byte[] modify(PluginMeta config, byte[] bytecode); + + class Provider { + + private static final ClassloaderBytecodeModifier INSTANCE = Services.service(ClassloaderBytecodeModifier.class).orElseThrow(); + + } + +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java new file mode 100644 index 0000000000000000000000000000000000000000..f9a2c55a354c877749db3f92956de802ae575788 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperClassloaderBytecodeModifier.java @@ -0,0 +1,12 @@ +package io.papermc.paper.plugin.entrypoint.classloader; + +import io.papermc.paper.plugin.configuration.PluginMeta; + +// Stub, implement in future. +public class PaperClassloaderBytecodeModifier implements ClassloaderBytecodeModifier { + + @Override + public byte[] modify(PluginMeta configuration, byte[] bytecode) { + return bytecode; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperPluginClassLoader.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperPluginClassLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..f1a2d25079c841daff19f41f2b24fb8e97d2c7d3 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperPluginClassLoader.java @@ -0,0 +1,193 @@ +package io.papermc.paper.plugin.entrypoint.classloader; + +import io.papermc.paper.plugin.configuration.PluginMeta; +import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; +import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; +import io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage; +import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage; +import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup; +import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; +import org.bukkit.Bukkit; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.jar.JarFile; +import java.util.logging.Logger; + +/** + * This is similar to a {@link org.bukkit.plugin.java.PluginClassLoader} but is completely kept hidden from the api. + * This is only used with Paper plugins. + * + * @see PaperPluginClassLoaderStorage + */ +public class PaperPluginClassLoader extends PaperSimplePluginClassLoader implements ConfiguredPluginClassLoader { + + static { + registerAsParallelCapable(); + } + + private final ClassLoader libraryLoader; + private final Set seenIllegalAccess = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Logger logger; + @Nullable + private JavaPlugin loadedJavaPlugin; + @Nullable + private PluginClassLoaderGroup group; + + public PaperPluginClassLoader(Logger logger, Path source, JarFile file, PaperPluginMeta configuration, ClassLoader parentLoader, ClassLoader libraryLoader) throws IOException { + super(source, file, configuration, parentLoader); + this.libraryLoader = libraryLoader; + + this.logger = logger; + if (this.configuration.hasOpenClassloader()) { + this.group = PaperClassLoaderStorage.instance().registerOpenGroup(this); + } + } + + public void refreshClassloaderDependencyTree(DependencyContext dependencyContext) { + if (this.configuration.hasOpenClassloader()) { + return; + } + if (this.group != null) { + // We need to unregister the classloader inorder to allow for dependencies + // to be recalculated + PaperClassLoaderStorage.instance().unregisterClassloader(this); + } + + this.group = PaperClassLoaderStorage.instance().registerAccessBackedGroup(this, (classLoader) -> { + return dependencyContext.isTransitiveDependency(PaperPluginClassLoader.this.configuration, classLoader.getConfiguration()); + }); + } + + @Override + public URL getResource(String name) { + URL resource = findResource(name); + if (resource == null && this.libraryLoader != null) { + return this.libraryLoader.getResource(name); + } + return resource; + } + + @Override + public Enumeration getResources(String name) throws IOException { + List resources = new ArrayList<>(); + this.addEnumeration(resources, this.findResources(name)); + if (this.libraryLoader != null) { + addEnumeration(resources, this.libraryLoader.getResources(name)); + } + return Collections.enumeration(resources); + } + + private void addEnumeration(List list, Enumeration enumeration) { + while (enumeration.hasMoreElements()) { + list.add(enumeration.nextElement()); + } + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + return this.loadClass(name, resolve, true, true); + } + + @Override + public PluginMeta getConfiguration() { + return this.configuration; + } + + @Override + public Class loadClass(@NotNull String name, boolean resolve, boolean checkGroup, boolean checkLibraries) throws ClassNotFoundException { + try { + Class result = super.loadClass(name, resolve); + + // SPIGOT-6749: Library classes will appear in the above, but we don't want to return them to other plugins + if (checkGroup || result.getClassLoader() == this) { + return result; + } + } catch (ClassNotFoundException ignored) { + } + + if (checkLibraries) { + try { + return this.libraryLoader.loadClass(name); + } catch (ClassNotFoundException ignored) { + } + } + + if (checkGroup) { + // This ignores the libraries of other plugins, unless they are transitive dependencies. + if (this.group == null) { + throw new IllegalStateException("Tried to resolve class while group was not yet initialized"); + } + + Class clazz = this.group.getClassByName(name, resolve, this); + if (clazz != null) { + return clazz; + } + } + + throw new ClassNotFoundException(name); + } + + @Override + public void init(JavaPlugin plugin) { + PluginMeta config = this.configuration; + PluginDescriptionFile pluginDescriptionFile = new PluginDescriptionFile( + config.getName(), + config.getDisplayName(), + config.getProvidedPlugins(), + config.getMainClass(), + "", // Classloader load order api + List.of(), // Dependencies + List.of(), // Soft Depends + List.of(), // Load Before + config.getVersion(), + Map.of(), // Commands, we use a separate system + config.getDescription(), + config.getAuthors(), + config.getContributors(), + config.getWebsite(), + config.getLoggerPrefix(), + config.getLoadOrder(), + config.getPermissions(), + config.getPermissionDefault(), + Set.of(), // Aware api + config.getAPIVersion(), + List.of() // Libraries + ); + + File dataFolder = new File(Bukkit.getPluginsFolder(), pluginDescriptionFile.getName()); + + plugin.init(Bukkit.getServer(), pluginDescriptionFile, dataFolder, this.source.toFile(), this, config); + plugin.logger = this.logger; + + this.loadedJavaPlugin = plugin; + } + + @Nullable + public JavaPlugin getLoadedJavaPlugin() { + return this.loadedJavaPlugin; + } + + @Override + public String toString() { + return "PaperPluginClassLoader{" + + "libraryLoader=" + this.libraryLoader + + ", seenIllegalAccess=" + this.seenIllegalAccess + + ", loadedJavaPlugin=" + this.loadedJavaPlugin + + ", group=" + this.group + + '}'; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperSimplePluginClassLoader.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperSimplePluginClassLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..6688f2af4a32c2f79785f162e2eac0330d439ac1 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/PaperSimplePluginClassLoader.java @@ -0,0 +1,116 @@ +package io.papermc.paper.plugin.entrypoint.classloader; + +import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; +import io.papermc.paper.plugin.util.NamespaceChecker; +import org.jetbrains.annotations.ApiStatus; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.security.CodeSigner; +import java.security.CodeSource; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +/** + * Represents a simple classloader used for paper plugin bootstrappers. + */ +@ApiStatus.Internal +public class PaperSimplePluginClassLoader extends URLClassLoader { + + static { + ClassLoader.registerAsParallelCapable(); + } + + protected final PaperPluginMeta configuration; + protected final Path source; + protected final Manifest jarManifest; + protected final URL jarUrl; + protected final JarFile jar; + + public PaperSimplePluginClassLoader(Path source, JarFile file, PaperPluginMeta configuration, ClassLoader parentLoader) throws IOException { + super(source.getFileName().toString(), new URL[]{source.toUri().toURL()}, parentLoader); + + this.source = source; + this.jarManifest = file.getManifest(); + this.jarUrl = source.toUri().toURL(); + this.configuration = configuration; + this.jar = file; + } + + @Override + public URL getResource(String name) { + return this.findResource(name); + } + + @Override + public Enumeration getResources(String name) throws IOException { + return this.findResources(name); + } + + // Bytecode modification supported loader + @Override + protected Class findClass(String name) throws ClassNotFoundException { + NamespaceChecker.validateNameSpaceForClassloading(name); + + // See UrlClassLoader#findClass(String) + String path = name.replace('.', '/').concat(".class"); + JarEntry entry = this.jar.getJarEntry(path); + if (entry == null) { + throw new ClassNotFoundException(); + } + + // See URLClassLoader#defineClass(String, Resource) + byte[] classBytes; + + try (InputStream is = this.jar.getInputStream(entry)) { + classBytes = is.readAllBytes(); + } catch (IOException ex) { + throw new ClassNotFoundException(name, ex); + } + + classBytes = ClassloaderBytecodeModifier.bytecodeModifier().modify(this.configuration, classBytes); + + int dot = name.lastIndexOf('.'); + if (dot != -1) { + String pkgName = name.substring(0, dot); + // Get defined package does not correctly handle sealed packages. + if (this.getDefinedPackage(pkgName) == null) { + try { + if (this.jarManifest != null) { + this.definePackage(pkgName, this.jarManifest, this.jarUrl); + } else { + this.definePackage(pkgName, null, null, null, null, null, null, null); + } + } catch (IllegalArgumentException ex) { + // parallel-capable class loaders: re-verify in case of a + // race condition + if (this.getDefinedPackage(pkgName) == null) { + // Should never happen + throw new IllegalStateException("Cannot find package " + pkgName); + } + } + } + } + + CodeSigner[] signers = entry.getCodeSigners(); + CodeSource source = new CodeSource(this.jarUrl, signers); + + return this.defineClass(name, classBytes, 0, classBytes.length, source); + } + + @Override + public String toString() { + return "PaperSimplePluginClassLoader{" + + "configuration=" + this.configuration + + ", source=" + this.source + + ", jarManifest=" + this.jarManifest + + ", jarUrl=" + this.jarUrl + + ", jar=" + this.jar + + '}'; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/DependencyBasedPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/DependencyBasedPluginClassLoaderGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..eaf5c794cbe8d6138c9d60eaae20f5fc7711f541 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/DependencyBasedPluginClassLoaderGroup.java @@ -0,0 +1,47 @@ +package io.papermc.paper.plugin.entrypoint.classloader.group; + +import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess; +import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; +import org.jetbrains.annotations.ApiStatus; + +import java.util.ArrayList; + +@ApiStatus.Internal +public class DependencyBasedPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup { + + private final GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup; + private final ClassLoaderAccess access; + + public DependencyBasedPluginClassLoaderGroup(GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup, ClassLoaderAccess access) { + super(new ArrayList<>()); + this.access = access; + this.globalPluginClassLoaderGroup = globalPluginClassLoaderGroup; + } + + /** + * This will refresh the dependencies of the current classloader. + */ + public void populateDependencies() { + this.classloaders.clear(); + for (ConfiguredPluginClassLoader configuredPluginClassLoader : this.globalPluginClassLoaderGroup.getClassLoaders()) { + if (this.access.canAccess(configuredPluginClassLoader)) { + this.classloaders.add(configuredPluginClassLoader); + } + } + + } + + @Override + public ClassLoaderAccess getAccess() { + return this.access; + } + + @Override + public String toString() { + return "DependencyBasedPluginClassLoaderGroup{" + + "globalPluginClassLoaderGroup=" + this.globalPluginClassLoaderGroup + + ", access=" + this.access + + ", classloaders=" + this.classloaders + + '}'; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/GlobalPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/GlobalPluginClassLoaderGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..2b7eef787f83e5a32896cb30c215406b6f652786 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/GlobalPluginClassLoaderGroup.java @@ -0,0 +1,18 @@ +package io.papermc.paper.plugin.entrypoint.classloader.group; + +import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess; +import org.jetbrains.annotations.ApiStatus; + +@ApiStatus.Internal +public class GlobalPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup { + + @Override + public ClassLoaderAccess getAccess() { + return (v) -> true; + } + + @Override + public String toString() { + return super.toString(); + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/LockingClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/LockingClassLoaderGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..aae50ebba6ba1579b75af5370c8b020d2a927b2c --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/LockingClassLoaderGroup.java @@ -0,0 +1,76 @@ +package io.papermc.paper.plugin.entrypoint.classloader.group; + +import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess; +import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; +import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +@ApiStatus.Internal +public class LockingClassLoaderGroup implements PluginClassLoaderGroup { + + private final PluginClassLoaderGroup parent; + private final Map classLoadLock = new HashMap<>(); + + public LockingClassLoaderGroup(PluginClassLoaderGroup parent) { + this.parent = parent; + } + + @Override + public @Nullable Class getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) { + // make MT safe + ClassLockEntry lock; + synchronized (this.classLoadLock) { + lock = this.classLoadLock.computeIfAbsent(name, (x) -> new ClassLockEntry(new AtomicInteger(0), new java.util.concurrent.locks.ReentrantReadWriteLock())); + lock.count.incrementAndGet(); + } + lock.reentrantReadWriteLock.writeLock().lock(); + try { + return parent.getClassByName(name, resolve, requester); + } finally { + synchronized (this.classLoadLock) { + lock.reentrantReadWriteLock.writeLock().unlock(); + if (lock.count.get() == 1) { + this.classLoadLock.remove(name); + } else { + lock.count.decrementAndGet(); + } + } + } + } + + @Override + public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) { + this.parent.remove(configuredPluginClassLoader); + } + + @Override + public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) { + this.parent.add(configuredPluginClassLoader); + } + + @Override + public ClassLoaderAccess getAccess() { + return this.parent.getAccess(); + } + + public PluginClassLoaderGroup getParent() { + return parent; + } + + record ClassLockEntry(AtomicInteger count, ReentrantReadWriteLock reentrantReadWriteLock) { + } + + @Override + public String toString() { + return "LockingClassLoaderGroup{" + + "parent=" + this.parent + + ", classLoadLock=" + this.classLoadLock + + '}'; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/PaperPluginClassLoaderStorage.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/PaperPluginClassLoaderStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..2c906e2c7d972b221a41acd614e00d0fbc1227c6 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/PaperPluginClassLoaderStorage.java @@ -0,0 +1,93 @@ +package io.papermc.paper.plugin.entrypoint.classloader.group; + +import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess; +import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; +import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage; +import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup; +import org.bukkit.Bukkit; +import org.bukkit.plugin.java.PluginClassLoader; +import org.jetbrains.annotations.ApiStatus; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * This is used for connecting multiple classloaders. + */ +public final class PaperPluginClassLoaderStorage implements PaperClassLoaderStorage { + + private final GlobalPluginClassLoaderGroup globalGroup = new GlobalPluginClassLoaderGroup(); + private final List groups = new CopyOnWriteArrayList<>(); + + public PaperPluginClassLoaderStorage() { + this.groups.add(this.globalGroup); + } + + @Override + public PluginClassLoaderGroup registerSpigotGroup(PluginClassLoader pluginClassLoader) { + return this.registerGroup(pluginClassLoader, new SpigotPluginClassLoaderGroup(this.globalGroup, (library) -> { + return Bukkit.getServer().getPluginManager().isTransitiveDependency(pluginClassLoader.getConfiguration(), library.getConfiguration()); + })); + } + + @Override + public PluginClassLoaderGroup registerOpenGroup(ConfiguredPluginClassLoader classLoader) { + return this.registerGroup(classLoader, this.globalGroup); + } + + @Override + public PluginClassLoaderGroup registerAccessBackedGroup(ConfiguredPluginClassLoader classLoader, ClassLoaderAccess access) { + List allowedLoaders = new ArrayList<>(); + for (ConfiguredPluginClassLoader configuredPluginClassLoader : this.globalGroup.getClassLoaders()) { + if (access.canAccess(configuredPluginClassLoader)) { + allowedLoaders.add(configuredPluginClassLoader); + } + } + + return this.registerGroup(classLoader, new StaticPluginClassLoaderGroup(allowedLoaders, access)); + } + + private PluginClassLoaderGroup registerGroup(ConfiguredPluginClassLoader classLoader, PluginClassLoaderGroup group) { + // Now add this classloader to any groups that allows it (includes global) + for (PluginClassLoaderGroup loaderGroup : this.groups) { + if (loaderGroup.getAccess().canAccess(classLoader)) { + loaderGroup.add(classLoader); + } + } + + group = new LockingClassLoaderGroup(group); + this.groups.add(group); + return group; + } + + @Override + public void unregisterClassloader(ConfiguredPluginClassLoader configuredPluginClassLoader) { + this.globalGroup.remove(configuredPluginClassLoader); + for (PluginClassLoaderGroup group : this.groups) { + group.remove(configuredPluginClassLoader); + } + } + + @Override + public boolean registerUnsafePlugin(ConfiguredPluginClassLoader pluginLoader) { + if (this.globalGroup.getClassLoaders().contains(pluginLoader)) { + return false; + } else { + this.globalGroup.getClassLoaders().add(pluginLoader); + return true; + } + } + + // Debug only + @ApiStatus.Internal + public GlobalPluginClassLoaderGroup getGlobalGroup() { + return this.globalGroup; + } + + // Debug only + @ApiStatus.Internal + public List getGroups() { + return this.groups; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SimpleListPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SimpleListPluginClassLoaderGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..23b6cb297f46c9c2b2944a3ab4031c31414620ad --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SimpleListPluginClassLoaderGroup.java @@ -0,0 +1,69 @@ +package io.papermc.paper.plugin.entrypoint.classloader.group; + +import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; +import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +@ApiStatus.Internal +public abstract class SimpleListPluginClassLoaderGroup implements PluginClassLoaderGroup { + + private static final boolean DISABLE_CLASS_PRIORITIZATION = Boolean.getBoolean("Paper.DisableClassPrioritization"); + + protected final List classloaders; + + protected SimpleListPluginClassLoaderGroup() { + this(new CopyOnWriteArrayList<>()); + } + + protected SimpleListPluginClassLoaderGroup(List classloaders) { + this.classloaders = classloaders; + } + + @Override + public @Nullable Class getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) { + if (!DISABLE_CLASS_PRIORITIZATION) { + try { + return this.lookupClass(name, false, requester); // First check the requester + } catch (ClassNotFoundException ignored) { + } + } + + for (ConfiguredPluginClassLoader loader : this.classloaders) { + try { + return this.lookupClass(name, resolve, loader); + } catch (ClassNotFoundException ignored) { + } + } + + return null; + } + + protected Class lookupClass(String name, boolean resolve, ConfiguredPluginClassLoader current) throws ClassNotFoundException { + return current.loadClass(name, resolve, false, true); + } + + @Override + public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) { + this.classloaders.remove(configuredPluginClassLoader); + } + + @Override + public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) { + this.classloaders.add(configuredPluginClassLoader); + } + + public List getClassLoaders() { + return classloaders; + } + + @Override + public String toString() { + return "SimpleListPluginClassLoaderGroup{" + + "classloaders=" + this.classloaders + + '}'; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SingletonPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SingletonPluginClassLoaderGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..3b670bd6b35ae7f56488a9b50df54709a0b28901 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SingletonPluginClassLoaderGroup.java @@ -0,0 +1,60 @@ +package io.papermc.paper.plugin.entrypoint.classloader.group; + +import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess; +import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; +import io.papermc.paper.plugin.provider.classloader.PluginClassLoaderGroup; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public class SingletonPluginClassLoaderGroup implements PluginClassLoaderGroup { + + private final ConfiguredPluginClassLoader configuredPluginClassLoader; + private final Access access; + + public SingletonPluginClassLoaderGroup(ConfiguredPluginClassLoader configuredPluginClassLoader) { + this.configuredPluginClassLoader = configuredPluginClassLoader; + this.access = new Access(); + } + + @Override + public @Nullable Class getClassByName(String name, boolean resolve, ConfiguredPluginClassLoader requester) { + try { + return this.configuredPluginClassLoader.loadClass(name, resolve, false, true); + } catch (ClassNotFoundException ignored) { + } + + return null; + } + + @Override + public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) { + } + + @Override + public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) { + } + + @Override + public ClassLoaderAccess getAccess() { + return this.access; + } + + @ApiStatus.Internal + private class Access implements ClassLoaderAccess { + + @Override + public boolean canAccess(ConfiguredPluginClassLoader classLoader) { + return SingletonPluginClassLoaderGroup.this.configuredPluginClassLoader == classLoader; + } + + } + + @Override + public String toString() { + return "SingletonPluginClassLoaderGroup{" + + "configuredPluginClassLoader=" + this.configuredPluginClassLoader + + ", access=" + this.access + + '}'; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SpigotPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SpigotPluginClassLoaderGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..5d26367524389388be163ae3120c1d2bf55cfef7 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/SpigotPluginClassLoaderGroup.java @@ -0,0 +1,49 @@ +package io.papermc.paper.plugin.entrypoint.classloader.group; + +import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess; +import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Predicate; + +/** + * Spigot classloaders have the ability to see everything. + * However, libraries are ONLY shared depending on their dependencies. + */ +@ApiStatus.Internal +public class SpigotPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup { + + private final Predicate libraryClassloaderPredicate; + + public SpigotPluginClassLoaderGroup(GlobalPluginClassLoaderGroup globalPluginClassLoaderGroup, Predicate libraryClassloaderPredicate) { + super(globalPluginClassLoaderGroup.getClassLoaders()); + this.libraryClassloaderPredicate = libraryClassloaderPredicate; + } + + // Mirrors global list + @Override + public void add(ConfiguredPluginClassLoader configuredPluginClassLoader) { + } + + @Override + public void remove(ConfiguredPluginClassLoader configuredPluginClassLoader) { + } + + @Override + protected Class lookupClass(String name, boolean resolve, ConfiguredPluginClassLoader current) throws ClassNotFoundException { + return current.loadClass(name, resolve, false, this.libraryClassloaderPredicate.test(current)); + } + + @Override + public ClassLoaderAccess getAccess() { + return v -> true; + } + + @Override + public String toString() { + return "SpigotPluginClassLoaderGroup{" + + "libraryClassloaderPredicate=" + this.libraryClassloaderPredicate + + ", classloaders=" + this.classloaders + + '}'; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/StaticPluginClassLoaderGroup.java b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/StaticPluginClassLoaderGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..7f7085b06271adf8a37485f4c9c9b8af605dd27d --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/classloader/group/StaticPluginClassLoaderGroup.java @@ -0,0 +1,31 @@ +package io.papermc.paper.plugin.entrypoint.classloader.group; + +import io.papermc.paper.plugin.provider.classloader.ClassLoaderAccess; +import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; +import org.jetbrains.annotations.ApiStatus; + +import java.util.List; + +@ApiStatus.Internal +public class StaticPluginClassLoaderGroup extends SimpleListPluginClassLoaderGroup { + + private final ClassLoaderAccess access; + + public StaticPluginClassLoaderGroup(List classloaders, ClassLoaderAccess access) { + super(classloaders); + this.access = access; + } + + @Override + public ClassLoaderAccess getAccess() { + return this.access; + } + + @Override + public String toString() { + return "StaticPluginClassLoaderGroup{" + + "access=" + this.access + + ", classloaders=" + this.classloaders + + '}'; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyContextHolder.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyContextHolder.java new file mode 100644 index 0000000000000000000000000000000000000000..f43295fdeaa587cf30c35a1d545167071d58ce4b --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyContextHolder.java @@ -0,0 +1,9 @@ +package io.papermc.paper.plugin.entrypoint.dependency; + +import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; + +public interface DependencyContextHolder { + + void setContext(DependencyContext context); + +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyUtil.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..1af3c3434eb1f3b00857b17a07f42e51086c1e2b --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/DependencyUtil.java @@ -0,0 +1,45 @@ +package io.papermc.paper.plugin.entrypoint.dependency; + +import com.google.common.graph.MutableGraph; +import io.papermc.paper.plugin.configuration.PluginMeta; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("UnstableApiUsage") +public class DependencyUtil { + + @NotNull + public static MutableGraph buildDependencyGraph(@NotNull MutableGraph dependencyGraph, @NotNull PluginMeta configuration) { + List dependencies = new ArrayList<>(); + dependencies.addAll(configuration.getPluginDependencies()); + dependencies.addAll(configuration.getPluginSoftDependencies()); + + return buildDependencyGraph(dependencyGraph, configuration.getName(), dependencies, configuration.getLoadBeforePlugins()); + } + + @NotNull + public static MutableGraph buildDependencyGraph(@NotNull MutableGraph dependencyGraph, String identifier, @NotNull Iterable depends, @NotNull Iterable loadBefore) { + for (String dependency : depends) { + dependencyGraph.putEdge(identifier, dependency); + } + + for (String loadBeforeTarget : loadBefore) { + dependencyGraph.putEdge(loadBeforeTarget, identifier); + } + + dependencyGraph.addNode(identifier); // Make sure dependencies at least have a node + return dependencyGraph; + } + + // This adds a provided plugin to another plugin, basically making it seem like a "dependency" + // in order to have plugins that need the provided plugin to load after the specified plugin name + @NotNull + public static MutableGraph addProvidedPlugin(@NotNull MutableGraph dependencyGraph, @NotNull String pluginName, @NotNull String providedName) { + dependencyGraph.putEdge(pluginName, providedName); + + return dependencyGraph; + } + +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/GraphDependencyContext.java b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/GraphDependencyContext.java new file mode 100644 index 0000000000000000000000000000000000000000..6f201a8131ca9631ac4af62c75e6f2e889cb5eae --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/dependency/GraphDependencyContext.java @@ -0,0 +1,43 @@ +package io.papermc.paper.plugin.entrypoint.dependency; + +import com.google.common.graph.Graph; +import com.google.common.graph.Graphs; +import io.papermc.paper.plugin.configuration.PluginMeta; +import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; + +import java.util.Set; + +@SuppressWarnings("UnstableApiUsage") +public class GraphDependencyContext implements DependencyContext { + + private final Graph dependencyGraph; + + public GraphDependencyContext(Graph dependencyGraph) { + this.dependencyGraph = dependencyGraph; + } + + @Override + public boolean isTransitiveDependency(PluginMeta plugin, PluginMeta depend) { + String pluginIdentifier = plugin.getName(); + + if (this.dependencyGraph.nodes().contains(pluginIdentifier)) { + Set reachableNodes = Graphs.reachableNodes(this.dependencyGraph, pluginIdentifier); + if (reachableNodes.contains(depend.getName())) { + return true; + } + for (String provided : depend.getProvidedPlugins()) { + if (reachableNodes.contains(provided)) { + return true; + } + } + } + + return false; + } + + @Override + public boolean hasDependency(String pluginIdentifier) { + return this.dependencyGraph.nodes().contains(pluginIdentifier); + } + +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java new file mode 100644 index 0000000000000000000000000000000000000000..22189a1c42459c00d3e8bdeb980d15a69b720805 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/JohnsonSimpleCycles.java @@ -0,0 +1,350 @@ +/* + * (C) Copyright 2013-2021, by Nikolay Ognyanov and Contributors. + * + * JGraphT : a free Java graph-theory library + * + * See the CONTRIBUTORS.md file distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the + * GNU Lesser General Public License v2.1 or later + * which is available at + * http://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html. + * + * SPDX-License-Identifier: EPL-2.0 OR LGPL-2.1-or-later + */ + +// MODIFICATIONS: +// - Modified to use a guava graph directly + +package io.papermc.paper.plugin.entrypoint.strategy; + +import com.google.common.base.Preconditions; +import com.google.common.graph.Graph; +import com.google.common.graph.GraphBuilder; +import com.google.common.graph.MutableGraph; +import com.mojang.datafixers.util.Pair; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +/** + * Find all simple cycles of a directed graph using the Johnson's algorithm. + * + *

+ * See:
+ * D.B.Johnson, Finding all the elementary circuits of a directed graph, SIAM J. Comput., 4 (1975), + * pp. 77-84. + * + * @param the vertex type. + * + * @author Nikolay Ognyanov + */ +public class JohnsonSimpleCycles +{ + // The graph. + private Graph graph; + + // The main state of the algorithm. + private Consumer> cycleConsumer = null; + private V[] iToV = null; + private Map vToI = null; + private Set blocked = null; + private Map> bSets = null; + private ArrayDeque stack = null; + + // The state of the embedded Tarjan SCC algorithm. + private List> foundSCCs = null; + private int index = 0; + private Map vIndex = null; + private Map vLowlink = null; + private ArrayDeque path = null; + private Set pathSet = null; + + /** + * Create a simple cycle finder for the specified graph. + * + * @param graph - the DirectedGraph in which to find cycles. + * + * @throws IllegalArgumentException if the graph argument is + * null. + */ + public JohnsonSimpleCycles(Graph graph) + { + Preconditions.checkState(graph.isDirected(), "Graph must be directed"); + this.graph = graph; + } + + /** + * Find the simple cycles of the graph. + * + * @return The list of all simple cycles. Possibly empty but never null. + */ + public List> findSimpleCycles() + { + List> result = new ArrayList<>(); + findSimpleCycles(result::add); + return result; + } + + /** + * Find the simple cycles of the graph. + * + * @param consumer Consumer that will be called with each cycle found. + */ + public void findSimpleCycles(Consumer> consumer) + { + if (graph == null) { + throw new IllegalArgumentException("Null graph."); + } + initState(consumer); + + int startIndex = 0; + int size = graph.nodes().size(); + while (startIndex < size) { + Pair, Integer> minSCCGResult = findMinSCSG(startIndex); + if (minSCCGResult != null) { + startIndex = minSCCGResult.getSecond(); + Graph scg = minSCCGResult.getFirst(); + V startV = toV(startIndex); + for (V v : scg.successors(startV)) { + blocked.remove(v); + getBSet(v).clear(); + } + findCyclesInSCG(startIndex, startIndex, scg); + startIndex++; + } else { + break; + } + } + + clearState(); + } + + private Pair, Integer> findMinSCSG(int startIndex) + { + /* + * Per Johnson : "adjacency structure of strong component $K$ with least vertex in subgraph + * of $G$ induced by $(s, s + 1, n)$". Or in contemporary terms: the strongly connected + * component of the subgraph induced by $(v_1, \dotso ,v_n)$ which contains the minimum + * (among those SCCs) vertex index. We return that index together with the graph. + */ + initMinSCGState(); + + List> foundSCCs = findSCCS(startIndex); + + // find the SCC with the minimum index + int minIndexFound = Integer.MAX_VALUE; + Set minSCC = null; + for (Set scc : foundSCCs) { + for (V v : scc) { + int t = toI(v); + if (t < minIndexFound) { + minIndexFound = t; + minSCC = scc; + } + } + } + if (minSCC == null) { + return null; + } + + // build a graph for the SCC found + MutableGraph dependencyGraph = GraphBuilder.directed().allowsSelfLoops(true).build(); + + for (V v : minSCC) { + for (V w : minSCC) { + if (graph.hasEdgeConnecting(v, w)) { + dependencyGraph.putEdge(v, w); + } + } + } + + Pair, Integer> result = Pair.of(dependencyGraph, minIndexFound); + clearMinSCCState(); + return result; + } + + private List> findSCCS(int startIndex) + { + // Find SCCs in the subgraph induced + // by vertices startIndex and beyond. + // A call to StrongConnectivityAlgorithm + // would be too expensive because of the + // need to materialize the subgraph. + // So - do a local search by the Tarjan's + // algorithm and pretend that vertices + // with an index smaller than startIndex + // do not exist. + for (V v : graph.nodes()) { + int vI = toI(v); + if (vI < startIndex) { + continue; + } + if (!vIndex.containsKey(v)) { + getSCCs(startIndex, vI); + } + } + List> result = foundSCCs; + foundSCCs = null; + return result; + } + + private void getSCCs(int startIndex, int vertexIndex) + { + V vertex = toV(vertexIndex); + vIndex.put(vertex, index); + vLowlink.put(vertex, index); + index++; + path.push(vertex); + pathSet.add(vertex); + + Set edges = graph.successors(vertex); + for (V successor : edges) { + int successorIndex = toI(successor); + if (successorIndex < startIndex) { + continue; + } + if (!vIndex.containsKey(successor)) { + getSCCs(startIndex, successorIndex); + vLowlink.put(vertex, Math.min(vLowlink.get(vertex), vLowlink.get(successor))); + } else if (pathSet.contains(successor)) { + vLowlink.put(vertex, Math.min(vLowlink.get(vertex), vIndex.get(successor))); + } + } + if (vLowlink.get(vertex).equals(vIndex.get(vertex))) { + Set result = new HashSet<>(); + V temp; + do { + temp = path.pop(); + pathSet.remove(temp); + result.add(temp); + } while (!vertex.equals(temp)); + if (result.size() == 1) { + V v = result.iterator().next(); + if (graph.edges().contains(vertex)) { + foundSCCs.add(result); + } + } else { + foundSCCs.add(result); + } + } + } + + private boolean findCyclesInSCG(int startIndex, int vertexIndex, Graph scg) + { + /* + * Find cycles in a strongly connected graph per Johnson. + */ + boolean foundCycle = false; + V vertex = toV(vertexIndex); + stack.push(vertex); + blocked.add(vertex); + + for (V successor : scg.successors(vertex)) { + int successorIndex = toI(successor); + if (successorIndex == startIndex) { + List cycle = new ArrayList<>(stack.size()); + stack.descendingIterator().forEachRemaining(cycle::add); + cycleConsumer.accept(cycle); + foundCycle = true; + } else if (!blocked.contains(successor)) { + boolean gotCycle = findCyclesInSCG(startIndex, successorIndex, scg); + foundCycle = foundCycle || gotCycle; + } + } + if (foundCycle) { + unblock(vertex); + } else { + for (V w : scg.successors(vertex)) { + Set bSet = getBSet(w); + bSet.add(vertex); + } + } + stack.pop(); + return foundCycle; + } + + private void unblock(V vertex) + { + blocked.remove(vertex); + Set bSet = getBSet(vertex); + while (bSet.size() > 0) { + V w = bSet.iterator().next(); + bSet.remove(w); + if (blocked.contains(w)) { + unblock(w); + } + } + } + + @SuppressWarnings("unchecked") + private void initState(Consumer> consumer) + { + cycleConsumer = consumer; + iToV = (V[]) graph.nodes().toArray(); + vToI = new HashMap<>(); + blocked = new HashSet<>(); + bSets = new HashMap<>(); + stack = new ArrayDeque<>(); + + for (int i = 0; i < iToV.length; i++) { + vToI.put(iToV[i], i); + } + } + + private void clearState() + { + cycleConsumer = null; + iToV = null; + vToI = null; + blocked = null; + bSets = null; + stack = null; + } + + private void initMinSCGState() + { + index = 0; + foundSCCs = new ArrayList<>(); + vIndex = new HashMap<>(); + vLowlink = new HashMap<>(); + path = new ArrayDeque<>(); + pathSet = new HashSet<>(); + } + + private void clearMinSCCState() + { + index = 0; + foundSCCs = null; + vIndex = null; + vLowlink = null; + path = null; + pathSet = null; + } + + private Integer toI(V vertex) + { + return vToI.get(vertex); + } + + private V toV(Integer i) + { + return iToV[i]; + } + + private Set getBSet(V v) + { + // B sets typically not all needed, + // so instantiate lazily. + return bSets.computeIfAbsent(v, k -> new HashSet<>()); + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java new file mode 100644 index 0000000000000000000000000000000000000000..c337852883f38f6c2d8d5afbed0c848b047701d9 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/LegacyPluginLoadingStrategy.java @@ -0,0 +1,260 @@ +package io.papermc.paper.plugin.entrypoint.strategy; + +import com.google.common.graph.GraphBuilder; +import com.google.common.graph.MutableGraph; +import io.papermc.paper.plugin.configuration.PluginMeta; +import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext; +import io.papermc.paper.plugin.provider.PluginProvider; +import org.bukkit.plugin.UnknownDependencyException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +@SuppressWarnings("UnstableApiUsage") +public class LegacyPluginLoadingStrategy implements ProviderLoadingStrategy { + + private static final Logger LOGGER = Logger.getLogger("LegacyPluginLoadingStrategy"); + private final ProviderConfiguration configuration; + + public LegacyPluginLoadingStrategy(ProviderConfiguration onLoad) { + this.configuration = onLoad; + } + + @Override + public List> loadProviders(List> providers) { + List> javapluginsLoaded = new ArrayList<>(); + MutableGraph dependencyGraph = GraphBuilder.directed().build(); + GraphDependencyContext dependencyContext = new GraphDependencyContext(dependencyGraph); + + Map> providersToLoad = new HashMap<>(); + Set loadedPlugins = new HashSet<>(); + Map pluginsProvided = new HashMap<>(); + Map> dependencies = new HashMap<>(); + Map> softDependencies = new HashMap<>(); + + for (PluginProvider provider : providers) { + PluginMeta configuration = provider.getMeta(); + + PluginProvider replacedProvider = providersToLoad.put(configuration.getName(), provider); + if (replacedProvider != null) { + LOGGER.severe(String.format( + "Ambiguous plugin name `%s' for files `%s' and `%s' in `%s'", + configuration.getName(), + provider.getSource(), + replacedProvider.getSource(), + replacedProvider.getParentSource() + )); + } + + String removedProvided = pluginsProvided.remove(configuration.getName()); + if (removedProvided != null) { + LOGGER.warning(String.format( + "Ambiguous plugin name `%s'. It is also provided by `%s'", + configuration.getName(), + removedProvided + )); + } + + for (String provided : configuration.getProvidedPlugins()) { + PluginProvider pluginProvider = providersToLoad.get(provided); + + if (pluginProvider != null) { + LOGGER.warning(String.format( + "`%s provides `%s' while this is also the name of `%s' in `%s'", + provider.getSource(), + provided, + pluginProvider.getSource(), + provider.getParentSource() + )); + } else { + String replacedPlugin = pluginsProvided.put(provided, configuration.getName()); + if (replacedPlugin != null) { + LOGGER.warning(String.format( + "`%s' is provided by both `%s' and `%s'", + provided, + configuration.getName(), + replacedPlugin + )); + } + } + } + + Collection softDependencySet = this.configuration.optionalDependencies(provider); + if (softDependencySet != null && !softDependencySet.isEmpty()) { + if (softDependencies.containsKey(configuration.getName())) { + // Duplicates do not matter, they will be removed together if applicable + softDependencies.get(configuration.getName()).addAll(softDependencySet); + } else { + softDependencies.put(configuration.getName(), new LinkedList(softDependencySet)); + } + + for (String depend : softDependencySet) { + dependencyGraph.putEdge(configuration.getName(), depend); + } + } + + Collection dependencySet = this.configuration.requiredDependencies(provider); + if (dependencySet != null && !dependencySet.isEmpty()) { + dependencies.put(configuration.getName(), new LinkedList(dependencySet)); + + for (String depend : dependencySet) { + dependencyGraph.putEdge(configuration.getName(), depend); + } + } + + Collection loadBeforeSet = this.configuration.loadBeforeDependencies(provider); + if (loadBeforeSet != null && !loadBeforeSet.isEmpty()) { + for (String loadBeforeTarget : loadBeforeSet) { + if (softDependencies.containsKey(loadBeforeTarget)) { + softDependencies.get(loadBeforeTarget).add(configuration.getName()); + } else { + // softDependencies is never iterated, so 'ghost' plugins aren't an issue + Collection shortSoftDependency = new LinkedList(); + shortSoftDependency.add(configuration.getName()); + softDependencies.put(loadBeforeTarget, shortSoftDependency); + } + + dependencyGraph.putEdge(loadBeforeTarget, configuration.getName()); + } + } + } + + while (!providersToLoad.isEmpty()) { + boolean missingDependency = true; + Iterator>> providerIterator = providersToLoad.entrySet().iterator(); + + while (providerIterator.hasNext()) { + Map.Entry> entry = providerIterator.next(); + String providerIdentifier = entry.getKey(); + + if (dependencies.containsKey(providerIdentifier)) { + Iterator dependencyIterator = dependencies.get(providerIdentifier).iterator(); + final Set missingHardDependencies = new HashSet<>(dependencies.get(providerIdentifier).size()); // Paper - list all missing hard depends + + while (dependencyIterator.hasNext()) { + String dependency = dependencyIterator.next(); + + // Dependency loaded + if (loadedPlugins.contains(dependency)) { + dependencyIterator.remove(); + + // We have a dependency not found + } else if (!providersToLoad.containsKey(dependency) && !pluginsProvided.containsKey(dependency)) { + // Paper start + missingHardDependencies.add(dependency); + } + } + if (!missingHardDependencies.isEmpty()) { + // Paper end + missingDependency = false; + providerIterator.remove(); + pluginsProvided.values().removeIf(s -> s.equals(providerIdentifier)); // Paper - remove provided plugins + softDependencies.remove(providerIdentifier); + dependencies.remove(providerIdentifier); + + LOGGER.log( + Level.SEVERE, + "Could not load '" + entry.getValue().getSource() + "' in folder '" + entry.getValue().getParentSource() + "'", // Paper + new UnknownDependencyException(missingHardDependencies, providerIdentifier)); // Paper + } + + if (dependencies.containsKey(providerIdentifier) && dependencies.get(providerIdentifier).isEmpty()) { + dependencies.remove(providerIdentifier); + } + } + if (softDependencies.containsKey(providerIdentifier)) { + Iterator softDependencyIterator = softDependencies.get(providerIdentifier).iterator(); + + while (softDependencyIterator.hasNext()) { + String softDependency = softDependencyIterator.next(); + + // Soft depend is no longer around + if (!providersToLoad.containsKey(softDependency) && !pluginsProvided.containsKey(softDependency)) { + softDependencyIterator.remove(); + } + } + + if (softDependencies.get(providerIdentifier).isEmpty()) { + softDependencies.remove(providerIdentifier); + } + } + if (!(dependencies.containsKey(providerIdentifier) || softDependencies.containsKey(providerIdentifier)) && providersToLoad.containsKey(providerIdentifier)) { + // We're clear to load, no more soft or hard dependencies left + PluginProvider file = providersToLoad.get(providerIdentifier); + providerIterator.remove(); + pluginsProvided.values().removeIf(s -> s.equals(providerIdentifier)); // Paper - remove provided plugins + missingDependency = false; + + try { + this.configuration.applyContext(file, dependencyContext); + T loadedPlugin = file.createInstance(); + + if (this.configuration.load(file, loadedPlugin)) { + loadedPlugins.add(file.getMeta().getName()); + loadedPlugins.addAll(file.getMeta().getProvidedPlugins()); + javapluginsLoaded.add(new ProviderPair<>(file, loadedPlugin)); + } + + } catch (Exception ex) { + LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "'", ex); // Paper + } + } + } + + if (missingDependency) { + // We now iterate over plugins until something loads + // This loop will ignore soft dependencies + providerIterator = providersToLoad.entrySet().iterator(); + + while (providerIterator.hasNext()) { + Map.Entry> entry = providerIterator.next(); + String plugin = entry.getKey(); + + if (!dependencies.containsKey(plugin)) { + softDependencies.remove(plugin); + missingDependency = false; + PluginProvider file = entry.getValue(); + providerIterator.remove(); + + try { + this.configuration.applyContext(file, dependencyContext); + T loadedPlugin = file.createInstance(); + + if (this.configuration.load(file, loadedPlugin)) { + loadedPlugins.add(file.getMeta().getName()); + loadedPlugins.addAll(file.getMeta().getProvidedPlugins()); + javapluginsLoaded.add(new ProviderPair<>(file, loadedPlugin)); + } + break; + } catch (Exception ex) { + LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "'", ex); // Paper + } + } + } + // We have no plugins left without a depend + if (missingDependency) { + softDependencies.clear(); + dependencies.clear(); + Iterator> failedPluginIterator = providersToLoad.values().iterator(); + + while (failedPluginIterator.hasNext()) { + PluginProvider file = failedPluginIterator.next(); + failedPluginIterator.remove(); + LOGGER.log(Level.SEVERE, "Could not load '" + file.getSource() + "' in folder '" + file.getParentSource() + "': circular dependency detected"); // Paper + } + } + } + } + + return javapluginsLoaded; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ModernPluginLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ModernPluginLoadingStrategy.java new file mode 100644 index 0000000000000000000000000000000000000000..30e56289cccc09456b3421f7960d8647b6610639 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ModernPluginLoadingStrategy.java @@ -0,0 +1,143 @@ +package io.papermc.paper.plugin.entrypoint.strategy; + +import com.google.common.collect.Lists; +import com.google.common.graph.GraphBuilder; +import com.google.common.graph.MutableGraph; +import com.mojang.logging.LogUtils; +import io.papermc.paper.plugin.configuration.PluginMeta; +import io.papermc.paper.plugin.entrypoint.dependency.DependencyUtil; +import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext; +import io.papermc.paper.plugin.provider.PluginProvider; +import org.bukkit.plugin.UnknownDependencyException; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@SuppressWarnings("UnstableApiUsage") +public class ModernPluginLoadingStrategy implements ProviderLoadingStrategy { + + private static final Logger LOGGER = LogUtils.getLogger(); + private final ProviderConfiguration configuration; + + public ModernPluginLoadingStrategy(ProviderConfiguration onLoad) { + this.configuration = onLoad; + } + + @Override + public List> loadProviders(List> pluginProviders) { + MutableGraph dependencyGraph = GraphBuilder.directed().build(); + Map> providerMap = new HashMap<>(); + List> validatedProviders = new ArrayList<>(); + + // Populate provider map + for (PluginProvider provider : pluginProviders) { + PluginMeta providerConfig = provider.getMeta(); + PluginProviderEntry entry = new PluginProviderEntry<>(provider); + + PluginProviderEntry replacedProvider = providerMap.put(providerConfig.getName(), entry); + if (replacedProvider != null) { + LOGGER.error(String.format( + "Ambiguous plugin name '%s' for files '%s' and '%s' in '%s'", + providerConfig.getName(), + provider.getSource(), + replacedProvider.provider.getSource(), + replacedProvider.provider.getParentSource() + )); + } + + for (String extra : providerConfig.getProvidedPlugins()) { + PluginProviderEntry replacedExtraProvider = providerMap.putIfAbsent(extra, entry); + if (replacedExtraProvider != null) { + LOGGER.warn(String.format( + "`%s' is provided by both `%s' and `%s'", + extra, + providerConfig.getName(), + replacedExtraProvider.provider.getMeta().getName() + )); + } + } + } + + // Validate providers, ensuring all of them have valid dependencies. Removing those who are invalid + for (PluginProvider provider : pluginProviders) { + PluginMeta configuration = provider.getMeta(); + + // Populate missing dependencies to capture if there are multiple missing ones. + List missingDependencies = new ArrayList<>(); + for (String hardDependency : this.configuration.requiredDependencies(provider)) { + if (!providerMap.containsKey(hardDependency)) { + missingDependencies.add(hardDependency); + } + } + + if (missingDependencies.isEmpty()) { + validatedProviders.add(provider); + } else { + LOGGER.error("Could not load '%s' in '%s'".formatted(provider.getSource(), provider.getParentSource()), new UnknownDependencyException(missingDependencies, configuration.getName())); // Paper + // Because the validator is invalid, remove it from the provider map + providerMap.remove(configuration.getName()); + } + } + + for (PluginProvider validated : validatedProviders) { + PluginMeta configuration = validated.getMeta(); + + // Build a validated provider's dependencies into the graph + DependencyUtil.buildDependencyGraph(dependencyGraph, configuration); + + // Add the provided plugins to the graph as well + for (String provides : configuration.getProvidedPlugins()) { + DependencyUtil.addProvidedPlugin(dependencyGraph, configuration.getName(), provides); + } + } + + // Reverse the topographic search to let us see which providers we can load first. + List reversedTopographicSort; + try { + reversedTopographicSort = Lists.reverse(TopographicGraphSorter.sortGraph(dependencyGraph)); + } catch (TopographicGraphSorter.GraphCycleException exception) { + throw new PluginGraphCycleException(new JohnsonSimpleCycles<>(dependencyGraph).findSimpleCycles()); + } + + GraphDependencyContext graphDependencyContext = new GraphDependencyContext(dependencyGraph); + List> loadedPlugins = new ArrayList<>(); + for (String providerIdentifier : reversedTopographicSort) { + // It's possible that this will be null because the above dependencies for soft/load before aren't validated if they exist. + // The graph could be MutableGraph>, but we would have to check if each dependency exists there... just + // nicer to do it here TBH. + PluginProviderEntry retrievedProviderEntry = providerMap.get(providerIdentifier); + if (retrievedProviderEntry == null || retrievedProviderEntry.provided) { + // OR if this was already provided (most likely from a plugin that already "provides" that dependency) + // This won't matter since the provided plugin is loaded as a dependency, meaning it should have been loaded correctly anyways + continue; // Skip provider that doesn't exist.... + } + retrievedProviderEntry.provided = true; + PluginProvider retrievedProvider = retrievedProviderEntry.provider; + try { + this.configuration.applyContext(retrievedProvider, graphDependencyContext); + + T instance = retrievedProvider.createInstance(); + if (this.configuration.load(retrievedProvider, instance)) { + loadedPlugins.add(new ProviderPair<>(retrievedProvider, instance)); + } + } catch (Exception ex) { + LOGGER.error("Could not load plugin '%s' in folder '%s'".formatted(retrievedProvider.getFileName(), retrievedProvider.getParentSource()), ex); // Paper + } + } + + return loadedPlugins; + } + + private static class PluginProviderEntry { + + private final PluginProvider provider; + private boolean provided; + + private PluginProviderEntry(PluginProvider provider) { + this.provider = provider; + } + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/PluginGraphCycleException.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/PluginGraphCycleException.java new file mode 100644 index 0000000000000000000000000000000000000000..2ea978ac957849260e7ca69c9ff56588d0ccc41b --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/PluginGraphCycleException.java @@ -0,0 +1,19 @@ +package io.papermc.paper.plugin.entrypoint.strategy; + +import java.util.List; + +/** + * Indicates a dependency cycle within a provider loading sequence. + */ +public class PluginGraphCycleException extends RuntimeException { + + private final List> cycles; + + public PluginGraphCycleException(List> cycles) { + this.cycles = cycles; + } + + public List> getCycles() { + return this.cycles; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..5e18616160014d70df2b539d7e65bb003ac7a4b7 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderConfiguration.java @@ -0,0 +1,26 @@ +package io.papermc.paper.plugin.entrypoint.strategy; + +import io.papermc.paper.plugin.provider.PluginProvider; +import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; + +import java.util.List; + +/** + * Used to share code with the modern and legacy plugin load strategy. + * + * @param + */ +public interface ProviderConfiguration { + + void applyContext(PluginProvider provider, DependencyContext dependencyContext); + + boolean load(PluginProvider provider, T provided); + + List requiredDependencies(PluginProvider provider); + + List optionalDependencies(PluginProvider provider); + + List loadBeforeDependencies(PluginProvider provider); + + +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java new file mode 100644 index 0000000000000000000000000000000000000000..dee83e821dcc9baf3a3e5ca8325b03ed2d5eb81c --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/ProviderLoadingStrategy.java @@ -0,0 +1,20 @@ +package io.papermc.paper.plugin.entrypoint.strategy; + +import io.papermc.paper.plugin.provider.PluginProvider; + +import java.util.List; + +/** + * Used by a {@link io.papermc.paper.plugin.storage.SimpleProviderStorage} to load plugin providers in a certain order. + *

+ * Returns providers loaded. + * @param

provider type + */ +public interface ProviderLoadingStrategy

{ + + List> loadProviders(List> providers); + + record ProviderPair

(PluginProvider

provider, P provided) { + + } +} diff --git a/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java new file mode 100644 index 0000000000000000000000000000000000000000..0720af0d48b39ca46e7d3aba08d7b359ed053461 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/entrypoint/strategy/TopographicGraphSorter.java @@ -0,0 +1,61 @@ +package io.papermc.paper.plugin.entrypoint.strategy; + +import com.google.common.graph.Graph; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TopographicGraphSorter { + + // Topographically sort dependencies + public static List sortGraph(Graph graph) throws PluginGraphCycleException { + List sorted = new ArrayList<>(); + Deque roots = new ArrayDeque<>(); + Map nonRoots = new HashMap<>(); + + for (N node : graph.nodes()) { + // Is a node being referred to by any other nodes? + int degree = graph.inDegree(node); + if (degree == 0) { + // Is a root + roots.add(node); + } else { + // Isn't a root, the number represents how many nodes connect to it. + nonRoots.put(node, degree); + } + } + + // Pick from nodes that aren't referred to anywhere else + while (!roots.isEmpty()) { + N next = roots.remove(); + + for (N successor : graph.successors(next)) { + // Traverse through, moving down a degree + int newInDegree = nonRoots.get(successor) - 1; + + if (newInDegree == 0) { + nonRoots.remove(successor); + roots.add(successor); + } else { + nonRoots.put(successor, newInDegree); + } + + } + sorted.add(next); + } + + if (!nonRoots.isEmpty()) { + throw new GraphCycleException(); + } + + return sorted; + } + + public static class GraphCycleException extends RuntimeException { + + } +} diff --git a/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..f38ecd7f65dc24e4a3f0bc675e3730287ac353f1 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/loader/PaperClasspathBuilder.java @@ -0,0 +1,64 @@ +package io.papermc.paper.plugin.loader; + +import io.papermc.paper.plugin.bootstrap.PluginProviderContext; +import io.papermc.paper.plugin.loader.library.ClassPathLibrary; +import io.papermc.paper.plugin.loader.library.PaperLibraryStore; +import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader; +import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.JarFile; +import java.util.logging.Logger; + +public class PaperClasspathBuilder implements PluginClasspathBuilder { + + private final List libraries = new ArrayList<>(); + + private final PluginProviderContext context; + + public PaperClasspathBuilder(PluginProviderContext context) { + this.context = context; + } + + @Override + public @NotNull PluginProviderContext getContext() { + return this.context; + } + + @Override + public @NotNull PluginClasspathBuilder addLibrary(@NotNull ClassPathLibrary classPathLibrary) { + this.libraries.add(classPathLibrary); + return this; + } + + public PaperPluginClassLoader buildClassLoader(Logger logger, Path source, JarFile jarFile, PaperPluginMeta configuration) { + PaperLibraryStore paperLibraryStore = new PaperLibraryStore(); + for (ClassPathLibrary library : this.libraries) { + library.register(paperLibraryStore); + } + + List paths = paperLibraryStore.getPaths(); + URL[] urls = new URL[paths.size()]; + for (int i = 0; i < paths.size(); i++) { + Path path = paperLibraryStore.getPaths().get(i); + try { + urls[i] = path.toUri().toURL(); + } catch (MalformedURLException e) { + throw new AssertionError(e); + } + } + + try { + return new PaperPluginClassLoader(logger, source, jarFile, configuration, this.getClass().getClassLoader(), new URLClassLoader(urls, getClass().getClassLoader())); + } catch (IOException exception) { + throw new RuntimeException(exception); + } + } +} diff --git a/src/main/java/io/papermc/paper/plugin/loader/library/PaperLibraryStore.java b/src/main/java/io/papermc/paper/plugin/loader/library/PaperLibraryStore.java new file mode 100644 index 0000000000000000000000000000000000000000..5fcce65009f715d46dd3013f1f92ec8393d66e15 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/loader/library/PaperLibraryStore.java @@ -0,0 +1,21 @@ +package io.papermc.paper.plugin.loader.library; + +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +public class PaperLibraryStore implements LibraryStore { + + private final List paths = new ArrayList<>(); + + @Override + public void addLibrary(@NotNull Path library) { + this.paths.add(library); + } + + public List getPaths() { + return this.paths; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/manager/DummyBukkitPluginLoader.java b/src/main/java/io/papermc/paper/plugin/manager/DummyBukkitPluginLoader.java new file mode 100644 index 0000000000000000000000000000000000000000..ea37ace14849ef4589a4f97287e6dcd64351370f --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/manager/DummyBukkitPluginLoader.java @@ -0,0 +1,57 @@ +package io.papermc.paper.plugin.manager; + +import org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.bukkit.event.Listener; +import org.bukkit.plugin.InvalidDescriptionException; +import org.bukkit.plugin.InvalidPluginException; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.PluginLoader; +import org.bukkit.plugin.RegisteredListener; +import org.bukkit.plugin.UnknownDependencyException; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * A purely internal type that implements the now deprecated {@link PluginLoader} after the implementation + * of papers new plugin system. + */ +@ApiStatus.Internal +public class DummyBukkitPluginLoader implements PluginLoader { + + @Override + public @NotNull Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, UnknownDependencyException { + throw new UnsupportedOperationException(); + } + + @Override + public @NotNull PluginDescriptionFile getPluginDescription(@NotNull File file) throws InvalidDescriptionException { + throw new UnsupportedOperationException(); + } + + @Override + public @NotNull Pattern[] getPluginFileFilters() { + throw new UnsupportedOperationException(); + } + + @Override + public @NotNull Map, Set> createRegisteredListeners(@NotNull Listener listener, @NotNull Plugin plugin) { + return PaperPluginManagerImpl.getInstance().paperEventManager.createRegisteredListeners(listener, plugin); + } + + @Override + public void enablePlugin(@NotNull Plugin plugin) { + Bukkit.getPluginManager().enablePlugin(plugin); + } + + @Override + public void disablePlugin(@NotNull Plugin plugin) { + Bukkit.getPluginManager().disablePlugin(plugin); + } +} diff --git a/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java b/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..b532e35505d5fffd4c525109f273012e2652f777 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/manager/MultiRuntimePluginProviderStorage.java @@ -0,0 +1,49 @@ +package io.papermc.paper.plugin.manager; + +import com.mojang.logging.LogUtils; +import io.papermc.paper.plugin.entrypoint.Entrypoint; +import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; +import io.papermc.paper.plugin.provider.PluginProvider; +import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent; +import io.papermc.paper.plugin.storage.ServerPluginProviderStorage; +import org.bukkit.plugin.java.JavaPlugin; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; + +public class MultiRuntimePluginProviderStorage extends ServerPluginProviderStorage { + + private static final Logger LOGGER = LogUtils.getLogger(); + private final List provided = new ArrayList<>(); + + @Override + public void register(PluginProvider provider) { + if (provider instanceof PaperPluginParent.PaperServerPluginProvider) { + LOGGER.warn("Skipping loading of paper plugin requested from SimplePluginManager."); + return; + } + super.register(provider); + /* + Register the provider into the server entrypoint, this allows it to show in /plugins correctly. Generally it might be better in the future to make a separate storage, + as putting it into the entrypoint handlers doesn't make much sense. + */ + LaunchEntryPointHandler.INSTANCE.register(Entrypoint.PLUGIN, provider); + } + + @Override + public void processProvided(PluginProvider provider, JavaPlugin provided) { + super.processProvided(provider, provided); + this.provided.add(provided); + } + + @Override + public boolean exitOnCycleDependencies() { + return false; + } + + public List getLoaded() { + return this.provided; + } + +} diff --git a/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java b/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java new file mode 100644 index 0000000000000000000000000000000000000000..6f6aaab295018017565ba27d6958a1f5c7b69bc8 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/manager/NormalPaperPermissionManager.java @@ -0,0 +1,43 @@ +package io.papermc.paper.plugin.manager; + +import org.bukkit.permissions.Permissible; +import org.bukkit.permissions.Permission; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +class NormalPaperPermissionManager extends PaperPermissionManager { + + private final Map permissions = new HashMap<>(); + private final Map> defaultPerms = new LinkedHashMap<>(); + private final Map> permSubs = new HashMap<>(); + private final Map> defSubs = new HashMap<>(); + + public NormalPaperPermissionManager() { + this.defaultPerms().put(true, new LinkedHashSet<>()); + this.defaultPerms().put(false, new LinkedHashSet<>()); + } + + @Override + public Map permissions() { + return this.permissions; + } + + @Override + public Map> defaultPerms() { + return this.defaultPerms; + } + + @Override + public Map> permSubs() { + return this.permSubs; + } + + @Override + public Map> defSubs() { + return this.defSubs; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java new file mode 100644 index 0000000000000000000000000000000000000000..7ce9ebba8ce304d1f3f21d4f15ee5f3560d7700b --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java @@ -0,0 +1,194 @@ +package io.papermc.paper.plugin.manager; + +import co.aikar.timings.TimedEventExecutor; +import com.destroystokyo.paper.event.server.ServerExceptionEvent; +import com.destroystokyo.paper.exception.ServerEventException; +import com.google.common.collect.Sets; +import org.bukkit.Server; +import org.bukkit.Warning; +import org.bukkit.event.Event; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.HandlerList; +import org.bukkit.event.Listener; +import org.bukkit.plugin.AuthorNagException; +import org.bukkit.plugin.EventExecutor; +import org.bukkit.plugin.IllegalPluginAccessException; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.RegisteredListener; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; + +class PaperEventManager { + + private final Server server; + + public PaperEventManager(Server server) { + this.server = server; + } + + // SimplePluginManager + public void callEvent(@NotNull Event event) { + if (event.isAsynchronous() && this.server.isPrimaryThread()) { + throw new IllegalStateException(event.getEventName() + " may only be triggered asynchronously."); + } else if (!event.isAsynchronous() && !this.server.isPrimaryThread() && !this.server.isStopping()) { + throw new IllegalStateException(event.getEventName() + " may only be triggered synchronously."); + } + + HandlerList handlers = event.getHandlers(); + RegisteredListener[] listeners = handlers.getRegisteredListeners(); + + for (RegisteredListener registration : listeners) { + if (!registration.getPlugin().isEnabled()) { + continue; + } + + try { + registration.callEvent(event); + } catch (AuthorNagException ex) { + Plugin plugin = registration.getPlugin(); + + if (plugin.isNaggable()) { + plugin.setNaggable(false); + + this.server.getLogger().log(Level.SEVERE, String.format( + "Nag author(s): '%s' of '%s' about the following: %s", + plugin.getPluginMeta().getAuthors(), + plugin.getPluginMeta().getDisplayName(), + ex.getMessage() + )); + } + } catch (Throwable ex) { + String msg = "Could not pass event " + event.getEventName() + " to " + registration.getPlugin().getPluginMeta().getDisplayName(); + this.server.getLogger().log(Level.SEVERE, msg, ex); + if (!(event instanceof ServerExceptionEvent)) { // We don't want to cause an endless event loop + this.callEvent(new ServerExceptionEvent(new ServerEventException(msg, ex, registration.getPlugin(), registration.getListener(), event))); + } + } + } + } + + public void registerEvents(@NotNull Listener listener, @NotNull Plugin plugin) { + if (!plugin.isEnabled()) { + throw new IllegalPluginAccessException("Plugin attempted to register " + listener + " while not enabled"); + } + + for (Map.Entry, Set> entry : this.createRegisteredListeners(listener, plugin).entrySet()) { + this.getEventListeners(this.getRegistrationClass(entry.getKey())).registerAll(entry.getValue()); + } + + } + + public void registerEvent(@NotNull Class event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin) { + this.registerEvent(event, listener, priority, executor, plugin, false); + } + + public void registerEvent(@NotNull Class event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin, boolean ignoreCancelled) { + if (!plugin.isEnabled()) { + throw new IllegalPluginAccessException("Plugin attempted to register " + event + " while not enabled"); + } + + executor = new TimedEventExecutor(executor, plugin, null, event); + this.getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled)); + } + + @NotNull + private HandlerList getEventListeners(@NotNull Class type) { + try { + Method method = this.getRegistrationClass(type).getDeclaredMethod("getHandlerList"); + method.setAccessible(true); + return (HandlerList) method.invoke(null); + } catch (Exception e) { + throw new IllegalPluginAccessException(e.toString()); + } + } + + @NotNull + private Class getRegistrationClass(@NotNull Class clazz) { + try { + clazz.getDeclaredMethod("getHandlerList"); + return clazz; + } catch (NoSuchMethodException e) { + if (clazz.getSuperclass() != null + && !clazz.getSuperclass().equals(Event.class) + && Event.class.isAssignableFrom(clazz.getSuperclass())) { + return this.getRegistrationClass(clazz.getSuperclass().asSubclass(Event.class)); + } else { + throw new IllegalPluginAccessException("Unable to find handler list for event " + clazz.getName() + ". Static getHandlerList method required!"); + } + } + } + + // JavaPluginLoader + @NotNull + public Map, Set> createRegisteredListeners(@NotNull Listener listener, @NotNull final Plugin plugin) { + Map, Set> ret = new HashMap<>(); + + Set methods; + try { + Class listenerClazz = listener.getClass(); + methods = Sets.union( + Set.of(listenerClazz.getMethods()), + Set.of(listenerClazz.getDeclaredMethods()) + ); + } catch (NoClassDefFoundError e) { + plugin.getLogger().severe("Failed to register events for " + listener.getClass() + " because " + e.getMessage() + " does not exist."); + return ret; + } + + for (final Method method : methods) { + final EventHandler eh = method.getAnnotation(EventHandler.class); + if (eh == null) continue; + // Do not register bridge or synthetic methods to avoid event duplication + // Fixes SPIGOT-893 + if (method.isBridge() || method.isSynthetic()) { + continue; + } + final Class checkClass; + if (method.getParameterTypes().length != 1 || !Event.class.isAssignableFrom(checkClass = method.getParameterTypes()[0])) { + plugin.getLogger().severe(plugin.getPluginMeta().getDisplayName() + " attempted to register an invalid EventHandler method signature \"" + method.toGenericString() + "\" in " + listener.getClass()); + continue; + } + final Class eventClass = checkClass.asSubclass(Event.class); + method.setAccessible(true); + Set eventSet = ret.computeIfAbsent(eventClass, k -> new HashSet<>()); + + for (Class clazz = eventClass; Event.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) { + // This loop checks for extending deprecated events + if (clazz.getAnnotation(Deprecated.class) != null) { + Warning warning = clazz.getAnnotation(Warning.class); + Warning.WarningState warningState = this.server.getWarningState(); + if (!warningState.printFor(warning)) { + break; + } + plugin.getLogger().log( + Level.WARNING, + String.format( + "\"%s\" has registered a listener for %s on method \"%s\", but the event is Deprecated. \"%s\"; please notify the authors %s.", + plugin.getPluginMeta().getDisplayName(), + clazz.getName(), + method.toGenericString(), + (warning != null && warning.reason().length() != 0) ? warning.reason() : "Server performance will be affected", + Arrays.toString(plugin.getPluginMeta().getAuthors().toArray())), + warningState == Warning.WarningState.ON ? new AuthorNagException(null) : null); + break; + } + } + + EventExecutor executor = new TimedEventExecutor(EventExecutor.create(method, eventClass), plugin, method, eventClass); + eventSet.add(new RegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled())); + } + return ret; + } + + public void clearEvents() { + HandlerList.unregisterAll(); + } +} diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java new file mode 100644 index 0000000000000000000000000000000000000000..92a69677f21b2c1c035119d8e5a6af63fa19b801 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPermissionManager.java @@ -0,0 +1,201 @@ +package io.papermc.paper.plugin.manager; + +import com.google.common.collect.ImmutableSet; +import io.papermc.paper.plugin.PermissionManager; +import org.bukkit.permissions.Permissible; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * See + * {@link StupidSPMPermissionManagerWrapper} + */ +abstract class PaperPermissionManager implements PermissionManager { + + public abstract Map permissions(); + + public abstract Map> defaultPerms(); + + public abstract Map> permSubs(); + + public abstract Map> defSubs(); + + @Override + @Nullable + public Permission getPermission(@NotNull String name) { + return this.permissions().get(name.toLowerCase(java.util.Locale.ENGLISH)); + } + + @Override + public void addPermission(@NotNull Permission perm) { + this.addPermission(perm, true); + } + + @Override + public void addPermissions(@NotNull List permissions) { + for (Permission permission : permissions) { + this.addPermission(permission, false); + } + this.dirtyPermissibles(); + } + + // Allow suppressing permission default calculations + private void addPermission(@NotNull Permission perm, boolean dirty) { + String name = perm.getName().toLowerCase(java.util.Locale.ENGLISH); + + if (this.permissions().containsKey(name)) { + throw new IllegalArgumentException("The permission " + name + " is already defined!"); + } + + this.permissions().put(name, perm); + this.calculatePermissionDefault(perm, dirty); + } + + @Override + @NotNull + public Set getDefaultPermissions(boolean op) { + return ImmutableSet.copyOf(this.defaultPerms().get(op)); + } + + + @Override + public void removePermission(@NotNull Permission perm) { + this.removePermission(perm.getName()); + } + + + @Override + public void removePermission(@NotNull String name) { + this.permissions().remove(name.toLowerCase(java.util.Locale.ENGLISH)); + } + + @Override + public void recalculatePermissionDefaults(@NotNull Permission perm) { + // we need a null check here because some plugins for some unknown reason pass null into this? + if (perm != null && this.permissions().containsKey(perm.getName().toLowerCase(Locale.ENGLISH))) { + this.defaultPerms().get(true).remove(perm); + this.defaultPerms().get(false).remove(perm); + + this.calculatePermissionDefault(perm, true); + } + } + + private void calculatePermissionDefault(@NotNull Permission perm, boolean dirty) { + if ((perm.getDefault() == PermissionDefault.OP) || (perm.getDefault() == PermissionDefault.TRUE)) { + this.defaultPerms().get(true).add(perm); + if (dirty) { + this.dirtyPermissibles(true); + } + } + if ((perm.getDefault() == PermissionDefault.NOT_OP) || (perm.getDefault() == PermissionDefault.TRUE)) { + this.defaultPerms().get(false).add(perm); + if (dirty) { + this.dirtyPermissibles(false); + } + } + } + + + @Override + public void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible) { + String name = permission.toLowerCase(java.util.Locale.ENGLISH); + Map map = this.permSubs().computeIfAbsent(name, k -> new WeakHashMap<>()); + + map.put(permissible, true); + } + + @Override + public void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible) { + String name = permission.toLowerCase(java.util.Locale.ENGLISH); + Map map = this.permSubs().get(name); + + if (map != null) { + map.remove(permissible); + + if (map.isEmpty()) { + this.permSubs().remove(name); + } + } + } + + @Override + @NotNull + public Set getPermissionSubscriptions(@NotNull String permission) { + String name = permission.toLowerCase(java.util.Locale.ENGLISH); + Map map = this.permSubs().get(name); + + if (map == null) { + return ImmutableSet.of(); + } else { + return ImmutableSet.copyOf(map.keySet()); + } + } + + @Override + public void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible) { + Map map = this.defSubs().computeIfAbsent(op, k -> new WeakHashMap<>()); + + map.put(permissible, true); + } + + @Override + public void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible) { + Map map = this.defSubs().get(op); + + if (map != null) { + map.remove(permissible); + + if (map.isEmpty()) { + this.defSubs().remove(op); + } + } + } + + @Override + @NotNull + public Set getDefaultPermSubscriptions(boolean op) { + Map map = this.defSubs().get(op); + + if (map == null) { + return ImmutableSet.of(); + } else { + return ImmutableSet.copyOf(map.keySet()); + } + } + + @Override + @NotNull + public Set getPermissions() { + return new HashSet<>(this.permissions().values()); + } + + @Override + public void clearPermissions() { + this.permissions().clear(); + this.defaultPerms().get(true).clear(); + this.defaultPerms().get(false).clear(); + } + + + void dirtyPermissibles(boolean op) { + Set permissibles = this.getDefaultPermSubscriptions(op); + + for (Permissible p : permissibles) { + p.recalculatePermissions(); + } + } + + void dirtyPermissibles() { + this.dirtyPermissibles(true); + this.dirtyPermissibles(false); + } +} diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java new file mode 100644 index 0000000000000000000000000000000000000000..c0e896343c22badd97c774c4ed1daa4e274f5d44 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginInstanceManager.java @@ -0,0 +1,304 @@ +package io.papermc.paper.plugin.manager; + +import com.google.common.base.Preconditions; +import com.google.common.graph.GraphBuilder; +import com.google.common.graph.MutableGraph; +import io.papermc.paper.plugin.configuration.PluginMeta; +import io.papermc.paper.plugin.entrypoint.Entrypoint; +import io.papermc.paper.plugin.entrypoint.dependency.DependencyUtil; +import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext; +import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException; +import io.papermc.paper.plugin.provider.classloader.ConfiguredPluginClassLoader; +import io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage; +import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; +import io.papermc.paper.plugin.provider.source.DirectoryProviderSource; +import io.papermc.paper.plugin.provider.source.FileProviderSource; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.command.Command; +import org.bukkit.command.CommandMap; +import org.bukkit.command.PluginCommandYamlParser; +import org.bukkit.craftbukkit.util.CraftMagicNumbers; +import org.bukkit.event.HandlerList; +import org.bukkit.event.server.PluginDisableEvent; +import org.bukkit.event.server.PluginEnableEvent; +import org.bukkit.plugin.InvalidDescriptionException; +import org.bukkit.plugin.InvalidPluginException; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.UnknownDependencyException; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.configurate.serialize.SerializationException; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; + +@SuppressWarnings("UnstableApiUsage") +class PaperPluginInstanceManager { + + private static final FileProviderSource FILE_PROVIDER_SOURCE = new FileProviderSource("File '%s'"::formatted); + private static final DirectoryProviderSource DIRECTORY_PROVIDER_SOURCE = new DirectoryProviderSource(); + + private final List plugins = new ArrayList<>(); + private final Map lookupNames = new HashMap<>(); + + private final PluginManager pluginManager; + private final CommandMap commandMap; + private final Server server; + + private final MutableGraph dependencyGraph = GraphBuilder.directed().build(); + private final DependencyContext context = new GraphDependencyContext(this.dependencyGraph); + + public PaperPluginInstanceManager(PluginManager pluginManager, CommandMap commandMap, Server server) { + this.commandMap = commandMap; + this.server = server; + this.pluginManager = pluginManager; + } + + public @Nullable Plugin getPlugin(@NotNull String name) { + return this.lookupNames.get(name.replace(' ', '_').toLowerCase(java.util.Locale.ENGLISH)); // Paper + } + + public @NotNull Plugin[] getPlugins() { + return this.plugins.toArray(new Plugin[0]); + } + + public boolean isPluginEnabled(@NotNull String name) { + Plugin plugin = this.getPlugin(name); + + return this.isPluginEnabled(plugin); + } + + public synchronized boolean isPluginEnabled(@Nullable Plugin plugin) { + if ((plugin != null) && (this.plugins.contains(plugin))) { + return plugin.isEnabled(); + } else { + return false; + } + } + + public void loadPlugin(Plugin provided) { + PluginMeta configuration = provided.getPluginMeta(); + + this.plugins.add(provided); + this.lookupNames.put(configuration.getName().toLowerCase(java.util.Locale.ENGLISH), provided); + for (String providedPlugin : configuration.getProvidedPlugins()) { + this.lookupNames.putIfAbsent(providedPlugin.toLowerCase(java.util.Locale.ENGLISH), provided); + } + + DependencyUtil.buildDependencyGraph(this.dependencyGraph, configuration); + } + + // InvalidDescriptionException is never used, because the old JavaPluginLoader would wrap the exception. + public @Nullable Plugin loadPlugin(@NotNull Path path) throws InvalidPluginException, UnknownDependencyException { + RuntimePluginEntrypointHandler runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new SingularRuntimePluginProviderStorage()); + + try { + FILE_PROVIDER_SOURCE.registerProviders(runtimePluginEntrypointHandler, path); + } catch (IllegalArgumentException exception) { + return null; // Return null when the plugin file is not valid / plugin type is unknown + } catch (PluginGraphCycleException exception) { + throw new InvalidPluginException("Cannot import plugin that causes cyclic dependencies!"); + } catch (SerializationException | + InvalidDescriptionException ex) { // The spigot implementation wraps it in an invalid plugin exception + throw new InvalidPluginException(ex); + } catch (Exception e) { + throw new InvalidPluginException(e); + } + + try { + runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN); + } catch (Throwable e) { + throw new InvalidPluginException(e); + } + + return runtimePluginEntrypointHandler.getPluginProviderStorage().getSingleLoaded() + .orElseThrow(() -> new InvalidPluginException("Plugin didn't load any plugin providers?")); + } + + // The behavior of this is that all errors are logged instead of being thrown + public @NotNull Plugin[] loadPlugins(@NotNull Path directory) { + Preconditions.checkArgument(Files.isDirectory(directory), "Directory must be a directory"); // Avoid creating a directory if it doesn't exist + + RuntimePluginEntrypointHandler runtimePluginEntrypointHandler = new RuntimePluginEntrypointHandler<>(new MultiRuntimePluginProviderStorage()); + try { + DIRECTORY_PROVIDER_SOURCE.registerProviders(runtimePluginEntrypointHandler, directory); + runtimePluginEntrypointHandler.enter(Entrypoint.PLUGIN); + } catch (Exception e) { + // This should never happen, any errors that occur in this provider should instead be logged. + this.server.getLogger().log(Level.SEVERE, "Unknown error occurred while loading plugins through PluginManager.", e); + } + + return runtimePluginEntrypointHandler.getPluginProviderStorage().getLoaded().toArray(new JavaPlugin[0]); + } + + // Plugins are disabled in order like this inorder to "rougly" prevent + // their dependencies unloading first. But, eh. + public void disablePlugins() { + Plugin[] plugins = this.getPlugins(); + for (int i = plugins.length - 1; i >= 0; i--) { + this.disablePlugin(plugins[i]); + } + } + + public void clearPlugins() { + synchronized (this) { + this.disablePlugins(); + this.plugins.clear(); + this.lookupNames.clear(); + } + } + + public synchronized void enablePlugin(@NotNull Plugin plugin) { + if (plugin.isEnabled()) { + return; + } + + if (plugin.getPluginMeta() instanceof PluginDescriptionFile) { + List bukkitCommands = PluginCommandYamlParser.parse(plugin); + + if (!bukkitCommands.isEmpty()) { + this.commandMap.registerAll(plugin.getPluginMeta().getName(), bukkitCommands); + } + } + + try { + String enableMsg = "Enabling " + plugin.getPluginMeta().getDisplayName(); + if (plugin.getPluginMeta() instanceof PluginDescriptionFile descriptionFile && CraftMagicNumbers.isLegacy(descriptionFile)) { + enableMsg += "*"; + } + plugin.getLogger().info(enableMsg); + + JavaPlugin jPlugin = (JavaPlugin) plugin; + + if (jPlugin.getClass().getClassLoader() instanceof ConfiguredPluginClassLoader classLoader) { // Paper + if (PaperClassLoaderStorage.instance().registerUnsafePlugin(classLoader)) { + this.server.getLogger().log(Level.WARNING, "Enabled plugin with unregistered ConfiguredPluginClassLoader " + plugin.getPluginMeta().getDisplayName()); + } + } // Paper + + try { + jPlugin.setEnabled(true); + } catch (Throwable ex) { + this.server.getLogger().log(Level.SEVERE, "Error occurred while enabling " + plugin.getPluginMeta().getDisplayName() + " (Is it up to date?)", ex); + // Paper start - Disable plugins that fail to load + this.server.getPluginManager().disablePlugin(jPlugin); + return; + // Paper end + } + + // Perhaps abort here, rather than continue going, but as it stands, + // an abort is not possible the way it's currently written + this.server.getPluginManager().callEvent(new PluginEnableEvent(plugin)); + } catch (Throwable ex) { + this.handlePluginException("Error occurred (in the plugin loader) while enabling " + + plugin.getPluginMeta().getDisplayName() + " (Is it up to date?)", ex, plugin); + } + + HandlerList.bakeAll(); + } + + public synchronized void disablePlugin(@NotNull Plugin plugin) { + if (!(plugin instanceof JavaPlugin javaPlugin)) { + throw new IllegalArgumentException("Only expects java plugins."); + } + if (!plugin.isEnabled()) { + return; + } + + String pluginName = plugin.getPluginMeta().getDisplayName(); + + try { + plugin.getLogger().info("Disabling %s".formatted(pluginName)); + + this.server.getPluginManager().callEvent(new PluginDisableEvent(plugin)); + + javaPlugin.setEnabled(false); + + ClassLoader classLoader = plugin.getClass().getClassLoader(); + if (classLoader instanceof ConfiguredPluginClassLoader configuredPluginClassLoader) { + try { + configuredPluginClassLoader.close(); + } catch (IOException ex) { + this.server.getLogger().log(Level.WARNING, "Error closing the classloader for '" + pluginName + "'", ex); // Paper - log exception + } + // Remove from the classloader pool inorder to prevent plugins from trying + // to access classes + PaperClassLoaderStorage.instance().unregisterClassloader(configuredPluginClassLoader); + } + + } catch (Throwable ex) { + this.handlePluginException("Error occurred (in the plugin loader) while disabling " + + pluginName + " (Is it up to date?)", ex, plugin); // Paper + } + + try { + this.server.getScheduler().cancelTasks(plugin); + } catch (Throwable ex) { + this.handlePluginException("Error occurred (in the plugin loader) while cancelling tasks for " + + pluginName + " (Is it up to date?)", ex, plugin); // Paper + } + + try { + this.server.getServicesManager().unregisterAll(plugin); + } catch (Throwable ex) { + this.handlePluginException("Error occurred (in the plugin loader) while unregistering services for " + + pluginName + " (Is it up to date?)", ex, plugin); // Paper + } + + try { + HandlerList.unregisterAll(plugin); + } catch (Throwable ex) { + this.handlePluginException("Error occurred (in the plugin loader) while unregistering events for " + + pluginName + " (Is it up to date?)", ex, plugin); // Paper + } + + try { + this.server.getMessenger().unregisterIncomingPluginChannel(plugin); + this.server.getMessenger().unregisterOutgoingPluginChannel(plugin); + } catch (Throwable ex) { + this.handlePluginException("Error occurred (in the plugin loader) while unregistering plugin channels for " + + pluginName + " (Is it up to date?)", ex, plugin); // Paper + } + + try { + for (World world : this.server.getWorlds()) { + world.removePluginChunkTickets(plugin); + } + } catch (Throwable ex) { + this.handlePluginException("Error occurred (in the plugin loader) while removing chunk tickets for " + pluginName + " (Is it up to date?)", ex, plugin); // Paper + } + + } + + // TODO: Implement event part in future patch (paper patch move up, this patch is lower) + private void handlePluginException(String msg, Throwable ex, Plugin plugin) { + Bukkit.getServer().getLogger().log(Level.SEVERE, msg, ex); + this.pluginManager.callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerPluginEnableDisableException(msg, ex, plugin))); + } + + public boolean isTransitiveDepend(@NotNull PluginMeta plugin, @NotNull PluginMeta depend) { + return this.context.isTransitiveDependency(plugin, depend); + } + + public boolean hasDependency(String pluginIdentifier) { + return this.getPlugin(pluginIdentifier) != null; + } + + // Debug only + @ApiStatus.Internal + public MutableGraph getDependencyGraph() { + return this.dependencyGraph; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..dab211c458311869c61779305580a1c7da830f71 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java @@ -0,0 +1,241 @@ +package io.papermc.paper.plugin.manager; + +import com.google.common.graph.MutableGraph; +import io.papermc.paper.plugin.PermissionManager; +import io.papermc.paper.plugin.configuration.PluginMeta; +import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.command.CommandMap; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.event.Event; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.permissions.Permissible; +import org.bukkit.permissions.Permission; +import org.bukkit.plugin.EventExecutor; +import org.bukkit.plugin.InvalidDescriptionException; +import org.bukkit.plugin.InvalidPluginException; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginLoader; +import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.SimplePluginManager; +import org.bukkit.plugin.UnknownDependencyException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.List; +import java.util.Set; + +public class PaperPluginManagerImpl implements PluginManager, DependencyContext { + + final PaperPluginInstanceManager instanceManager; + final PaperEventManager paperEventManager; + PermissionManager permissionManager; + + public PaperPluginManagerImpl(Server server, CommandMap commandMap, @Nullable SimplePluginManager permissionManager) { + this.instanceManager = new PaperPluginInstanceManager(this, commandMap, server); + this.paperEventManager = new PaperEventManager(server); + + if (permissionManager == null) { + this.permissionManager = new NormalPaperPermissionManager(); + } else { + this.permissionManager = new StupidSPMPermissionManagerWrapper(permissionManager); // TODO: See comment when SimplePermissionManager is removed + } + } + + // REMOVE THIS WHEN SimplePluginManager is removed. + // Just cast and use Bukkit.getServer().getPluginManager() + public static PaperPluginManagerImpl getInstance() { + return ((CraftServer) (Bukkit.getServer())).paperPluginManager; + } + + // Plugin Manipulation + + @Override + public @Nullable Plugin getPlugin(@NotNull String name) { + return this.instanceManager.getPlugin(name); + } + + @Override + public @NotNull Plugin[] getPlugins() { + return this.instanceManager.getPlugins(); + } + + @Override + public boolean isPluginEnabled(@NotNull String name) { + return this.instanceManager.isPluginEnabled(name); + } + + @Override + public boolean isPluginEnabled(@Nullable Plugin plugin) { + return this.instanceManager.isPluginEnabled(plugin); + } + + public void loadPlugin(Plugin plugin) { + this.instanceManager.loadPlugin(plugin); + } + + @Override + public @Nullable Plugin loadPlugin(@NotNull File file) throws InvalidPluginException, InvalidDescriptionException, UnknownDependencyException { + return this.instanceManager.loadPlugin(file.toPath()); + } + + @Override + public @NotNull Plugin[] loadPlugins(@NotNull File directory) { + return this.instanceManager.loadPlugins(directory.toPath()); + } + + @Override + public void disablePlugins() { + this.instanceManager.disablePlugins(); + } + + @Override + public synchronized void clearPlugins() { + this.instanceManager.clearPlugins(); + this.permissionManager.clearPermissions(); + this.paperEventManager.clearEvents(); + } + + @Override + public void enablePlugin(@NotNull Plugin plugin) { + this.instanceManager.enablePlugin(plugin); + } + + @Override + public void disablePlugin(@NotNull Plugin plugin) { + this.instanceManager.disablePlugin(plugin); + } + + @Override + public boolean isTransitiveDependency(PluginMeta pluginMeta, PluginMeta dependencyConfig) { + return this.instanceManager.isTransitiveDepend(pluginMeta, dependencyConfig); + } + + @Override + public boolean hasDependency(String pluginIdentifier) { + return this.instanceManager.hasDependency(pluginIdentifier); + } + + // Event manipulation + + @Override + public void callEvent(@NotNull Event event) throws IllegalStateException { + this.paperEventManager.callEvent(event); + } + + @Override + public void registerEvents(@NotNull Listener listener, @NotNull Plugin plugin) { + this.paperEventManager.registerEvents(listener, plugin); + } + + @Override + public void registerEvent(@NotNull Class event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin) { + this.paperEventManager.registerEvent(event, listener, priority, executor, plugin); + } + + @Override + public void registerEvent(@NotNull Class event, @NotNull Listener listener, @NotNull EventPriority priority, @NotNull EventExecutor executor, @NotNull Plugin plugin, boolean ignoreCancelled) { + this.paperEventManager.registerEvent(event, listener, priority, executor, plugin, ignoreCancelled); + } + + // Permission manipulation + + @Override + public @Nullable Permission getPermission(@NotNull String name) { + return this.permissionManager.getPermission(name); + } + + @Override + public void addPermission(@NotNull Permission perm) { + this.permissionManager.addPermission(perm); + } + + @Override + public void removePermission(@NotNull Permission perm) { + this.permissionManager.removePermission(perm); + } + + @Override + public void removePermission(@NotNull String name) { + this.permissionManager.removePermission(name); + } + + @Override + public @NotNull Set getDefaultPermissions(boolean op) { + return this.permissionManager.getDefaultPermissions(op); + } + + @Override + public void recalculatePermissionDefaults(@NotNull Permission perm) { + this.permissionManager.recalculatePermissionDefaults(perm); + } + + @Override + public void subscribeToPermission(@NotNull String permission, @NotNull Permissible permissible) { + this.permissionManager.subscribeToPermission(permission, permissible); + } + + @Override + public void unsubscribeFromPermission(@NotNull String permission, @NotNull Permissible permissible) { + this.permissionManager.unsubscribeFromPermission(permission, permissible); + } + + @Override + public @NotNull Set getPermissionSubscriptions(@NotNull String permission) { + return this.permissionManager.getPermissionSubscriptions(permission); + } + + @Override + public void subscribeToDefaultPerms(boolean op, @NotNull Permissible permissible) { + this.permissionManager.subscribeToDefaultPerms(op, permissible); + } + + @Override + public void unsubscribeFromDefaultPerms(boolean op, @NotNull Permissible permissible) { + this.permissionManager.unsubscribeFromDefaultPerms(op, permissible); + } + + @Override + public @NotNull Set getDefaultPermSubscriptions(boolean op) { + return this.permissionManager.getDefaultPermSubscriptions(op); + } + + @Override + public @NotNull Set getPermissions() { + return this.permissionManager.getPermissions(); + } + + @Override + public void addPermissions(@NotNull List perm) { + this.permissionManager.addPermissions(perm); + } + + @Override + public void clearPermissions() { + this.permissionManager.clearPermissions(); + } + + @Override + public void overridePermissionManager(@NotNull Plugin plugin, @Nullable PermissionManager permissionManager) { + this.permissionManager = permissionManager; + } + + // Etc + + @Override + public boolean useTimings() { + return co.aikar.timings.Timings.isTimingsEnabled(); + } + + @Override + public void registerInterface(@NotNull Class loader) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + public MutableGraph getInstanceManagerGraph() { + return instanceManager.getDependencyGraph(); + } +} diff --git a/src/main/java/io/papermc/paper/plugin/manager/RuntimePluginEntrypointHandler.java b/src/main/java/io/papermc/paper/plugin/manager/RuntimePluginEntrypointHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..5d50d1d312388e979c0e1cd53a6bf5977ca6e549 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/manager/RuntimePluginEntrypointHandler.java @@ -0,0 +1,47 @@ +package io.papermc.paper.plugin.manager; + +import com.destroystokyo.paper.util.SneakyThrow; +import io.papermc.paper.plugin.entrypoint.Entrypoint; +import io.papermc.paper.plugin.entrypoint.EntrypointHandler; +import io.papermc.paper.plugin.provider.PluginProvider; +import io.papermc.paper.plugin.storage.ProviderStorage; +import org.bukkit.plugin.InvalidPluginException; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +/** + * Used for loading plugins during runtime, only supporting providers that are plugins. + * This is only used for the plugin manager, as it only allows plugins to be + * registered to a provider storage. + */ +class RuntimePluginEntrypointHandler> implements EntrypointHandler { + + private final T providerStorage; + + RuntimePluginEntrypointHandler(T providerStorage) { + this.providerStorage = providerStorage; + } + + @Override + public void register(Entrypoint entrypoint, PluginProvider provider) { + if (!entrypoint.equals(Entrypoint.PLUGIN)) { + SneakyThrow.sneaky(new InvalidPluginException("Plugin cannot register entrypoints other than PLUGIN during runtime. Tried registering %s!".formatted(entrypoint))); + // We have to throw an invalid plugin exception for legacy reasons + } + + this.providerStorage.register((PluginProvider) provider); + } + + @Override + public void enter(Entrypoint entrypoint) { + if (entrypoint != Entrypoint.PLUGIN) { + throw new IllegalArgumentException("Only plugin entrypoint supported"); + } + this.providerStorage.enter(); + } + + @NotNull + public T getPluginProviderStorage() { + return this.providerStorage; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java b/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..194f1439e322cb6b3433a72f80a8a4c7624b20d5 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/manager/SingularRuntimePluginProviderStorage.java @@ -0,0 +1,80 @@ +package io.papermc.paper.plugin.manager; + +import com.destroystokyo.paper.util.SneakyThrow; +import io.papermc.paper.plugin.entrypoint.Entrypoint; +import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; +import io.papermc.paper.plugin.provider.PluginProvider; +import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent; +import io.papermc.paper.plugin.storage.ServerPluginProviderStorage; +import org.bukkit.plugin.InvalidPluginException; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.UnknownDependencyException; +import org.bukkit.plugin.java.JavaPlugin; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Used for registering a single plugin provider. + * This has special behavior in that some errors are thrown instead of logged. + */ +class SingularRuntimePluginProviderStorage extends ServerPluginProviderStorage { + + private PluginProvider lastProvider; + private JavaPlugin singleLoaded; + + @Override + public void register(PluginProvider provider) { + super.register(provider); + if (this.lastProvider != null) { + SneakyThrow.sneaky(new InvalidPluginException("Plugin registered two JavaPlugins")); + } + if (provider instanceof PaperPluginParent.PaperServerPluginProvider) { + throw new IllegalStateException("Cannot register paper plugins during runtime!"); + } + this.lastProvider = provider; + // Register the provider into the server entrypoint, this allows it to show in /plugins correctly. + // Generally it might be better in the future to make a separate storage, as putting it into the entrypoint handlers doesn't make much sense. + LaunchEntryPointHandler.INSTANCE.register(Entrypoint.PLUGIN, provider); + } + + @Override + public void enter() { + PluginProvider provider = this.lastProvider; + if (provider == null) { + return; + } + + // Manually validate dependencies, LEGACY BEHAVIOR. + // Normally it is logged, but manually adding one plugin will cause it to actually throw exceptions. + PluginDescriptionFile descriptionFile = (PluginDescriptionFile) provider.getMeta(); + List missingDependencies = new ArrayList<>(); + for (String dependency : descriptionFile.getDepend()) { + if (!PaperPluginManagerImpl.getInstance().isPluginEnabled(dependency)) { + missingDependencies.add(dependency); + } + } + if (!missingDependencies.isEmpty()) { + throw new UnknownDependencyException(missingDependencies, provider.getFileName().toString()); + } + + // Go through normal plugin loading logic + super.enter(); + } + + @Override + public void processProvided(PluginProvider provider, JavaPlugin provided) { + super.processProvided(provider, provided); + this.singleLoaded = provided; + } + + @Override + public boolean exitOnCycleDependencies() { + return false; + } + + public Optional getSingleLoaded() { + return Optional.ofNullable(this.singleLoaded); + } +} diff --git a/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java b/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..ea8cf22c35242eb9f3914b95df00e20504aef5c1 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/manager/StupidSPMPermissionManagerWrapper.java @@ -0,0 +1,42 @@ +package io.papermc.paper.plugin.manager; + +import org.bukkit.permissions.Permissible; +import org.bukkit.permissions.Permission; +import org.bukkit.plugin.SimplePluginManager; + +import java.util.Map; +import java.util.Set; + +/* +This is actually so cursed I hate it. +We need to wrap these in fields as people override the fields, so we need to access them lazily at all times. +// TODO: When SimplePluginManager is GONE remove this and cleanup the PaperPermissionManager to use actual fields. + */ +class StupidSPMPermissionManagerWrapper extends PaperPermissionManager { + + private final SimplePluginManager simplePluginManager; + + public StupidSPMPermissionManagerWrapper(SimplePluginManager simplePluginManager) { + this.simplePluginManager = simplePluginManager; + } + + @Override + public Map permissions() { + return this.simplePluginManager.permissions; + } + + @Override + public Map> defaultPerms() { + return this.simplePluginManager.defaultPerms; + } + + @Override + public Map> permSubs() { + return this.simplePluginManager.permSubs; + } + + @Override + public Map> defSubs() { + return this.simplePluginManager.defSubs; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/PluginProvider.java b/src/main/java/io/papermc/paper/plugin/provider/PluginProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..11b6cb377c9b04b63b6359918eef214ba3032d96 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/PluginProvider.java @@ -0,0 +1,47 @@ +package io.papermc.paper.plugin.provider; + +import io.papermc.paper.plugin.configuration.PluginMeta; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; +import java.util.jar.JarFile; +import java.util.logging.Logger; + +/** + * PluginProviders are created by a {@link io.papermc.paper.plugin.provider.source.ProviderSource}, + * which is loaded into an {@link io.papermc.paper.plugin.entrypoint.EntrypointHandler}. + *

+ * A PluginProvider is responsible for providing part of a plugin, whether it's a Bootstrapper or Server Plugin. + * The point of this class is to be able to create the actual instance later, as at the time this is created the server + * may be missing some key parts. For example, the Bukkit singleton will not be initialized yet, therefor we need to + * have a PluginServerProvider load the server plugin later. + *

+ * Plugin providers are currently not exposed in any way of the api. It is preferred that this stays this way, + * as providers are only needed for initialization. + * + * @param provider type + */ +@ApiStatus.Internal +public interface PluginProvider { + + @NotNull + Path getSource(); + + default Path getFileName() { + return this.getSource().getFileName(); + } + + default Path getParentSource() { + return this.getSource().getParent(); + } + + JarFile file(); + + T createInstance(); + + PluginMeta getMeta(); + + Logger getLogger(); + +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/ProviderStatus.java b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..6154e864b0ff01cb70acaaeee5ca8c9f4a90a90e --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatus.java @@ -0,0 +1,13 @@ +package io.papermc.paper.plugin.provider; + +import org.jetbrains.annotations.ApiStatus; + +/** + * This is used for the /plugins command, where it will look in the {@link io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler} and + * use the provider statuses to determine the color. + */ +@ApiStatus.Internal +public enum ProviderStatus { + INITIALIZED, + ERRORED, +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/ProviderStatusHolder.java b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatusHolder.java new file mode 100644 index 0000000000000000000000000000000000000000..17284d0f61c459dff765c0adae4ad2c641e054c1 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/ProviderStatusHolder.java @@ -0,0 +1,11 @@ +package io.papermc.paper.plugin.provider; + +/** + * This is used to mark that a plugin provider is able to hold a status for the /plugins command. + */ +public interface ProviderStatusHolder { + + ProviderStatus getLastProvidedStatus(); + + void setStatus(ProviderStatus status); +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/FlattenedResolver.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/FlattenedResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..6ba3bcc468c0a60c76d6d0f0243bda661c737f2f --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/FlattenedResolver.java @@ -0,0 +1,29 @@ +package io.papermc.paper.plugin.provider.configuration; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.objectmapping.meta.NodeResolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.AnnotatedElement; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface FlattenedResolver { + + final class Factory implements NodeResolver.Factory { + + @Override + public @Nullable NodeResolver make(String name, AnnotatedElement element) { + if (element.isAnnotationPresent(FlattenedResolver.class)) { + return (node) -> node; + } else { + return null; + } + } + } + + +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java new file mode 100644 index 0000000000000000000000000000000000000000..622a6c5fdfcb6d8cc12054f3de81c73c9af2389f --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java @@ -0,0 +1,213 @@ +package io.papermc.paper.plugin.provider.configuration; + +import com.google.common.collect.ImmutableList; +import io.leangen.geantyref.TypeToken; +import io.papermc.paper.configuration.constraint.Constraint; +import io.papermc.paper.configuration.serializer.ComponentSerializer; +import io.papermc.paper.configuration.serializer.EnumValueSerializer; +import io.papermc.paper.configuration.serializer.collections.MapSerializer; +import io.papermc.paper.plugin.configuration.PluginMeta; +import io.papermc.paper.plugin.provider.configuration.serializer.ImmutableListSerializer; +import io.papermc.paper.plugin.provider.configuration.serializer.PermissionConfigurationSerializer; +import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints; +import io.papermc.paper.plugin.provider.configuration.type.DependencyConfiguration; +import io.papermc.paper.plugin.provider.configuration.type.PermissionConfiguration; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.bukkit.plugin.PluginLoadOrder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.configurate.CommentedConfigurationNode; +import org.spongepowered.configurate.ConfigurateException; +import org.spongepowered.configurate.loader.HeaderMode; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.ObjectMapper; +import org.spongepowered.configurate.objectmapping.meta.Required; +import org.spongepowered.configurate.yaml.NodeStyle; +import org.spongepowered.configurate.yaml.YamlConfigurationLoader; + +import java.io.BufferedReader; +import java.util.List; + +@SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic"}) +@ConfigSerializable +public class PaperPluginMeta implements PluginMeta { + + @PluginConfigConstraints.PluginName + @Required + private String name; + @Required + @PluginConfigConstraints.PluginNameSpace + private String main; + @PluginConfigConstraints.PluginNameSpace + private String bootstrapper; + @PluginConfigConstraints.PluginNameSpace + private String loader; + private List dependencies = List.of(); + private List loadBefore = List.of(); + private List provides = List.of(); + private boolean hasOpenClassloader = false; + @Required + private String version; + private String description; + private List authors = List.of(); + private List contributors = List.of(); + private String website; + private String prefix; + private PluginLoadOrder load = PluginLoadOrder.POSTWORLD; + @FlattenedResolver + private PermissionConfiguration permissionConfiguration = new PermissionConfiguration(PermissionDefault.OP, List.of()); + @Required + @PluginConfigConstraints.PluginVersion + private String apiVersion; + + private transient String displayName; + + public PaperPluginMeta() { + } + + public static PaperPluginMeta create(BufferedReader reader) throws ConfigurateException { + YamlConfigurationLoader loader = YamlConfigurationLoader.builder() + .indent(2) + .nodeStyle(NodeStyle.BLOCK) + .headerMode(HeaderMode.NONE) + .source(() -> reader) + .defaultOptions((options) -> { + + return options.serializers((serializers) -> { + serializers + .register(new EnumValueSerializer()) + .register(MapSerializer.TYPE, new MapSerializer(false)) + .register(new TypeToken<>() { + }, new ImmutableListSerializer()) + .register(PermissionConfiguration.class, PermissionConfigurationSerializer.SERIALIZER) + .register(new ComponentSerializer()) + .registerAnnotatedObjects( + ObjectMapper.factoryBuilder() + .addConstraint(Constraint.class, new Constraint.Factory()) + .addConstraint(PluginConfigConstraints.PluginName.class, String.class, new PluginConfigConstraints.PluginName.Factory()) + .addConstraint(PluginConfigConstraints.PluginVersion.class, String.class, new PluginConfigConstraints.PluginVersion.Factory()) + .addConstraint(PluginConfigConstraints.PluginNameSpace.class, String.class, new PluginConfigConstraints.PluginNameSpace.Factory()) + .addNodeResolver(new FlattenedResolver.Factory()) + .build() + ); + + }); + }) + .build(); + CommentedConfigurationNode node = loader.load(); + PaperPluginMeta pluginConfiguration = node.require(PaperPluginMeta.class); + + if (!node.node("author").virtual()) { + pluginConfiguration.authors = ImmutableList.builder() + .addAll(pluginConfiguration.authors) + .add(node.node("author").getString()) + .build(); + } + + pluginConfiguration.displayName = pluginConfiguration.name.replace('_', ' '); + + return pluginConfiguration; + } + + @Override + public @NotNull String getName() { + return this.name; + } + + @Override + public @NotNull String getMainClass() { + return this.main; + } + + @Override + public @NotNull String getVersion() { + return this.version; + } + + @Override + public @NotNull String getDisplayName() { + return this.displayName; + } + + @Override + public @Nullable String getLoggerPrefix() { + return this.prefix; + } + + @Override + public @NotNull List getPluginDependencies() { + return this.dependencies.stream().filter((dependency) -> dependency.required() && !dependency.bootstrap()).map(DependencyConfiguration::name).toList(); + } + + @Override + public @NotNull List getPluginSoftDependencies() { + return this.dependencies.stream().filter((dependency) -> !dependency.required() && !dependency.bootstrap()).map(DependencyConfiguration::name).toList(); + } + + @Override + public @NotNull List getLoadBeforePlugins() { + return this.loadBefore; + } + + @Override + public @NotNull PluginLoadOrder getLoadOrder() { + return this.load; + } + + @Override + public @NotNull String getDescription() { + return this.description; + } + + @Override + public @NotNull List getAuthors() { + return this.authors; + } + + @Override + public @NotNull List getContributors() { + return this.contributors; + } + + @Override + public String getWebsite() { + return this.website; + } + + @Override + public @NotNull List getPermissions() { + return this.permissionConfiguration.permissions(); + } + + @Override + public @NotNull PermissionDefault getPermissionDefault() { + return this.permissionConfiguration.defaultPerm(); + } + + @Override + public @NotNull String getAPIVersion() { + return this.apiVersion; + } + + @Override + public @NotNull List getProvidedPlugins() { + return this.provides; + } + + public String getBootstrapper() { + return this.bootstrapper; + } + + public String getLoader() { + return this.loader; + } + + public boolean hasOpenClassloader() { + return this.hasOpenClassloader; + } + + public List getDependencies() { + return dependencies; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableCollectionSerializer.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableCollectionSerializer.java new file mode 100644 index 0000000000000000000000000000000000000000..f0cdb1bab30faaa438aa3e6de6125ade3fae98c2 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableCollectionSerializer.java @@ -0,0 +1,90 @@ +package io.papermc.paper.plugin.provider.configuration.serializer; + +import com.google.common.collect.ImmutableCollection; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.ConfigurationOptions; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.serialize.TypeSerializer; +import org.spongepowered.configurate.util.CheckedConsumer; + +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +@SuppressWarnings("unchecked") +public abstract class ImmutableCollectionSerializer, T extends Collection> implements TypeSerializer { + + protected ImmutableCollectionSerializer() { + } + + @Override + public final T deserialize(final Type type, final ConfigurationNode node) throws SerializationException { + final Type entryType = this.elementType(type); + final @Nullable TypeSerializer entrySerial = node.options().serializers().get(entryType); + if (entrySerial == null) { + throw new SerializationException(node, entryType, "No applicable type serializer for type"); + } + + if (node.isList()) { + final List values = node.childrenList(); + final B builder = this.createNew(values.size()); + for (ConfigurationNode value : values) { + try { + this.deserializeSingle(builder, entrySerial.deserialize(entryType, value)); + } catch (final SerializationException ex) { + ex.initPath(value::path); + throw ex; + } + } + return (T) builder.build(); + } else { + final @Nullable Object unwrappedVal = node.raw(); + if (unwrappedVal != null) { + final B builder = this.createNew(1); + this.deserializeSingle(builder, entrySerial.deserialize(entryType, node)); + return (T) builder.build(); + } + } + return this.emptyValue(type, null); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public final void serialize(final Type type, final @Nullable T obj, final ConfigurationNode node) throws SerializationException { + final Type entryType = this.elementType(type); + final @Nullable TypeSerializer entrySerial = node.options().serializers().get(entryType); + if (entrySerial == null) { + throw new SerializationException(node, entryType, "No applicable type serializer for type"); + } + + node.raw(Collections.emptyList()); + if (obj != null) { + this.forEachElement(obj, el -> { + final ConfigurationNode child = node.appendListNode(); + try { + entrySerial.serialize(entryType, el, child); + } catch (final SerializationException ex) { + ex.initPath(child::path); + throw ex; + } + }); + } + } + + @SuppressWarnings({"unchecked"}) + @Override + public @Nullable T emptyValue(final Type specificType, final ConfigurationOptions options) { + return (T) this.createNew(0).build(); + } + + protected abstract Type elementType(Type containerType) throws SerializationException; + + protected abstract B createNew(int size); + + protected abstract void forEachElement(T collection, CheckedConsumer action) throws SerializationException; + + protected abstract void deserializeSingle(B builder, @Nullable Object deserialized) throws SerializationException; + +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableListSerializer.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableListSerializer.java new file mode 100644 index 0000000000000000000000000000000000000000..7757d7df70e39a6fe4d92d02b6f905a22f80dbf3 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/ImmutableListSerializer.java @@ -0,0 +1,43 @@ +package io.papermc.paper.plugin.provider.configuration.serializer; + +import com.google.common.collect.ImmutableList; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.util.CheckedConsumer; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; + +public class ImmutableListSerializer extends ImmutableCollectionSerializer, List> { + + @Override + protected Type elementType(Type containerType) throws SerializationException { + if (!(containerType instanceof ParameterizedType)) { + throw new SerializationException(containerType, "Raw types are not supported for collections"); + } + return ((ParameterizedType) containerType).getActualTypeArguments()[0]; + } + + @Override + protected ImmutableList.Builder createNew(int size) { + return ImmutableList.builderWithExpectedSize(size); + } + + @Override + protected void forEachElement(List collection, CheckedConsumer action) throws SerializationException { + for (Object obj : collection) { + action.accept(obj); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + protected void deserializeSingle(ImmutableList.Builder builder, @Nullable Object deserialized) throws SerializationException { + if (deserialized == null) { + return; + } + + ((ImmutableList.Builder) builder).add(deserialized); + } +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/PermissionConfigurationSerializer.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/PermissionConfigurationSerializer.java new file mode 100644 index 0000000000000000000000000000000000000000..d1088e4b7fa5f8e689f23b150b83645ce1ae5a0e --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/PermissionConfigurationSerializer.java @@ -0,0 +1,56 @@ +package io.papermc.paper.plugin.provider.configuration.serializer; + +import io.papermc.paper.plugin.provider.configuration.type.PermissionConfiguration; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.serialize.SerializationException; +import org.spongepowered.configurate.serialize.TypeSerializer; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class PermissionConfigurationSerializer { + + public static final Serializer SERIALIZER = new Serializer(); + + private static final class Serializer implements TypeSerializer { + private Serializer() { + super(); + } + + @Override + public PermissionConfiguration deserialize(Type type, ConfigurationNode node) throws SerializationException { + Map map = (Map) node.node("permissions").raw(); + + PermissionDefault permissionDefault; + ConfigurationNode permNode = node.node("defaultPerm"); + if (permNode.virtual()) { + permissionDefault = PermissionDefault.OP; + } else { + permissionDefault = PermissionDefault.getByName(permNode.getString()); + } + + List result = new ArrayList<>(); + if (map != null) { + for (Map.Entry entry : map.entrySet()) { + try { + result.add(Permission.loadPermission(entry.getKey().toString(), (Map) entry.getValue(), permissionDefault, result)); + } catch (Throwable ex) { + throw new SerializationException(null, "Error loading permission %s".formatted(entry.getKey()), ex); + } + } + } + + return new PermissionConfiguration(permissionDefault, List.copyOf(result)); + } + + @Override + public void serialize(Type type, @org.checkerframework.checker.nullness.qual.Nullable PermissionConfiguration obj, ConfigurationNode node) throws SerializationException { + + } + + } +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/constraints/PluginConfigConstraints.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/constraints/PluginConfigConstraints.java new file mode 100644 index 0000000000000000000000000000000000000000..a0109a388188b0808900405d334a40318ab16ec1 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/serializer/constraints/PluginConfigConstraints.java @@ -0,0 +1,86 @@ +package io.papermc.paper.plugin.provider.configuration.serializer.constraints; + +import io.papermc.paper.plugin.util.NamespaceChecker; +import org.spongepowered.configurate.objectmapping.meta.Constraint; +import org.spongepowered.configurate.serialize.SerializationException; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Type; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Pattern; + +public final class PluginConfigConstraints { + + public static final Set RESERVED_KEYS = Set.of("bukkit", "minecraft", "mojang", "spigot", "paper"); + public static final Set VALID_PAPER_VERSIONS = Set.of("1.19"); + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface PluginName { + + final class Factory implements Constraint.Factory { + + private static final Pattern VALID_NAME = Pattern.compile("^[A-Za-z\\d _.-]+$"); + + @Override + public Constraint make(PluginName data, Type type) { + return value -> { + if (value != null) { + if (RESERVED_KEYS.contains(value.toLowerCase(Locale.ROOT))) { + throw new SerializationException("Restricted name, cannot use '%s' as a plugin name.".formatted(data)); + } else if (value.indexOf(' ') != -1) { + // For legacy reasons, the space condition has a separate exception message. + throw new SerializationException("Restricted name, cannot use 0x20 (space character) in a plugin name."); + } + + if (!VALID_NAME.matcher(value).matches()) { + throw new SerializationException("name '" + value + "' contains invalid characters."); + } + } + }; + } + } + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface PluginNameSpace { + + final class Factory implements Constraint.Factory { + + @Override + public Constraint make(PluginNameSpace data, Type type) { + return value -> { + if (value != null && !NamespaceChecker.isValidNameSpace(value)) { + throw new SerializationException("provided class '%s' is in an invalid namespace.".formatted(value)); + } + }; + } + } + } + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface PluginVersion { + + final class Factory implements Constraint.Factory { + + @Override + public Constraint make(PluginVersion data, Type type) { + return value -> { + if (value != null && !VALID_PAPER_VERSIONS.contains(value)) { + throw new SerializationException("Provided plugin's version (%s) is not supported on this version.".formatted(value)); + } + }; + } + } + } +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/type/DependencyConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/DependencyConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..071bff3f988a4391be424bdf7e98a6c35e6cac67 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/DependencyConfiguration.java @@ -0,0 +1,12 @@ +package io.papermc.paper.plugin.provider.configuration.type; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Required; + +@ConfigSerializable +public record DependencyConfiguration( + @Required String name, + boolean required, + boolean bootstrap +) { +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PermissionConfiguration.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PermissionConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..a180612a1ec395202dbae1ca5b97ec01382097e4 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/type/PermissionConfiguration.java @@ -0,0 +1,14 @@ +package io.papermc.paper.plugin.provider.configuration.type; + +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +import java.util.List; + +// Record components used for deserialization!!!! +@ConfigSerializable +public record PermissionConfiguration( + PermissionDefault defaultPerm, + List permissions) { +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java new file mode 100644 index 0000000000000000000000000000000000000000..1822e076601db51c8a7954036853bee1fb8e3704 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/source/DirectoryProviderSource.java @@ -0,0 +1,40 @@ +package io.papermc.paper.plugin.provider.source; + +import com.mojang.logging.LogUtils; +import io.papermc.paper.plugin.entrypoint.EntrypointHandler; +import org.slf4j.Logger; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.logging.Level; + +/** + * Loads all plugin providers in the given directory. + */ +public class DirectoryProviderSource extends FileProviderSource { + + public static final DirectoryProviderSource INSTANCE = new DirectoryProviderSource(); + private static final Logger LOGGER = LogUtils.getLogger(); + + public DirectoryProviderSource() { + super("Directory '%s'"::formatted); + } + + @Override + public void registerProviders(EntrypointHandler entrypointHandler, Path context) throws Exception { + // Sym link happy, create file if missing. + if (!Files.isDirectory(context)) { + Files.createDirectories(context); + } + + Files.walk(context, 1).filter(Files::isRegularFile).forEach((path) -> { + try { + super.registerProviders(entrypointHandler, path); + } catch (IllegalArgumentException ignored) { + // Ignore initial argument exceptions + } catch (Exception e) { + LOGGER.error("Error loading plugin: " + e.getMessage(), e); + } + }); + } +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java new file mode 100644 index 0000000000000000000000000000000000000000..e6a99f422038fad519215abf239135b11edc2bce --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java @@ -0,0 +1,156 @@ +package io.papermc.paper.plugin.provider.source; + +import io.papermc.paper.plugin.PluginInitializerManager; +import io.papermc.paper.plugin.entrypoint.EntrypointHandler; +import io.papermc.paper.plugin.provider.type.PluginFileType; +import org.bukkit.plugin.InvalidPluginException; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Set; +import java.util.function.Function; +import java.util.jar.JarFile; + +/** + * Loads a plugin provider at the given plugin jar file path. + */ +public class FileProviderSource implements ProviderSource { + + private final Function contextChecker; + + public FileProviderSource(Function contextChecker) { + this.contextChecker = contextChecker; + } + + @Override + public void registerProviders(EntrypointHandler entrypointHandler, Path context) throws Exception { + String source = this.contextChecker.apply(context); + + if (Files.notExists(context)) { + throw new IllegalArgumentException(source + " does not exist, cannot load a plugin from it!"); + } + + if (!Files.isRegularFile(context)) { + throw new IllegalArgumentException(source + " is not a file, cannot load a plugin from it!"); + } + + if (!context.getFileName().toString().endsWith(".jar")) { + throw new IllegalArgumentException(source + " is not a jar file, cannot load a plugin from it!"); + } + + try { + context = this.checkUpdate(context); + + JarFile file = new JarFile(context.toFile()); + PluginFileType type = PluginFileType.guessType(file); + if (type == null) { + throw new IllegalArgumentException(source + " is not a valid plugin file, cannot load a plugin from it!"); + } + + type.register(entrypointHandler, file, context); + } catch (Exception exception) { + throw new RuntimeException(source + " failed to load!", exception); + } + } + + /** + * Replaces a plugin with a plugin of the same plugin name in the update folder. + * + * @param file + */ + private Path checkUpdate(Path file) throws Exception { + PluginInitializerManager pluginSystem = PluginInitializerManager.instance(); + Path updateDirectory = pluginSystem.pluginUpdatePath(); + if (updateDirectory == null || !Files.isDirectory(updateDirectory)) { + return file; + } + + try { + String pluginName = this.getPluginName(file); + UpdateFileVisitor visitor = new UpdateFileVisitor(pluginName); + Files.walkFileTree(updateDirectory, Set.of(), 1, visitor); + if (visitor.getValidPlugin() != null) { + Path updateLocation = visitor.getValidPlugin(); + + try { + Files.copy(updateLocation, file, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException exception) { + throw new RuntimeException("Could not copy '" + updateLocation + "' to '" + file + "' in update plugin process", exception); + } + + // Idk what this is about, TODO + File newName = new File(file.toFile().getParentFile(), updateLocation.toFile().getName()); + file.toFile().renameTo(newName); + updateLocation.toFile().delete(); + return newName.toPath(); + } + } catch (Exception e) { + throw new InvalidPluginException(e); + } + return file; + } + + private String getPluginName(Path path) throws Exception { + JarFile file = new JarFile(path.toFile()); + PluginFileType type = PluginFileType.guessType(file); + if (type == null) { + throw new IllegalArgumentException(path + " is not a valid plugin file, cannot load a plugin from it!"); + } + + return type.getConfig(file).getName(); + } + + private class UpdateFileVisitor implements FileVisitor { + + private final String targetName; + @Nullable + private Path validPlugin; + + private UpdateFileVisitor(String targetName) { + this.targetName = targetName; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + try { + String updatePluginName = FileProviderSource.this.getPluginName(file); + if (this.targetName.equals(updatePluginName)) { + this.validPlugin = file; + return FileVisitResult.TERMINATE; + } + } catch (Exception e) { + // We failed to load this data for some reason, so, we'll skip over this + } + + + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + return FileVisitResult.CONTINUE; + } + + @Nullable + public Path getValidPlugin() { + return validPlugin; + } + } +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java new file mode 100644 index 0000000000000000000000000000000000000000..e62b476ddbce2bdc66061c116aa0228622f6fd16 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/source/PluginFlagProviderSource.java @@ -0,0 +1,29 @@ +package io.papermc.paper.plugin.provider.source; + +import com.mojang.logging.LogUtils; +import io.papermc.paper.plugin.entrypoint.EntrypointHandler; +import org.slf4j.Logger; + +import java.io.File; +import java.util.List; + +/** + * Registers providers at the provided files in the add-plugin argument. + */ +public class PluginFlagProviderSource implements ProviderSource> { + + public static final PluginFlagProviderSource INSTANCE = new PluginFlagProviderSource(); + private static final Logger LOGGER = LogUtils.getLogger(); + private final FileProviderSource providerSource = new FileProviderSource("File '%s' specified through 'add-plugin' argument"::formatted); + + @Override + public void registerProviders(EntrypointHandler entrypointHandler, List context) { + for (File file : context) { + try { + this.providerSource.registerProviders(entrypointHandler, file.toPath()); + } catch (Exception e) { + LOGGER.error("Error loading plugin: " + e.getMessage(), e); + } + } + } +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/ProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/ProviderSource.java new file mode 100644 index 0000000000000000000000000000000000000000..6d247819ee842eb054a74711a0e5805ac8f0498e --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/source/ProviderSource.java @@ -0,0 +1,14 @@ +package io.papermc.paper.plugin.provider.source; + +import io.papermc.paper.plugin.entrypoint.EntrypointHandler; + +/** + * A provider source is responsible for giving PluginTypes an EntrypointHandler for + * registering providers at. + * + * @param context + */ +public interface ProviderSource { + + void registerProviders(EntrypointHandler entrypointHandler, C context) throws Throwable; +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java b/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java new file mode 100644 index 0000000000000000000000000000000000000000..22c25dc6fdfd336f5074fa52c3a4e8128d433ccc --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java @@ -0,0 +1,77 @@ +package io.papermc.paper.plugin.provider.type; + +import io.papermc.paper.plugin.configuration.PluginMeta; +import io.papermc.paper.plugin.entrypoint.Entrypoint; +import io.papermc.paper.plugin.entrypoint.EntrypointHandler; +import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; +import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent; +import io.papermc.paper.plugin.provider.type.spigot.SpigotPluginProvider; +import org.bukkit.plugin.PluginDescriptionFile; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * This is where spigot/paper plugins are registered. + * This will get the jar and find a certain config file, create an object + * then registering it into a {@link EntrypointHandler} at a certain {@link Entrypoint}. + */ +public abstract class PluginFileType { + + public static final PluginFileType PAPER = new PluginFileType<>("paper-plugin.yml", PaperPluginParent.FACTORY) { + @Override + protected void register(EntrypointHandler entrypointHandler, PaperPluginParent parent) { + PaperPluginParent.PaperBootstrapProvider bootstrapPluginProvider = null; + if (parent.shouldCreateBootstrap()) { + bootstrapPluginProvider = parent.createBootstrapProvider(); + entrypointHandler.register(Entrypoint.BOOTSTRAPPER, bootstrapPluginProvider); + } + + entrypointHandler.register(Entrypoint.PLUGIN, parent.createPluginProvider(bootstrapPluginProvider)); + } + }; + public static final PluginFileType SPIGOT = new PluginFileType<>("plugin.yml", SpigotPluginProvider.FACTORY) { + @Override + protected void register(EntrypointHandler entrypointHandler, SpigotPluginProvider provider) { + entrypointHandler.register(Entrypoint.PLUGIN, provider); + } + }; + + private static final List> VALUES = List.of(PAPER, SPIGOT); + + private final String config; + private final PluginTypeFactory factory; + + PluginFileType(String config, PluginTypeFactory factory) { + this.config = config; + this.factory = factory; + } + + @Nullable + public static PluginFileType guessType(JarFile file) { + for (PluginFileType type : VALUES) { + JarEntry entry = file.getJarEntry(type.config); + if (entry != null) { + return type; + } + } + + return null; + } + + public T register(EntrypointHandler entrypointHandler, JarFile file, Path context) throws Exception { + C config = this.getConfig(file); + T provider = this.factory.build(file, config, context); + this.register(entrypointHandler, provider); + return provider; + } + + public C getConfig(JarFile file) throws Exception { + return this.factory.create(file, file.getJarEntry(this.config)); + } + + protected abstract void register(EntrypointHandler entrypointHandler, T provider); +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/PluginTypeFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/PluginTypeFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..32f230d66f6953520b59ccbf3079c5a6242ca92c --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/type/PluginTypeFactory.java @@ -0,0 +1,21 @@ +package io.papermc.paper.plugin.provider.type; + +import io.papermc.paper.plugin.configuration.PluginMeta; + +import java.nio.file.Path; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * A plugin type factory is responsible for building an object + * and config for a certain plugin type. + * + * @param plugin provider type (may not be a plugin provider) + * @param config type + */ +public interface PluginTypeFactory { + + T build(JarFile file, C configuration, Path source) throws Exception; + + C create(JarFile file, JarEntry config) throws Exception; +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java new file mode 100644 index 0000000000000000000000000000000000000000..46ee1b7f4b4c509932c68a4152e1d8445811d3b8 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java @@ -0,0 +1,226 @@ +package io.papermc.paper.plugin.provider.type.paper; + +import com.destroystokyo.paper.util.SneakyThrow; +import io.papermc.paper.plugin.bootstrap.PluginProviderContext; +import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; +import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder; +import io.papermc.paper.plugin.bootstrap.PluginBootstrap; +import io.papermc.paper.plugin.bootstrap.PluginProviderContextImpl; +import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader; +import io.papermc.paper.plugin.provider.PluginProvider; +import io.papermc.paper.plugin.provider.ProviderStatus; +import io.papermc.paper.plugin.provider.ProviderStatusHolder; +import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; +import io.papermc.paper.plugin.provider.type.PluginTypeFactory; +import io.papermc.paper.plugin.provider.util.ProviderUtil; +import org.bukkit.Bukkit; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; +import java.util.jar.JarFile; +import java.util.logging.Logger; + +public class PaperPluginParent { + + public static final PluginTypeFactory FACTORY = new PaperPluginProviderFactory(); + private final Path path; + private final JarFile jarFile; + private final PaperPluginMeta description; + private final PaperPluginClassLoader classLoader; + private final PluginProviderContext context; + private final Logger logger; + + public PaperPluginParent(Path path, JarFile jarFile, PaperPluginMeta description, PaperPluginClassLoader classLoader, PluginProviderContext context) { + this.path = path; + this.jarFile = jarFile; + this.description = description; + this.classLoader = classLoader; + this.context = context; + this.logger = context.getLogger(); + } + + public boolean shouldCreateBootstrap() { + return this.description.getBootstrapper() != null; + } + + public PaperBootstrapProvider createBootstrapProvider() { + return new PaperBootstrapProvider(); + } + + public PaperServerPluginProvider createPluginProvider(PaperBootstrapProvider provider) { + return new PaperServerPluginProvider(provider); + } + + public class PaperBootstrapProvider implements PluginProvider, ProviderStatusHolder, DependencyContextHolder { + + private ProviderStatus status; + private PluginBootstrap lastProvided; + + @Override + public @NotNull Path getSource() { + return PaperPluginParent.this.path; + } + + @Override + public JarFile file() { + return PaperPluginParent.this.jarFile; + } + + @Override + public PluginBootstrap createInstance() { + PluginBootstrap bootstrap = ProviderUtil.loadClass(PaperPluginParent.this.description.getBootstrapper(), + PluginBootstrap.class, PaperPluginParent.this.classLoader, () -> this.status = ProviderStatus.ERRORED); + this.status = ProviderStatus.INITIALIZED; + this.lastProvided = bootstrap; + return bootstrap; + } + + @Override + public PaperPluginMeta getMeta() { + return PaperPluginParent.this.description; + } + + @Override + public Logger getLogger() { + return PaperPluginParent.this.logger; + } + + @Override + public ProviderStatus getLastProvidedStatus() { + return this.status; + } + + @Override + public void setStatus(ProviderStatus status) { + this.status = status; + } + + public PluginBootstrap getLastProvided() { + return this.lastProvided; + } + + @Override + public void setContext(DependencyContext context) { + PaperPluginParent.this.classLoader.refreshClassloaderDependencyTree(context); + } + + @Override + public String toString() { + return "PaperBootstrapProvider{" + + "parent=" + PaperPluginParent.this + + "status=" + status + + ", lastProvided=" + lastProvided + + '}'; + } + } + + public class PaperServerPluginProvider implements PluginProvider, ProviderStatusHolder, DependencyContextHolder { + + private final PaperBootstrapProvider bootstrapProvider; + + private ProviderStatus status; + + PaperServerPluginProvider(PaperBootstrapProvider bootstrapProvider) { + this.bootstrapProvider = bootstrapProvider; + } + + @Override + public @NotNull Path getSource() { + return PaperPluginParent.this.path; + } + + @Override + public JarFile file() { + return PaperPluginParent.this.jarFile; + } + + @Override + public JavaPlugin createInstance() { + PluginBootstrap bootstrap = null; + if (this.bootstrapProvider != null && this.bootstrapProvider.getLastProvided() != null) { + bootstrap = this.bootstrapProvider.getLastProvided(); + } + + try { + JavaPlugin plugin; + if (bootstrap == null) { + plugin = ProviderUtil.loadClass(PaperPluginParent.this.description.getMainClass(), JavaPlugin.class, PaperPluginParent.this.classLoader); + } else { + plugin = bootstrap.createPlugin(PaperPluginParent.this.context); + } + + // Don't allow plugins to load plugins other than the one defined in main. This restriction might not be necessary. + if (!plugin.getClass().isAssignableFrom(Class.forName(PaperPluginParent.this.description.getMainClass(), true, plugin.getClass().getClassLoader()))) { + throw new IllegalArgumentException("Plugin provided must be the same type as main defined in plugin configuration!"); + } + + this.status = ProviderStatus.INITIALIZED; + return plugin; + } catch (Throwable throwable) { + this.status = ProviderStatus.ERRORED; + SneakyThrow.sneaky(throwable); + } + + throw new AssertionError(); // Impossible + } + + @Override + public PaperPluginMeta getMeta() { + return PaperPluginParent.this.description; + } + + @Override + public Logger getLogger() { + return PaperPluginParent.this.logger; + } + + @Override + public ProviderStatus getLastProvidedStatus() { + return this.status; + } + + @Override + public void setStatus(ProviderStatus status) { + this.status = status; + } + + public boolean shouldSkipCreation() { + if (this.bootstrapProvider == null) { + return false; + } + + return this.bootstrapProvider.getLastProvidedStatus() == ProviderStatus.ERRORED; + } + + /* + The plugin has to reuse the classloader in order to share the bootstrapper. + However, a plugin may have totally separate dependencies during bootstrapping. + This is a bit yuck, but in general we have to treat bootstrapping and normal game as connected. + */ + @Override + public void setContext(DependencyContext context) { + PaperPluginParent.this.classLoader.refreshClassloaderDependencyTree(context); + } + + @Override + public String toString() { + return "PaperServerPluginProvider{" + + "parent=" + PaperPluginParent.this + + "bootstrapProvider=" + bootstrapProvider + + ", status=" + status + + '}'; + } + } + + + @Override + public String toString() { + return "PaperPluginParent{" + + "path=" + path + + ", jarFile=" + jarFile + + ", description=" + description + + ", classLoader=" + classLoader + + '}'; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..db343a2f482ac375078610f0875692861f412ee2 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java @@ -0,0 +1,54 @@ +package io.papermc.paper.plugin.provider.type.paper; + +import com.destroystokyo.paper.utils.PaperPluginLogger; +import io.papermc.paper.plugin.bootstrap.PluginProviderContext; +import io.papermc.paper.plugin.bootstrap.PluginProviderContextImpl; +import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader; +import io.papermc.paper.plugin.entrypoint.classloader.PaperSimplePluginClassLoader; +import io.papermc.paper.plugin.loader.PaperClasspathBuilder; +import io.papermc.paper.plugin.loader.PluginLoader; +import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; +import io.papermc.paper.plugin.provider.type.PluginTypeFactory; +import io.papermc.paper.plugin.provider.util.ProviderUtil; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Path; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.logging.Logger; + +class PaperPluginProviderFactory implements PluginTypeFactory { + + @Override + public PaperPluginParent build(JarFile file, PaperPluginMeta configuration, Path source) throws Exception { + Logger logger = PaperPluginLogger.getLogger(configuration); + PluginProviderContext context = PluginProviderContextImpl.of(configuration, logger); + + PaperClasspathBuilder builder = new PaperClasspathBuilder(context); + + if (configuration.getLoader() != null) { + try ( + PaperSimplePluginClassLoader simplePluginClassLoader = new PaperSimplePluginClassLoader(source, file, configuration, this.getClass().getClassLoader()) + ) { + PluginLoader loader = ProviderUtil.loadClass(configuration.getLoader(), PluginLoader.class, simplePluginClassLoader); + loader.classloader(builder); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + PaperPluginClassLoader classLoader = builder.buildClassLoader(logger, source, file, configuration); + return new PaperPluginParent(source, file, configuration, classLoader, context); + } + + @Override + public PaperPluginMeta create(JarFile file, JarEntry config) throws Exception { + PaperPluginMeta configuration; + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(file.getInputStream(config)))) { + configuration = PaperPluginMeta.create(bufferedReader); + } + return configuration; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProvider.java b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..309c3dc492cbf469768d6a712485a0e42e73f947 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProvider.java @@ -0,0 +1,172 @@ +package io.papermc.paper.plugin.provider.type.spigot; + +import com.destroystokyo.paper.util.SneakyThrow; +import com.destroystokyo.paper.utils.PaperPluginLogger; +import io.papermc.paper.plugin.manager.PaperPluginManagerImpl; +import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; +import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder; +import io.papermc.paper.plugin.provider.PluginProvider; +import io.papermc.paper.plugin.provider.ProviderStatus; +import io.papermc.paper.plugin.provider.ProviderStatusHolder; +import io.papermc.paper.plugin.provider.type.PluginTypeFactory; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.plugin.InvalidPluginException; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.UnknownDependencyException; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.plugin.java.LibraryLoader; +import org.bukkit.plugin.java.PluginClassLoader; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarFile; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class SpigotPluginProvider implements PluginProvider, ProviderStatusHolder, DependencyContextHolder { + + public static final PluginTypeFactory FACTORY = new SpigotPluginProviderFactory(); + private static final LibraryLoader LIBRARY_LOADER = new LibraryLoader(Logger.getLogger("SpigotLibraryLoader")); + private final Path path; + private final PluginDescriptionFile description; + private final JarFile jarFile; + private final Logger logger; + private ProviderStatus status; + private DependencyContext dependencyContext; + + SpigotPluginProvider(Path path, JarFile file, PluginDescriptionFile description) { + this.path = path; + this.jarFile = file; + this.description = description; + this.logger = PaperPluginLogger.getLogger(description); + } + + @Override + public @NotNull Path getSource() { + return this.path; + } + + @Override + public JarFile file() { + return this.jarFile; + } + + @Override + public JavaPlugin createInstance() { + Server server = Bukkit.getServer(); + try { + + final File parentFile = server.getPluginsFolder(); // Paper + final File dataFolder = new File(parentFile, this.description.getName()); + @SuppressWarnings("deprecation") final File oldDataFolder = new File(parentFile, this.description.getRawName()); + + // Found old data folder + if (dataFolder.equals(oldDataFolder)) { + // They are equal -- nothing needs to be done! + } else if (dataFolder.isDirectory() && oldDataFolder.isDirectory()) { + server.getLogger().warning(String.format( + "While loading %s (%s) found old-data folder: `%s' next to the new one `%s'", + this.description.getFullName(), + this.path, + oldDataFolder, + dataFolder + )); + } else if (oldDataFolder.isDirectory() && !dataFolder.exists()) { + if (!oldDataFolder.renameTo(dataFolder)) { + throw new InvalidPluginException("Unable to rename old data folder: `" + oldDataFolder + "' to: `" + dataFolder + "'"); + } + server.getLogger().log(Level.INFO, String.format( + "While loading %s (%s) renamed data folder: `%s' to `%s'", + this.description.getFullName(), + this.path, + oldDataFolder, + dataFolder + )); + } + + if (dataFolder.exists() && !dataFolder.isDirectory()) { + throw new InvalidPluginException(String.format( + "Projected datafolder: `%s' for %s (%s) exists and is not a directory", + dataFolder, + this.description.getFullName(), + this.path + )); + } + + Set missingHardDependencies = new HashSet<>(this.description.getDepend().size()); // Paper - list all missing hard depends + for (final String pluginName : this.description.getDepend()) { + if (!this.dependencyContext.hasDependency(pluginName)) { + missingHardDependencies.add(pluginName); // Paper - list all missing hard depends + } + } + // Paper start - list all missing hard depends + if (!missingHardDependencies.isEmpty()) { + throw new UnknownDependencyException(missingHardDependencies, this.description.getFullName()); + } + // Paper end + + server.getUnsafe().checkSupported(this.description); + + final PluginClassLoader loader; + try { + loader = new PluginClassLoader(this.getClass().getClassLoader(), this.description, dataFolder, this.path.toFile(), LIBRARY_LOADER.createLoader(this.description), this.dependencyContext); // Paper + } catch (InvalidPluginException ex) { + throw ex; + } catch (Throwable ex) { + throw new InvalidPluginException(ex); + } + + // Override dependency context. + // We must provide a temporary context in order to properly handle dependencies on the plugin classloader constructor. + loader.dependencyContext = PaperPluginManagerImpl.getInstance(); + + this.status = ProviderStatus.INITIALIZED; + return loader.plugin; + } catch (Throwable ex) { + this.status = ProviderStatus.ERRORED; + SneakyThrow.sneaky(ex); + } + + throw new AssertionError(); // Shouldn't happen + } + + @Override + public PluginDescriptionFile getMeta() { + return this.description; + } + + @Override + public Logger getLogger() { + return this.logger; + } + + @Override + public ProviderStatus getLastProvidedStatus() { + return this.status; + } + + @Override + public void setStatus(ProviderStatus status) { + this.status = status; + } + + @Override + public void setContext(DependencyContext context) { + this.dependencyContext = context; + } + + @Override + public String toString() { + return "SpigotPluginProvider{" + + "path=" + path + + ", description=" + description + + ", jarFile=" + jarFile + + ", status=" + status + + ", dependencyContext=" + dependencyContext + + '}'; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..14ed05945ba5bfeb2b539d4786278b0e04130404 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/provider/type/spigot/SpigotPluginProviderFactory.java @@ -0,0 +1,45 @@ +package io.papermc.paper.plugin.provider.type.spigot; + +import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints; +import io.papermc.paper.plugin.provider.type.PluginTypeFactory; +import org.bukkit.plugin.InvalidDescriptionException; +import org.bukkit.plugin.PluginDescriptionFile; +import org.yaml.snakeyaml.error.YAMLException; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Locale; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +class SpigotPluginProviderFactory implements PluginTypeFactory { + + @Override + public SpigotPluginProvider build(JarFile file, PluginDescriptionFile configuration, Path source) throws Exception { + // Copied from SimplePluginManager#loadPlugins + // Spigot doesn't validate the name when the config is created, and instead when the plugin is loaded. + // Paper plugin configuration will do these checks in config serializer instead of when this is created. + String name = configuration.getRawName(); + if (PluginConfigConstraints.RESERVED_KEYS.contains(name.toLowerCase(Locale.ROOT))) { + throw new InvalidDescriptionException("Restricted name, cannot use %s as a plugin name.".formatted(name)); + } else if (name.indexOf(' ') != -1) { + throw new InvalidDescriptionException("Restricted name, cannot use 0x20 (space character) in a plugin name."); + } + + return new SpigotPluginProvider(source, file, configuration); + } + + @Override + public PluginDescriptionFile create(JarFile file, JarEntry config) throws Exception { + PluginDescriptionFile descriptionFile; + try (InputStream inputStream = file.getInputStream(config)) { + descriptionFile = new PluginDescriptionFile(inputStream); + } catch (IOException | YAMLException ex) { + throw new InvalidDescriptionException(ex); + } + + return descriptionFile; + } +} + diff --git a/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..7af995b941ce83265a93cdc6b5a2de8ad27e4db6 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/storage/BootstrapProviderStorage.java @@ -0,0 +1,113 @@ +package io.papermc.paper.plugin.storage; + +import com.mojang.logging.LogUtils; +import io.papermc.paper.plugin.PluginInitializerManager; +import io.papermc.paper.plugin.bootstrap.PluginBootstrap; +import io.papermc.paper.plugin.bootstrap.PluginProviderContext; +import io.papermc.paper.plugin.bootstrap.PluginProviderContextImpl; +import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; +import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder; +import io.papermc.paper.plugin.entrypoint.strategy.ModernPluginLoadingStrategy; +import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException; +import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration; +import io.papermc.paper.plugin.provider.PluginProvider; +import io.papermc.paper.plugin.provider.ProviderStatus; +import io.papermc.paper.plugin.provider.ProviderStatusHolder; +import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; +import io.papermc.paper.plugin.provider.configuration.type.DependencyConfiguration; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.List; + +public class BootstrapProviderStorage extends SimpleProviderStorage { + + private static final Logger LOGGER = LogUtils.getLogger(); + + public BootstrapProviderStorage() { + super(new ModernPluginLoadingStrategy<>(new ProviderConfiguration<>() { + @Override + public void applyContext(PluginProvider provider, DependencyContext dependencyContext) { + if (provider instanceof DependencyContextHolder contextHolder) { + contextHolder.setContext(dependencyContext); + } + } + + @Override + public boolean load(PluginProvider provider, PluginBootstrap provided) { + try { + PluginProviderContext context = PluginProviderContextImpl.of(provider, PluginInitializerManager.instance().pluginDirectoryPath()); + provided.bootstrap(context); + return true; + } catch (Exception e) { + LOGGER.error("Failed to run bootstrapper for %s. This plugin will not be loaded.".formatted(provider.getSource()), e); + if (provider instanceof ProviderStatusHolder statusHolder) { + statusHolder.setStatus(ProviderStatus.ERRORED); + } + return false; + } + } + + @Override + public List requiredDependencies(PluginProvider provider) { + List dependencies = new ArrayList<>(); + if (provider.getMeta() instanceof PaperPluginMeta paperPluginMeta) { + for (DependencyConfiguration configuration : paperPluginMeta.getDependencies()) { + if (configuration.required() && configuration.bootstrap()) { + dependencies.add(configuration.name()); + } + } + + return dependencies; + } + + throw new IllegalStateException(); + } + + @Override + public List optionalDependencies(PluginProvider provider) { + List dependencies = new ArrayList<>(); + if (provider.getMeta() instanceof PaperPluginMeta paperPluginMeta) { + for (DependencyConfiguration configuration : paperPluginMeta.getDependencies()) { + if (!configuration.required() && configuration.bootstrap()) { + dependencies.add(configuration.name()); + } + } + + return dependencies; + } + + throw new IllegalStateException(); + } + + @Override + public List loadBeforeDependencies(PluginProvider provider) { + return provider.getMeta().getLoadBeforePlugins(); + } + })); + } + + @Override + protected void handleCycle(PluginGraphCycleException exception) { + List logMessages = new ArrayList<>(); + for (List list : exception.getCycles()) { + // CoolPlugin depends on Dependency depends on CoolPlugin... + logMessages.add(String.join(" depends on ", list) + " depends on " + list.get(0) + "..."); + } + + LOGGER.error("Circular dependencies detected!"); + LOGGER.error("You have a plugin that is depending on a plugin which refers back to that plugin. Your server will shut down until these are resolved, or the strategy is changed."); + LOGGER.error("Circular dependencies:"); + for (String message : logMessages) { + LOGGER.error(message); + } + LOGGER.error("If you would like to still load these plugins, acknowledging that there may be unexpected plugin loading issues, run the server with -Dpaper.useLegacyPluginLoading=true"); + + System.exit(-1); + } + + @Override + public String toString() { + return "BOOTSTRAP:" + super.toString(); + } +} diff --git a/src/main/java/io/papermc/paper/plugin/storage/ConfiguredProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/ConfiguredProviderStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..eb4a7e1fbd9f0e853ebf965c6b4f9e0e6061ad74 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/storage/ConfiguredProviderStorage.java @@ -0,0 +1,51 @@ +package io.papermc.paper.plugin.storage; + +import io.papermc.paper.plugin.entrypoint.strategy.LegacyPluginLoadingStrategy; +import io.papermc.paper.plugin.entrypoint.strategy.ModernPluginLoadingStrategy; +import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException; +import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public abstract class ConfiguredProviderStorage extends SimpleProviderStorage { + + private static final Logger LOGGER = Logger.getLogger("ConfiguredOrderedProviderStorage"); + public static final boolean LEGACY_PLUGIN_LOADING = Boolean.getBoolean("paper.useLegacyPluginLoading"); + + protected ConfiguredProviderStorage(ProviderConfiguration onLoad) { + // This doesn't work with reloading. + // Should we care? + super(LEGACY_PLUGIN_LOADING ? new LegacyPluginLoadingStrategy<>(onLoad) : new ModernPluginLoadingStrategy<>(onLoad)); + } + + @Override + protected void handleCycle(PluginGraphCycleException exception) { + List logMessages = new ArrayList<>(); + for (List list : exception.getCycles()) { + logMessages.add(String.join(" -> ", list) + " -> " + list.get(0)); + } + + LOGGER.log(Level.SEVERE, "Circular dependencies detected! This happens when"); + LOGGER.log(Level.SEVERE, " i) plugin A has a plugin B in its (soft)depend list, and plugin B has plugin A in its (soft)depend list, or"); + LOGGER.log(Level.SEVERE, " ii) plugin A has plugin B both in its (soft)depend list and its loadbefore list."); + LOGGER.log(Level.SEVERE, "Circular dependencies:"); + for (String logMessage : logMessages) { + LOGGER.log(Level.SEVERE, " " + logMessage); + } + LOGGER.log(Level.SEVERE, "Please report this to the plugin authors of the first plugin of each loop or join the PaperMC Discord server for further help."); + LOGGER.log(Level.SEVERE, "If you would like to still load these plugins, acknowledging that there may be unexpected plugin loading issues, run the server with -Dpaper.useLegacyPluginLoading=true"); + + if (this.exitOnCycleDependencies()) { + throw new IllegalStateException("Circular plugin dependencies from plugins " + exception.getCycles().stream().map(cycle -> cycle.get(0)).collect(Collectors.joining(", "))); + } + } + + public boolean exitOnCycleDependencies() { + return true; + } + +} diff --git a/src/main/java/io/papermc/paper/plugin/storage/ProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/ProviderStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..374e7d3d69fc8603ecf54999f173123d3a9fbf6e --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/storage/ProviderStorage.java @@ -0,0 +1,18 @@ +package io.papermc.paper.plugin.storage; + +import io.papermc.paper.plugin.provider.PluginProvider; + +/** + * A provider storage is meant to be a singleton that stores providers. + * + * @param provider type + */ +public interface ProviderStorage { + + void register(PluginProvider provider); + + void enter(); + + Iterable> getRegisteredProviders(); + +} diff --git a/src/main/java/io/papermc/paper/plugin/storage/ServerPluginProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/ServerPluginProviderStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..8b47e585c85e54a71e34aa57d61c1b2b8c9edfdf --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/storage/ServerPluginProviderStorage.java @@ -0,0 +1,85 @@ +package io.papermc.paper.plugin.storage; + +import com.mojang.logging.LogUtils; +import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; +import io.papermc.paper.plugin.entrypoint.dependency.DependencyContextHolder; +import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration; +import io.papermc.paper.plugin.manager.PaperPluginManagerImpl; +import io.papermc.paper.plugin.provider.PluginProvider; +import io.papermc.paper.plugin.provider.ProviderStatus; +import io.papermc.paper.plugin.provider.ProviderStatusHolder; +import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; +import org.slf4j.Logger; + +import java.util.List; + +public class ServerPluginProviderStorage extends ConfiguredProviderStorage { + + private static final Logger LOGGER = LogUtils.getLogger(); + + public ServerPluginProviderStorage() { + super(new ProviderConfiguration<>() { + @Override + public void applyContext(PluginProvider provider, DependencyContext dependencyContext) { + Plugin alreadyLoadedPlugin = PaperPluginManagerImpl.getInstance().getPlugin(provider.getMeta().getName()); + if (alreadyLoadedPlugin != null) { + throw new IllegalStateException("Provider " + provider + " attempted to add duplicate plugin identifier " + alreadyLoadedPlugin + " THIS WILL CREATE BUGS!!!"); + } + + if (provider instanceof DependencyContextHolder contextHolder) { + contextHolder.setContext(dependencyContext); + } + } + + @Override + public boolean load(PluginProvider provider, JavaPlugin provided) { + // Add it to the map here, we have to run the actual loading logic later. + PaperPluginManagerImpl.getInstance().loadPlugin(provided); + return true; + } + + @Override + public List requiredDependencies(PluginProvider provider) { + return provider.getMeta().getPluginDependencies(); + } + + @Override + public List optionalDependencies(PluginProvider provider) { + return provider.getMeta().getPluginSoftDependencies(); + } + + @Override + public List loadBeforeDependencies(PluginProvider provider) { + return provider.getMeta().getLoadBeforePlugins(); + } + }); + } + + @Override + protected void filterLoadingProviders(List> pluginProviders) { + /* + Have to do this to prevent loading plugin providers that have failed initializers. + This is a hack and a better solution here would be to store failed plugin providers elsewhere. + */ + pluginProviders.removeIf((provider) -> (provider instanceof PaperPluginParent.PaperServerPluginProvider pluginProvider && pluginProvider.shouldSkipCreation())); + } + + // We need to call the load methods AFTER all plugins are constructed + @Override + public void processProvided(PluginProvider provider, JavaPlugin provided) { + try { + provided.getLogger().info(String.format("Loading server plugin %s", provided.getPluginMeta().getDisplayName())); + provided.onLoad(); + } catch (Throwable ex) { + // Don't mark that provider as ERRORED, as this apparently still needs to run the onEnable logic. + provided.getSLF4JLogger().error("Error initializing plugin '%s' in folder '%s' (Is it up to date?)".formatted(provider.getFileName(), provider.getParentSource()), ex); + } + } + + @Override + public String toString() { + return "PLUGIN:" + super.toString(); + } +} diff --git a/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java new file mode 100644 index 0000000000000000000000000000000000000000..1c4c344eb65dd90e1d3698908b0d9b46262c9540 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java @@ -0,0 +1,57 @@ +package io.papermc.paper.plugin.storage; + +import io.papermc.paper.plugin.entrypoint.strategy.PluginGraphCycleException; +import io.papermc.paper.plugin.entrypoint.strategy.ProviderLoadingStrategy; +import io.papermc.paper.plugin.provider.PluginProvider; + +import java.util.ArrayList; +import java.util.List; + +public abstract class SimpleProviderStorage implements ProviderStorage { + + protected final List> providers = new ArrayList<>(); + protected ProviderLoadingStrategy strategy; + + protected SimpleProviderStorage(ProviderLoadingStrategy strategy) { + this.strategy = strategy; + } + + @Override + public void register(PluginProvider provider) { + this.providers.add(provider); + } + + @Override + public void enter() { + List> providerList = new ArrayList<>(this.providers); + this.filterLoadingProviders(providerList); + + try { + for (ProviderLoadingStrategy.ProviderPair providerPair : this.strategy.loadProviders(providerList)) { + this.processProvided(providerPair.provider(), providerPair.provided()); + } + } catch (PluginGraphCycleException exception) { + this.handleCycle(exception); + } + } + + @Override + public Iterable> getRegisteredProviders() { + return this.providers; + } + + public void processProvided(PluginProvider provider, T provided) {} + + // Mutable enter + protected void filterLoadingProviders(List> providers) {} + + protected abstract void handleCycle(PluginGraphCycleException exception); + + @Override + public String toString() { + return "SimpleProviderStorage{" + + "providers=" + this.providers + + ", strategy=" + this.strategy + + '}'; + } +} diff --git a/src/main/java/io/papermc/paper/plugin/storage/package-info.java b/src/main/java/io/papermc/paper/plugin/storage/package-info.java new file mode 100644 index 0000000000000000000000000000000000000000..c1114675137e862ac9682b635bfdbfbc1d7c6e67 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/storage/package-info.java @@ -0,0 +1,5 @@ +/** + * Classes in this package are supposed to connect components of {@link io.papermc.paper.plugin.entrypoint} and {@link io.papermc.paper.plugin.provider} packages. + * @see io.papermc.paper.plugin.entrypoint.Entrypoint + */ +package io.papermc.paper.plugin.storage; diff --git a/src/main/java/io/papermc/paper/plugin/util/EntrypointUtil.java b/src/main/java/io/papermc/paper/plugin/util/EntrypointUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..98518351e616e536315cd89790b327d3bad33d0e --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/util/EntrypointUtil.java @@ -0,0 +1,19 @@ +package io.papermc.paper.plugin.util; + +import com.mojang.logging.LogUtils; +import io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler; +import io.papermc.paper.plugin.provider.source.ProviderSource; +import org.slf4j.Logger; + +public class EntrypointUtil { + + private static final Logger LOGGER = LogUtils.getLogger(); + + public static void registerProvidersFromSource(ProviderSource source, C context) { + try { + source.registerProviders(LaunchEntryPointHandler.INSTANCE, context); + } catch (Throwable e) { + LOGGER.error(e.getMessage(), e); + } + } +} diff --git a/src/main/java/io/papermc/paper/plugin/util/NamespaceChecker.java b/src/main/java/io/papermc/paper/plugin/util/NamespaceChecker.java new file mode 100644 index 0000000000000000000000000000000000000000..fd55fd1d6518ebd1bc2513dd331f072018fd4782 --- /dev/null +++ b/src/main/java/io/papermc/paper/plugin/util/NamespaceChecker.java @@ -0,0 +1,37 @@ +package io.papermc.paper.plugin.util; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public class NamespaceChecker { + + private static final String[] QUICK_INVALID_NAMESPACES = { + "net.minecraft.", + "org.bukkit.", + "io.papermc.paper.", + "com.destroystokoyo.paper." + }; + + /** + * Used for a variety of namespaces that shouldn't be resolved and should instead be moved to + * other classloaders. We can assume this because only plugins should be using this classloader. + * + * @param name namespace + */ + public static void validateNameSpaceForClassloading(@NotNull String name) throws ClassNotFoundException { + if (!isValidNameSpace(name)) { + throw new ClassNotFoundException(name); + } + } + + public static boolean isValidNameSpace(@NotNull String name) { + for (String string : QUICK_INVALID_NAMESPACES) { + if (name.startsWith(string)) { + return false; + } + } + + return true; + } +} diff --git a/src/main/java/io/papermc/paper/util/StackWalkerUtil.java b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java index f7114d5b8f2f93f62883e24da29afaf9f74ee1a6..00e78a495575abb9496b1849822605b613afe3b4 100644 --- a/src/main/java/io/papermc/paper/util/StackWalkerUtil.java +++ b/src/main/java/io/papermc/paper/util/StackWalkerUtil.java @@ -1,9 +1,11 @@ package io.papermc.paper.util; +import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader; import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.PluginClassLoader; import org.jetbrains.annotations.Nullable; +import java.util.Objects; import java.util.Optional; public class StackWalkerUtil { @@ -12,11 +14,20 @@ public class StackWalkerUtil { public static JavaPlugin getFirstPluginCaller() { Optional foundFrame = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) .walk(stream -> stream - .filter(frame -> frame.getDeclaringClass().getClassLoader() instanceof PluginClassLoader) .map((frame) -> { - PluginClassLoader classLoader = (PluginClassLoader) frame.getDeclaringClass().getClassLoader(); - return classLoader.getPlugin(); + ClassLoader classLoader = frame.getDeclaringClass().getClassLoader(); + JavaPlugin plugin; + if (classLoader instanceof PaperPluginClassLoader pluginClassLoader) { + plugin = pluginClassLoader.getLoadedJavaPlugin(); + } else if (classLoader instanceof PluginClassLoader spigotClassloader) { + plugin = spigotClassloader.getPlugin(); + } else { + plugin = null; + } + + return plugin; }) + .filter(Objects::nonNull) .findFirst()); return foundFrame.orElse(null); diff --git a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java index 5b8ecf5b0165ed2cd4397cdee958e97c2e8f18d5..ad802eb211f05f646159d7fc53f8a9427b46cb89 100644 --- a/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java +++ b/src/main/java/net/minecraft/core/registries/BuiltInRegistries.java @@ -310,7 +310,13 @@ public class BuiltInRegistries { } public static void bootStrap() { + // Paper start + bootStrap(() -> {}); + } + public static void bootStrap(Runnable runnable) { + // Paper end createContents(); + runnable.run(); // Paper freeze(); validate(REGISTRY); } diff --git a/src/main/java/net/minecraft/server/Bootstrap.java b/src/main/java/net/minecraft/server/Bootstrap.java index b5aa358638b9d0638dfe47f7ebac04cca1dd80b9..e43096e69a00f9ea96badd7c966443cfcf3e7b95 100644 --- a/src/main/java/net/minecraft/server/Bootstrap.java +++ b/src/main/java/net/minecraft/server/Bootstrap.java @@ -69,7 +69,11 @@ public class Bootstrap { EntitySelectorOptions.bootStrap(); DispenseItemBehavior.bootStrap(); CauldronInteraction.bootStrap(); - BuiltInRegistries.bootStrap(); + // Paper start + BuiltInRegistries.bootStrap(() -> { + io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.enterBootstrappers(); // Paper - Entrypoint for bootstrapping + }); + // Paper end Bootstrap.wrapStreams(); } // CraftBukkit start - easier than fixing the decompile diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java index b7399e29094c66c88a6f4c0e996a906bcaa3b4ca..abf4c54eec6881d6e05893983f83f9eb4b249634 100644 --- a/src/main/java/net/minecraft/server/Main.java +++ b/src/main/java/net/minecraft/server/Main.java @@ -110,6 +110,17 @@ public class Main { JvmProfiler.INSTANCE.start(Environment.SERVER); } + // Paper start + + // We have to load the bukkit configuration inorder to get the update folder location. + io.papermc.paper.plugin.PluginInitializerManager pluginSystem = io.papermc.paper.plugin.PluginInitializerManager.init(optionset); + // Register the default plugin directory + io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.DirectoryProviderSource.INSTANCE, pluginSystem.pluginDirectoryPath()); + @SuppressWarnings("unchecked") + java.util.List files = (java.util.List) optionset.valuesOf("add-plugin"); + // Register plugins from the flag + io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.PluginFlagProviderSource.INSTANCE, files); + // Paper end Bootstrap.bootStrap(); Bootstrap.validate(); Util.startTimerHackThread(); diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java index 26ca07b5e302cc4cc02e06f5d07f6d9eb541275e..17a6290969a63be85fa780e2cad4ce63790379b1 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -268,7 +268,8 @@ public final class CraftServer implements Server { private final CraftCommandMap commandMap = new CraftCommandMap(this); private final SimpleHelpMap helpMap = new SimpleHelpMap(this); private final StandardMessenger messenger = new StandardMessenger(); - private final SimplePluginManager pluginManager = new SimplePluginManager(this, this.commandMap); + private final SimplePluginManager pluginManager = new SimplePluginManager(this, commandMap); + public final io.papermc.paper.plugin.manager.PaperPluginManagerImpl paperPluginManager = new io.papermc.paper.plugin.manager.PaperPluginManagerImpl(this, this.commandMap, pluginManager); {this.pluginManager.paperPluginManager = this.paperPluginManager;} // Paper private final StructureManager structureManager; protected final DedicatedServer console; protected final DedicatedPlayerList playerList; @@ -416,24 +417,7 @@ public final class CraftServer implements Server { } public void loadPlugins() { - this.pluginManager.registerInterface(JavaPluginLoader.class); - - File pluginFolder = (File) console.options.valueOf("plugins"); - - if (pluginFolder.exists()) { - Plugin[] plugins = this.pluginManager.loadPlugins(pluginFolder); - for (Plugin plugin : plugins) { - try { - String message = String.format("Loading %s", plugin.getDescription().getFullName()); - plugin.getLogger().info(message); - plugin.onLoad(); - } catch (Throwable ex) { - Logger.getLogger(CraftServer.class.getName()).log(Level.SEVERE, ex.getMessage() + " initializing " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex); - } - } - } else { - pluginFolder.mkdir(); - } + io.papermc.paper.plugin.entrypoint.LaunchEntryPointHandler.INSTANCE.enter(io.papermc.paper.plugin.entrypoint.Entrypoint.PLUGIN); // Paper - replace implementation } public void enablePlugins(PluginLoadOrder type) { @@ -522,15 +506,17 @@ public final class CraftServer implements Server { private void enablePlugin(Plugin plugin) { try { List perms = plugin.getDescription().getPermissions(); - + List permsToLoad = new ArrayList<>(); // Paper for (Permission perm : perms) { - try { - this.pluginManager.addPermission(perm, false); - } catch (IllegalArgumentException ex) { - this.getLogger().log(Level.WARNING, "Plugin " + plugin.getDescription().getFullName() + " tried to register permission '" + perm.getName() + "' but it's already registered", ex); + // Paper start + if (this.paperPluginManager.getPermission(perm.getName()) == null) { + permsToLoad.add(perm); + } else { + this.getLogger().log(Level.WARNING, "Plugin " + plugin.getDescription().getFullName() + " tried to register permission '" + perm.getName() + "' but it's already registered"); + // Paper end } } - this.pluginManager.dirtyPermissibles(); + this.paperPluginManager.addPermissions(permsToLoad); // Paper this.pluginManager.enablePlugin(plugin); } catch (Throwable ex) { diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java index 909b2c98e7a9117d2f737245e4661792ffafb744..d96399e9bf1a58db5a4a22e58abb99e7660e0694 100644 --- a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java +++ b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java @@ -42,6 +42,12 @@ public class MinecraftInternalPlugin extends PluginBase { public PluginDescriptionFile getDescription() { return pdf; } + // Paper start + @Override + public io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta() { + return pdf; + } + // Paper end @Override public FileConfiguration getConfig() { diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java index 750f4b3930278c291f10015c7a8a8df57d04a286..3d2303dbd06a12968302cb100e36be9de28700f0 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java @@ -436,6 +436,12 @@ public final class CraftMagicNumbers implements UnsafeValues { net.minecraft.world.item.ItemStack nmsItemStack = CraftItemStack.asNMSCopy(itemStack); return nmsItemStack.getItem().getDescriptionId(nmsItemStack); } + // Paper start + @Override + public boolean isSupportedApiVersion(String apiVersion) { + return apiVersion != null && SUPPORTED_API.contains(apiVersion); + } + // Paper end /** * This helper class represents the different NBT Tags. diff --git a/src/main/resources/META-INF/services/io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier b/src/main/resources/META-INF/services/io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier new file mode 100644 index 0000000000000000000000000000000000000000..20dbe2775951bfcdb85c5d679ac86c77a93e0847 --- /dev/null +++ b/src/main/resources/META-INF/services/io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier @@ -0,0 +1 @@ +io.papermc.paper.plugin.entrypoint.classloader.PaperClassloaderBytecodeModifier diff --git a/src/main/resources/META-INF/services/io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage b/src/main/resources/META-INF/services/io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage new file mode 100644 index 0000000000000000000000000000000000000000..a22647244037cd92262b3b5a6582f0a11172fdc8 --- /dev/null +++ b/src/main/resources/META-INF/services/io.papermc.paper.plugin.provider.classloader.PaperClassLoaderStorage @@ -0,0 +1 @@ +io.papermc.paper.plugin.entrypoint.classloader.group.PaperPluginClassLoaderStorage diff --git a/src/main/resources/META-INF/services/org.bukkit.plugin.PluginLoader b/src/main/resources/META-INF/services/org.bukkit.plugin.PluginLoader new file mode 100644 index 0000000000000000000000000000000000000000..4f78bc62d03460463b9694de933e5b73da8df6e3 --- /dev/null +++ b/src/main/resources/META-INF/services/org.bukkit.plugin.PluginLoader @@ -0,0 +1 @@ +io.papermc.paper.plugin.manager.DummyBukkitPluginLoader diff --git a/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java b/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java new file mode 100644 index 0000000000000000000000000000000000000000..1d14f530ef888102e47eeeaf0d1a6076e51871c4 --- /dev/null +++ b/src/test/java/io/papermc/paper/plugin/PaperTestPlugin.java @@ -0,0 +1,146 @@ +package io.papermc.paper.plugin; + +import io.papermc.paper.plugin.configuration.PluginMeta; +import org.bukkit.Server; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.plugin.PluginBase; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.PluginLoader; +import org.bukkit.plugin.PluginLogger; + +import java.io.File; +import java.io.InputStream; +import java.util.List; + +public class PaperTestPlugin extends PluginBase { + private final String pluginName; + private boolean enabled = true; + private final PluginMeta configuration; + + public PaperTestPlugin(String pluginName) { + this.pluginName = pluginName; + this.configuration = new TestPluginMeta(pluginName); + } + + public PaperTestPlugin(PluginMeta configuration) { + this.configuration = configuration; + this.pluginName = configuration.getName(); + } + + @Override + public File getDataFolder() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public PluginDescriptionFile getDescription() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public PluginMeta getPluginMeta() { + return this.configuration; + } + + @Override + public FileConfiguration getConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public InputStream getResource(String filename) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void saveConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void saveDefaultConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void saveResource(String resourcePath, boolean replace) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void reloadConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public PluginLogger getLogger() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public PluginLoader getPluginLoader() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public Server getServer() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public void onDisable() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void onLoad() { + } + + @Override + public void onEnable() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean isNaggable() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void setNaggable(boolean canNag) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public BiomeProvider getDefaultBiomeProvider(String worldName, String id) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + throw new UnsupportedOperationException("Not supported."); + } +} diff --git a/src/test/java/io/papermc/paper/plugin/PluginDependencyLoadingTest.java b/src/test/java/io/papermc/paper/plugin/PluginDependencyLoadingTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7665967dc1849dde750c6d24298e76c2c74f8443 --- /dev/null +++ b/src/test/java/io/papermc/paper/plugin/PluginDependencyLoadingTest.java @@ -0,0 +1,159 @@ +package io.papermc.paper.plugin; + +import io.papermc.paper.plugin.provider.entrypoint.DependencyContext; +import io.papermc.paper.plugin.entrypoint.strategy.ModernPluginLoadingStrategy; +import io.papermc.paper.plugin.entrypoint.strategy.ProviderConfiguration; +import io.papermc.paper.plugin.provider.PluginProvider; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +public class PluginDependencyLoadingTest { + + private static List> REGISTERED_PROVIDERS = new ArrayList<>(); + private static Map LOAD_ORDER = new HashMap<>(); + + static { + setup(); + } + + private static TestJavaPluginProvider setup(String identifier, String[] hard, String[] soft, String[] before) { + TestPluginMeta configuration = new TestPluginMeta(identifier); + configuration.setHardDependencies(List.of(hard)); + configuration.setSoftDependencies(List.of(soft)); + configuration.setLoadBefore(List.of(before)); + + TestJavaPluginProvider provider = new TestJavaPluginProvider(configuration); + REGISTERED_PROVIDERS.add(provider); + return provider; + } + + /** + * Obfuscated plugin names, this uses a real dependency tree... + */ + private static void setup() { + setup("RedAir", new String[]{}, new String[]{"NightShovel", "EmeraldFire"}, new String[]{"GreenShovel", "IronSpork", "BrightBlueShovel", "WireDoor"}); + setup("BigGrass", new String[]{}, new String[]{"IronEarth", "RedAir"}, new String[]{"BlueFire"}); + setup("BlueFire", new String[]{}, new String[]{}, new String[]{}); + setup("BigPaper", new String[]{}, new String[]{"BlueFire"}, new String[]{}); + setup("EmeraldSpork", new String[]{}, new String[]{}, new String[]{"GoldPaper", "YellowSnow"}); + setup("GreenShovel", new String[]{}, new String[]{}, new String[]{}); + setup("BrightBlueGrass", new String[]{"BigPaper"}, new String[]{"DarkSpork"}, new String[]{}); + setup("GoldPaper", new String[]{}, new String[]{"BlueFire"}, new String[]{}); + setup("GreenGlass", new String[]{}, new String[]{}, new String[]{}); + setup("GoldNeptune", new String[]{}, new String[]{"GreenShovel", "GoldNeptuneVersioning"}, new String[]{}); + setup("RedPaper", new String[]{}, new String[]{"GoldPaper", "GoldFire", "EmeraldGrass", "BlueFire", "CopperSpork", "YellowDoor", "OrangeClam", "BlueSponge", "GoldNeptune", "BrightBlueGrass", "DarkSpoon", "BigShovel", "GreenGlass", "IronGlass"}, new String[]{"IronPaper", "YellowFire"}); + setup("YellowGrass", new String[]{}, new String[]{"RedAir"}, new String[]{}); + setup("WireFire", new String[]{}, new String[]{"RedPaper", "WireGrass", "YellowSpork", "NightAir"}, new String[]{}); + setup("OrangeNeptune", new String[]{}, new String[]{}, new String[]{}); + setup("BigSpoon", new String[]{"YellowGrass", "GreenShovel"}, new String[]{"RedAir", "GoldNeptune", "BrightBlueGrass", "LightDoor", "LightSpork", "LightEarth", "NightDoor", "OrangeSpoon", "GoldSponge", "GoldDoor", "DarkPaper", "RedPaper", "GreenGlass", "IronGlass", "NightGlass", "BigGrass", "BlueFire", "YellowSpoon", "DiamondGrass", "DiamondShovel", "DarkSnow", "EmeraldGlass", "EmeraldSpoon", "LightFire", "WireGrass", "RedEarth", "WireFire"}, new String[]{}); + setup("CopperSnow", new String[]{}, new String[]{"RedSnow", "OrangeFire", "WireAir", "GreenGlass", "NightSpork", "EmeraldPaper"}, new String[]{"BlueGrass"}); + setup("BrightBluePaper", new String[]{}, new String[]{"GoldEarth", "BrightBlueSpoon", "CopperGlass", "LightSporkChat", "DarkAir", "LightEarth", "DiamondDoor", "YellowShovel", "BlueAir", "DarkShovel", "GoldPaper", "BlueFire", "GreenGlass", "YellowSpork", "BigGrass", "OrangePaper", "DarkPaper"}, new String[]{"WireShovel"}); + setup("LightSponge", new String[]{}, new String[]{}, new String[]{}); + setup("OrangeShovel", new String[]{}, new String[]{}, new String[]{}); + setup("GoldGrass", new String[]{}, new String[]{"GreenGlass", "BlueFire"}, new String[]{}); + setup("IronSponge", new String[]{}, new String[]{"DiamondEarth"}, new String[]{}); + setup("EmeraldSnow", new String[]{}, new String[]{}, new String[]{}); + setup("BlueSpoon", new String[]{"BigGrass"}, new String[]{"GreenGlass", "GoldPaper", "GreenShovel", "YellowClam"}, new String[]{}); + setup("BigSpork", new String[]{}, new String[]{"BigPaper"}, new String[]{}); + setup("BluePaper", new String[]{}, new String[]{"BigClam", "RedSpoon", "GreenFire", "WireSnow", "OrangeSnow", "BlueFire", "BrightBlueGrass", "YellowSpork", "GreenGlass"}, new String[]{}); + setup("OrangeSpork", new String[]{}, new String[]{}, new String[]{}); + setup("DiamondNeptune", new String[]{}, new String[]{"GreenGlass", "GreenShovel", "YellowNeptune"}, new String[]{}); + setup("BigFire", new String[]{}, new String[]{"BlueFire", "BrightBlueDoor", "GreenGlass"}, new String[]{}); + setup("NightNeptune", new String[]{}, new String[]{"BlueFire", "DarkGlass", "GoldPaper", "YellowNeptune", "BlueShovel"}, new String[]{}); + setup("YellowEarth", new String[]{"RedAir"}, new String[]{}, new String[]{}); + setup("DiamondClam", new String[]{}, new String[]{}, new String[]{}); + setup("CopperAir", new String[]{}, new String[]{"BigPaper"}, new String[]{}); + setup("NightSpoon", new String[]{"OrangeNeptune"}, new String[]{"BlueFire", "GreenGlass", "RedSpork", "GoldPaper", "BigShovel", "YellowSponge", "EmeraldSpork"}, new String[]{}); + setup("GreenClam", new String[]{}, new String[]{"GreenShovel", "BrightBlueEarth", "BigSpoon", "RedPaper", "BlueFire", "GreenGlass", "WireFire", "GreenSnow"}, new String[]{}); + setup("YellowPaper", new String[]{}, new String[]{}, new String[]{}); + setup("WireGlass", new String[]{"YellowGrass"}, new String[]{"YellowGlass", "BigSpoon", "CopperSnow", "GreenGlass", "BlueEarth"}, new String[]{}); + setup("BlueSpork", new String[]{}, new String[]{"BrightBlueGrass"}, new String[]{}); + setup("CopperShovel", new String[]{}, new String[]{"GreenGlass"}, new String[]{}); + setup("RedClam", new String[]{}, new String[]{}, new String[]{}); + setup("EmeraldClam", new String[]{}, new String[]{"BlueFire"}, new String[]{}); + setup("DarkClam", new String[]{}, new String[]{"GoldAir", "LightGlass"}, new String[]{}); + setup("WireSpoon", new String[]{}, new String[]{"GoldPaper", "LightSnow"}, new String[]{}); + setup("CopperNeptune", new String[]{}, new String[]{"GreenGlass", "BigGrass"}, new String[]{}); + setup("RedNeptune", new String[]{}, new String[]{}, new String[]{}); + setup("GreenAir", new String[]{}, new String[]{}, new String[]{}); + setup("RedFire", new String[]{"BrightBlueGrass", "BigPaper"}, new String[]{"BlueFire", "GreenGlass", "BigGrass"}, new String[]{}); + } + + @Before + public void loadProviders() { + AtomicInteger currentLoad = new AtomicInteger(); + ModernPluginLoadingStrategy modernPluginLoadingStrategy = new ModernPluginLoadingStrategy<>(new ProviderConfiguration<>() { + @Override + public void applyContext(PluginProvider provider, DependencyContext dependencyContext) { + } + + @Override + public boolean load(PluginProvider provider, PaperTestPlugin provided) { + LOAD_ORDER.put(provider.getMeta().getName(), currentLoad.getAndIncrement()); + return false; + } + + @Override + public List requiredDependencies(PluginProvider provider) { + return provider.getMeta().getPluginDependencies(); + } + + @Override + public List optionalDependencies(PluginProvider provider) { + return provider.getMeta().getPluginSoftDependencies(); + } + + @Override + public List loadBeforeDependencies(PluginProvider provider) { + return provider.getMeta().getLoadBeforePlugins(); + } + }); + + modernPluginLoadingStrategy.loadProviders(REGISTERED_PROVIDERS); + } + + @Test + public void testDependencies() { + for (PluginProvider provider : REGISTERED_PROVIDERS) { + TestPluginMeta pluginMeta = (TestPluginMeta) provider.getMeta(); + String identifier = pluginMeta.getName(); + Assert.assertTrue("Provider wasn't loaded! (%s)".formatted(identifier), LOAD_ORDER.containsKey(identifier)); + + int index = LOAD_ORDER.get(identifier); + + // Hard dependencies should be loaded BEFORE + for (String hardDependency : pluginMeta.getPluginDependencies()) { + Assert.assertTrue("Plugin (%s) is missing hard dependency (%s)".formatted(identifier, hardDependency), LOAD_ORDER.containsKey(hardDependency)); + + int dependencyIndex = LOAD_ORDER.get(hardDependency); + Assert.assertTrue("Plugin (%s) was not loaded BEFORE soft dependency. (%s)".formatted(identifier, hardDependency), index > dependencyIndex); + } + + for (String softDependency : pluginMeta.getPluginSoftDependencies()) { + if (!LOAD_ORDER.containsKey(softDependency)) { + continue; + } + + int dependencyIndex = LOAD_ORDER.get(softDependency); + + Assert.assertTrue("Plugin (%s) was not loaded BEFORE soft dependency. (%s)".formatted(identifier, softDependency), index > dependencyIndex); + } + + for (String loadBefore : pluginMeta.getLoadBeforePlugins()) { + if (!LOAD_ORDER.containsKey(loadBefore)) { + continue; + } + + int dependencyIndex = LOAD_ORDER.get(loadBefore); + Assert.assertTrue("Plugin (%s) was NOT loaded BEFORE loadbefore dependency. (%s)".formatted(identifier, loadBefore), index < dependencyIndex); + } + } + } +} diff --git a/src/test/java/io/papermc/paper/plugin/PluginLoadingTest.java b/src/test/java/io/papermc/paper/plugin/PluginLoadingTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/test/java/io/papermc/paper/plugin/PluginManagerTest.java b/src/test/java/io/papermc/paper/plugin/PluginManagerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..726eba26470e62b0e94a91418512e242464800ae --- /dev/null +++ b/src/test/java/io/papermc/paper/plugin/PluginManagerTest.java @@ -0,0 +1,75 @@ +package io.papermc.paper.plugin; + +import org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.bukkit.permissions.Permission; +import org.bukkit.plugin.PluginManager; +import org.bukkit.support.AbstractTestingBase; +import org.junit.After; +import org.junit.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class PluginManagerTest extends AbstractTestingBase { + + private static final PluginManager pm = Bukkit.getPluginManager(); + + @Test + public void testSyncSameThread() { + final Event event = new TestEvent(false); + pm.callEvent(event); + } + + @Test + public void testRemovePermissionByNameLower() { + this.testRemovePermissionByName("lower"); + } + + @Test + public void testRemovePermissionByNameUpper() { + this.testRemovePermissionByName("UPPER"); + } + + @Test + public void testRemovePermissionByNameCamel() { + this.testRemovePermissionByName("CaMeL"); + } + + @Test + public void testRemovePermissionByPermissionLower() { + this.testRemovePermissionByPermission("lower"); + } + + @Test + public void testRemovePermissionByPermissionUpper() { + this.testRemovePermissionByPermission("UPPER"); + } + + @Test + public void testRemovePermissionByPermissionCamel() { + this.testRemovePermissionByPermission("CaMeL"); + } + + private void testRemovePermissionByName(final String name) { + final Permission perm = new Permission(name); + pm.addPermission(perm); + assertThat("Permission \"" + name + "\" was not added", pm.getPermission(name), is(perm)); + pm.removePermission(name); + assertThat("Permission \"" + name + "\" was not removed", pm.getPermission(name), is(nullValue())); + } + + private void testRemovePermissionByPermission(final String name) { + final Permission perm = new Permission(name); + pm.addPermission(perm); + assertThat("Permission \"" + name + "\" was not added", pm.getPermission(name), is(perm)); + pm.removePermission(perm); + assertThat("Permission \"" + name + "\" was not removed", pm.getPermission(name), is(nullValue())); + } + + @After + public void tearDown() { + pm.clearPlugins(); + assertThat(pm.getPermissions(), is(empty())); + } +} diff --git a/src/test/java/io/papermc/paper/plugin/SyntheticEventTest.java b/src/test/java/io/papermc/paper/plugin/SyntheticEventTest.java new file mode 100644 index 0000000000000000000000000000000000000000..51c08740edffd152c8e2b6d3676ff7f1ce6090c6 --- /dev/null +++ b/src/test/java/io/papermc/paper/plugin/SyntheticEventTest.java @@ -0,0 +1,42 @@ +package io.papermc.paper.plugin; + +import io.papermc.paper.plugin.manager.PaperPluginManagerImpl; +import org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.junit.Assert; +import org.junit.Test; + +public class SyntheticEventTest { + + @Test + public void test() { + PaperTestPlugin paperTestPlugin = new PaperTestPlugin("synthetictest"); + PaperPluginManagerImpl paperPluginManager = new PaperPluginManagerImpl(Bukkit.getServer(), null, null); + + TestEvent event = new TestEvent(false); + Impl impl = new Impl(); + + paperPluginManager.registerEvents(impl, paperTestPlugin); + paperPluginManager.callEvent(event); + + Assert.assertEquals(1, impl.callCount); + } + + public abstract static class Base implements Listener { + int callCount = 0; + + public void accept(E evt) { + callCount++; + } + } + + public static class Impl extends Base { + @Override + @EventHandler + public void accept(TestEvent evt) { + super.accept(evt); + } + } +} diff --git a/src/test/java/io/papermc/paper/plugin/TestEvent.java b/src/test/java/io/papermc/paper/plugin/TestEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..04903794a8ee4dd73162ae240862ff6dc4cb4e24 --- /dev/null +++ b/src/test/java/io/papermc/paper/plugin/TestEvent.java @@ -0,0 +1,22 @@ +package io.papermc.paper.plugin; + + +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; + +public class TestEvent extends Event { + private static final HandlerList handlers = new HandlerList(); + + public TestEvent(boolean async) { + super(async); + } + + @Override + public HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } +} diff --git a/src/test/java/io/papermc/paper/plugin/TestJavaPluginProvider.java b/src/test/java/io/papermc/paper/plugin/TestJavaPluginProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..8837b131dedc41e7d0d58ccf3d1e251168c5b9b8 --- /dev/null +++ b/src/test/java/io/papermc/paper/plugin/TestJavaPluginProvider.java @@ -0,0 +1,42 @@ +package io.papermc.paper.plugin; + +import io.papermc.paper.plugin.provider.PluginProvider; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; +import java.util.jar.JarFile; +import java.util.logging.Logger; + +public class TestJavaPluginProvider implements PluginProvider { + + private final TestPluginMeta testPluginConfiguration; + + public TestJavaPluginProvider(TestPluginMeta testPluginConfiguration) { + this.testPluginConfiguration = testPluginConfiguration; + } + + @Override + public @NotNull Path getSource() { + return Path.of("dummy"); + } + + @Override + public JarFile file() { + throw new UnsupportedOperationException(); + } + + @Override + public PaperTestPlugin createInstance() { + return new PaperTestPlugin(this.testPluginConfiguration); + } + + @Override + public TestPluginMeta getMeta() { + return this.testPluginConfiguration; + } + + @Override + public Logger getLogger() { + return Logger.getLogger("TestPlugin"); + } +} diff --git a/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java b/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java new file mode 100644 index 0000000000000000000000000000000000000000..ba271c35eb2804f94cfc893bf94affb9ae13d3ba --- /dev/null +++ b/src/test/java/io/papermc/paper/plugin/TestPluginMeta.java @@ -0,0 +1,114 @@ +package io.papermc.paper.plugin; + +import io.papermc.paper.plugin.configuration.PluginMeta; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.bukkit.plugin.PluginLoadOrder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class TestPluginMeta implements PluginMeta { + + private final String identifier; + private List hardDependencies = List.of(); + private List softDependencies = List.of(); + private List loadBefore = List.of(); + + public TestPluginMeta(String identifier) { + this.identifier = identifier; + } + + @Override + public @NotNull String getName() { + return this.identifier; + } + + @Override + public @NotNull String getMainClass() { + return "null"; + } + + @Override + public @NotNull PluginLoadOrder getLoadOrder() { + return PluginLoadOrder.POSTWORLD; + } + + @Override + public @NotNull String getVersion() { + return "1.0"; + } + + @Override + public @Nullable String getLoggerPrefix() { + return this.identifier; + } + + public void setHardDependencies(List hardDependencies) { + this.hardDependencies = hardDependencies; + } + + @Override + public @NotNull List getPluginDependencies() { + return this.hardDependencies; + } + + public void setSoftDependencies(List softDependencies) { + this.softDependencies = softDependencies; + } + + @Override + public @NotNull List getPluginSoftDependencies() { + return this.softDependencies; + } + + public void setLoadBefore(List loadBefore) { + this.loadBefore = loadBefore; + } + + @Override + public @NotNull List getLoadBeforePlugins() { + return this.loadBefore; + } + + @Override + public @NotNull List getProvidedPlugins() { + return List.of(); + } + + @Override + public @NotNull List getAuthors() { + return List.of(); + } + + @Override + public @NotNull List getContributors() { + return List.of(); + } + + @Override + public @Nullable String getDescription() { + return "null"; + } + + @Override + public @Nullable String getWebsite() { + return "null"; + } + + @Override + public @NotNull List getPermissions() { + return List.of(); + } + + @Override + public @NotNull PermissionDefault getPermissionDefault() { + return PermissionDefault.TRUE; + } + + @Override + public @NotNull String getAPIVersion() { + return "null"; + } +} diff --git a/src/test/java/io/papermc/paper/testing/DummyServer.java b/src/test/java/io/papermc/paper/testing/DummyServer.java index 286790061c9d81c872108ef63e1a1aba2fbec809..53ac8df47a2c9a84c751955fd4fbb352a3ded16b 100644 --- a/src/test/java/io/papermc/paper/testing/DummyServer.java +++ b/src/test/java/io/papermc/paper/testing/DummyServer.java @@ -57,7 +57,7 @@ public final class DummyServer { return new LazyRegistry(() -> CraftRegistry.createRegistry(invocation.getArgument(0, Class.class), AbstractTestingBase.REGISTRY_CUSTOM)); }); - final PluginManager pluginManager = new SimplePluginManager(dummyServer, new SimpleCommandMap(dummyServer)); + final PluginManager pluginManager = new io.papermc.paper.plugin.manager.PaperPluginManagerImpl(dummyServer, new SimpleCommandMap(dummyServer), null); when(dummyServer.getPluginManager()).thenReturn(pluginManager); Bukkit.setServer(dummyServer);