@@ -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())))
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user