From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Warrior <50800980+Warriorrrr@users.noreply.github.com> Date: Tue, 25 Oct 2022 21:15:37 +0200 Subject: [PATCH] Add /paper dumplisteners command Co-authored-by: TwoLeggedCat <80929284+TwoLeggedCat@users.noreply.github.com> diff --git a/src/main/java/io/papermc/paper/command/PaperCommand.java b/src/main/java/io/papermc/paper/command/PaperCommand.java index 724592234e2a178a518f6ab7d09c3180780371a7..92154550b41b2e1d03deb1271b71bb3baa735e0a 100644 --- a/src/main/java/io/papermc/paper/command/PaperCommand.java +++ b/src/main/java/io/papermc/paper/command/PaperCommand.java @@ -2,6 +2,7 @@ package io.papermc.paper.command; import io.papermc.paper.command.subcommands.ChunkDebugCommand; import io.papermc.paper.command.subcommands.DumpItemCommand; +import io.papermc.paper.command.subcommands.DumpListenersCommand; import io.papermc.paper.command.subcommands.EntityCommand; import io.papermc.paper.command.subcommands.FixLightCommand; import io.papermc.paper.command.subcommands.HeapDumpCommand; @@ -50,6 +51,7 @@ public final class PaperCommand extends Command { commands.put(Set.of("syncloadinfo"), new SyncLoadInfoCommand()); commands.put(Set.of("dumpitem"), new DumpItemCommand()); commands.put(Set.of("mobcaps", "playermobcaps"), new MobcapsCommand()); + commands.put(Set.of("dumplisteners"), new DumpListenersCommand()); 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/subcommands/DumpListenersCommand.java b/src/main/java/io/papermc/paper/command/subcommands/DumpListenersCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..dab8d6fb489f8b3acc7c8fdaa1b5b6b83fa0eeb4 --- /dev/null +++ b/src/main/java/io/papermc/paper/command/subcommands/DumpListenersCommand.java @@ -0,0 +1,120 @@ +package io.papermc.paper.command.subcommands; + +import com.destroystokyo.paper.util.SneakyThrow; +import io.papermc.paper.command.PaperSubcommand; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Stream; +import net.minecraft.server.MinecraftServer; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.event.HandlerList; +import org.bukkit.plugin.Plugin; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +import static net.kyori.adventure.text.Component.space; +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.GRAY; +import static net.kyori.adventure.text.format.NamedTextColor.GREEN; +import static net.kyori.adventure.text.format.NamedTextColor.RED; + +@DefaultQualifier(NonNull.class) +public final class DumpListenersCommand implements PaperSubcommand { + private static final MethodHandle EVENT_TYPES_HANDLE; + + static { + try { + final Field eventTypesField = HandlerList.class.getDeclaredField("EVENT_TYPES"); + eventTypesField.setAccessible(true); + EVENT_TYPES_HANDLE = MethodHandles.lookup().unreflectGetter(eventTypesField); + } catch (final ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { + this.doDumpListeners(sender, args); + return true; + } + + private void doDumpListeners(final CommandSender sender, final String[] args) { + if (args.length == 0) { + sender.sendMessage(text("Usage: /paper dumplisteners [className]", RED)); + return; + } + + try { + final HandlerList handlers = (HandlerList) findClass(args[0]).getMethod("getHandlerList").invoke(null); + + if (handlers.getRegisteredListeners().length == 0) { + sender.sendMessage(text(args[0] + " does not have any registered listeners.")); + return; + } + + sender.sendMessage(text("Listeners for " + args[0] + ":")); + + Stream.of(handlers.getRegisteredListeners()) + .map(listener -> text(listener.getPlugin().getName(), GREEN) + .append(space()) + .append(text("(" + listener.getListener().getClass().getName() + ")", GRAY) + .hoverEvent(text("Priority: " + listener.getPriority().name() + " (" + listener.getPriority().getSlot() + ")", GRAY)))) + .forEach(sender::sendMessage); + + sender.sendMessage(text("Total listeners: " + handlers.getRegisteredListeners().length)); + + } catch (final ClassNotFoundException e) { + sender.sendMessage(text("Unable to find a class named '" + args[0] + "'. Make sure to use the fully qualified name.", RED)); + } catch (final NoSuchMethodException e) { + sender.sendMessage(text("Class '" + args[0] + "' does not have a valid getHandlerList method.", RED)); + } catch (final ReflectiveOperationException e) { + sender.sendMessage(text("Something went wrong, see the console for more details.", RED)); + MinecraftServer.LOGGER.warn("Error occurred while dumping listeners for class " + args[0], e); + } + } + + @Override + public List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { + return switch (args.length) { + case 0 -> List.copyOf(getEventClasses()); + case 1 -> getEventClasses().stream() + .filter(clazz -> clazz.toLowerCase(Locale.ROOT).contains(args[0].toLowerCase(Locale.ROOT))) + .toList(); + default -> Collections.emptyList(); + }; + } + + @SuppressWarnings("unchecked") + private static Set getEventClasses() { + try { + return (Set) EVENT_TYPES_HANDLE.invokeExact(); + } catch (final Throwable e) { + SneakyThrow.sneaky(e); + return Collections.emptySet(); // Unreachable + } + } + + private static Class findClass(final String className) throws ClassNotFoundException { + try { + return Class.forName(className); + } catch (final ClassNotFoundException ignore) { + for (final Plugin plugin : Bukkit.getServer().getPluginManager().getPlugins()) { + if (!plugin.isEnabled()) { + continue; + } + + try { + return Class.forName(className, false, plugin.getClass().getClassLoader()); + } catch (final ClassNotFoundException ignore0) { + } + } + } + throw new ClassNotFoundException(className); + } +}