Paper Plugins

Co-authored-by: Micah Rao <micah.s.rao@gmail.com>
This commit is contained in:
Owen1212055
2022-07-06 23:00:31 -04:00
parent 329dfdabdc
commit 216388dfdf
103 changed files with 7450 additions and 42 deletions

View File

@@ -37,6 +37,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())))

View File

@@ -24,5 +24,6 @@ public final class PaperCommands {
COMMANDS.forEach((s, command) -> {
server.server.getCommandMap().register(s, "Paper", command);
});
server.server.getCommandMap().register("bukkit", new PaperPluginsCommand());
}
}

View File

@@ -0,0 +1,215 @@
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.
<link to 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<PluginProvider<JavaPlugin>>() {}.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 <T> List<Component> formatProviders(TreeMap<String, PluginProvider<T>> plugins) {
List<Component> components = new ArrayList<>(plugins.size());
for (PluginProvider<T> entry : plugins.values()) {
components.add(formatProvider(entry));
}
boolean isFirst = true;
List<Component> 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<Component> 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<String, PluginProvider<JavaPlugin>> paperPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
TreeMap<String, PluginProvider<JavaPlugin>> spigotPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (PluginProvider<JavaPlugin> 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);
if (!paperPlugins.isEmpty()) {
sender.sendMessage(PAPER_HEADER);
}
for (Component component : formatProviders(paperPlugins)) {
sender.sendMessage(component);
}
if (!spigotPlugins.isEmpty()) {
sender.sendMessage(BUKKIT_HEADER);
}
for (Component component : formatProviders(spigotPlugins)) {
sender.sendMessage(component);
}
return true;
}
@NotNull
@Override
public List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException {
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,203 @@
package io.papermc.paper.command.subcommands;
import com.google.common.graph.GraphBuilder;
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.entrypoint.classloader.group.SpigotPluginClassLoaderGroup;
import io.papermc.paper.plugin.entrypoint.classloader.group.StaticPluginClassLoaderGroup;
import io.papermc.paper.plugin.entrypoint.dependency.GraphDependencyContext;
import io.papermc.paper.plugin.entrypoint.dependency.SimpleMetaDependencyTree;
import io.papermc.paper.plugin.provider.entrypoint.DependencyContext;
import io.papermc.paper.plugin.entrypoint.strategy.modern.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<Entrypoint<?>, ProviderStorage<?>> entry : LaunchEntryPointHandler.INSTANCE.getStorage().entrySet()) {
JsonObject entrypoint = new JsonObject();
JsonArray providers = new JsonArray();
entrypoint.add("providers", providers);
List<PluginProvider<Object>> 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<Object>) provider);
}
JsonArray loadOrder = new JsonArray();
entrypoint.add("load-order", loadOrder);
ModernPluginLoadingStrategy<Object> modernPluginLoadingStrategy = new ModernPluginLoadingStrategy<>(new ProviderConfiguration<>() {
@Override
public void applyContext(PluginProvider<Object> provider, DependencyContext dependencyContext) {
}
@Override
public boolean load(PluginProvider<Object> provider, Object provided) {
return true;
}
@Override
public boolean preloadProvider(PluginProvider<Object> provider) {
// Don't load provider
loadOrder.add(provider.getMeta().getName());
return false;
}
});
modernPluginLoadingStrategy.loadProviders(pluginProviders, new SimpleMetaDependencyTree(GraphBuilder.directed().build()));
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());
if (group instanceof StaticPluginClassLoaderGroup staticPluginClassLoaderGroup) {
classLoadersRoot.addProperty("plugin-holder", staticPluginClassLoaderGroup.getPluginClassloader().toString());
} else if (group instanceof SpigotPluginClassLoaderGroup spigotPluginClassLoaderGroup) {
classLoadersRoot.addProperty("plugin-holder", spigotPluginClassLoaderGroup.getPluginClassLoader().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());
}
}