From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Mon, 29 Feb 2016 21:02:09 -0600 Subject: [PATCH] Paper command diff --git a/src/main/java/com/destroystokyo/paper/PaperCommand.java b/src/main/java/com/destroystokyo/paper/PaperCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..b506cd11b76901827cbe66f46db8df400f7015de --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/PaperCommand.java @@ -0,0 +1,287 @@ +package com.destroystokyo.paper; + +import com.google.common.base.Functions; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.ChunkPos; +import org.apache.commons.lang3.tuple.MutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.entity.Player; + +import java.io.File; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +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; +import static net.kyori.adventure.text.format.NamedTextColor.YELLOW; + +public class PaperCommand extends Command { + private static final String BASE_PERM = "bukkit.command.paper."; + private static final ImmutableSet SUBCOMMANDS = ImmutableSet.builder().add("heap", "entity", "reload", "version").build(); + + public PaperCommand(String name) { + super(name); + this.description = "Paper related commands"; + this.usageMessage = "/paper [" + Joiner.on(" | ").join(SUBCOMMANDS) + "]"; + this.setPermission("bukkit.command.paper;" + Joiner.on(';').join(SUBCOMMANDS.stream().map(s -> BASE_PERM + s).collect(Collectors.toSet()))); + } + + private static boolean testPermission(CommandSender commandSender, String permission) { + if (commandSender.hasPermission(BASE_PERM + permission) || commandSender.hasPermission("bukkit.command.paper")) return true; + commandSender.sendMessage(text("I'm sorry, but you do not have permission to perform this command. Please contact the server administrators if you believe that this is in error.", RED)); + return false; + } + + @Override + public List tabComplete(CommandSender sender, String alias, String[] args, Location location) throws IllegalArgumentException { + if (args.length <= 1) + return getListMatchingLast(sender, args, SUBCOMMANDS); + + switch (args[0].toLowerCase(Locale.ENGLISH)) + { + case "entity": + if (args.length == 2) + return getListMatchingLast(sender, args, "help", "list"); + if (args.length == 3) + return getListMatchingLast(sender, args, Registry.ENTITY_TYPE.keySet().stream().map(ResourceLocation::toString).sorted().toArray(String[]::new)); + break; + } + return Collections.emptyList(); + } + + // Code from Mojang - copyright them + public static List getListMatchingLast(CommandSender sender, String[] args, String... matches) { + return getListMatchingLast(sender, args, (Collection) Arrays.asList(matches)); + } + + public static boolean matches(String s, String s1) { + return s1.regionMatches(true, 0, s, 0, s.length()); + } + + public static List getListMatchingLast(CommandSender sender, String[] strings, Collection collection) { + String last = strings[strings.length - 1]; + ArrayList results = Lists.newArrayList(); + + if (!collection.isEmpty()) { + Iterator iterator = Iterables.transform(collection, Functions.toStringFunction()).iterator(); + + while (iterator.hasNext()) { + String s1 = (String) iterator.next(); + + if (matches(last, s1) && (sender.hasPermission(BASE_PERM + s1) || sender.hasPermission("bukkit.command.paper"))) { + results.add(s1); + } + } + + if (results.isEmpty()) { + iterator = collection.iterator(); + + while (iterator.hasNext()) { + Object object = iterator.next(); + + if (object instanceof ResourceLocation && matches(last, ((ResourceLocation) object).getPath())) { + results.add(String.valueOf(object)); + } + } + } + } + + return results; + } + // end copy stuff + + @Override + public boolean execute(CommandSender sender, String commandLabel, String[] args) { + if (!testPermission(sender)) return true; + + if (args.length == 0) { + sender.sendMessage(text("Usage: " + this.usageMessage, RED)); + return false; + } + if (SUBCOMMANDS.contains(args[0].toLowerCase(Locale.ENGLISH))) { + if (!testPermission(sender, args[0].toLowerCase(Locale.ENGLISH))) return true; + } + switch (args[0].toLowerCase(Locale.ENGLISH)) { + case "heap": + dumpHeap(sender); + break; + case "entity": + listEntities(sender, args); + break; + case "reload": + doReload(sender); + break; + case "ver": + if (!testPermission(sender, "version")) break; // "ver" needs a special check because it's an alias. All other commands are checked up before the switch statement (because they are present in the SUBCOMMANDS set) + case "version": + Command ver = MinecraftServer.getServer().server.getCommandMap().getCommand("version"); + if (ver != null) { + ver.execute(sender, commandLabel, new String[0]); + break; + } + // else - fall through to default + default: + sender.sendMessage(text("Usage: " + this.usageMessage, RED)); + return false; + } + + return true; + } + + /* + * Ported from MinecraftForge - author: LexManos - License: LGPLv2.1 + */ + private void listEntities(CommandSender sender, String[] args) { + if (args.length < 2 || args[1].toLowerCase(Locale.ENGLISH).equals("help")) { + sender.sendMessage(text("Use /paper entity [list] help for more information on a specific command", RED)); + return; + } + + switch (args[1].toLowerCase(Locale.ENGLISH)) { + case "list": + String filter = "*"; + if (args.length > 2) { + if (args[2].toLowerCase(Locale.ENGLISH).equals("help")) { + sender.sendMessage(text("Use /paper entity list [filter] [worldName] to get entity info that matches the optional filter.", RED)); + return; + } + filter = args[2]; + } + final String cleanfilter = filter.replace("?", ".?").replace("*", ".*?"); + Set names = Registry.ENTITY_TYPE.keySet().stream() + .filter(n -> n.toString().matches(cleanfilter)) + .collect(Collectors.toSet()); + + if (names.isEmpty()) { + sender.sendMessage(text("Invalid filter, does not match any entities. Use /paper entity list for a proper list", RED)); + sender.sendMessage(text("Usage: /paper entity list [filter] [worldName]", RED)); + return; + } + + String worldName; + if (args.length > 3) { + worldName = args[3]; + } else if (sender instanceof Player) { + worldName = ((Player) sender).getWorld().getName(); + } else { + sender.sendMessage(text("Please specify the name of a world", RED)); + sender.sendMessage(text("To do so without a filter, specify '*' as the filter", RED)); + sender.sendMessage(text("Usage: /paper entity list [filter] [worldName]", RED)); + return; + } + + Map>> list = Maps.newHashMap(); + World bukkitWorld = Bukkit.getWorld(worldName); + if (bukkitWorld == null) { + sender.sendMessage(text("Could not load world for " + worldName + ". Please select a valid world.", RED)); + sender.sendMessage(text("Usage: /paper entity list [filter] [worldName]", RED)); + return; + } + ServerLevel world = ((CraftWorld) Bukkit.getWorld(worldName)).getHandle(); + + Map nonEntityTicking = Maps.newHashMap(); + ServerChunkCache chunkProviderServer = world.getChunkSource(); + + world.getAllEntities().forEach(e -> { + ResourceLocation key = EntityType.getKey(e.getType()); + + MutablePair> info = list.computeIfAbsent(key, k -> MutablePair.of(0, Maps.newHashMap())); + ChunkPos chunk = e.chunkPosition(); + info.left++; + info.right.put(chunk, info.right.getOrDefault(chunk, 0) + 1); + if (!chunkProviderServer.isPositionTicking(e)) { + nonEntityTicking.merge(key, Integer.valueOf(1), Integer::sum); + } + }); + + if (names.size() == 1) { + ResourceLocation name = names.iterator().next(); + Pair> info = list.get(name); + int nonTicking = nonEntityTicking.getOrDefault(name, Integer.valueOf(0)).intValue(); + if (info == null) { + sender.sendMessage(text("No entities found.", RED)); + return; + } + sender.sendMessage("Entity: " + name + " Total Ticking: " + (info.getLeft() - nonTicking) + ", Total Non-Ticking: " + nonTicking); + info.getRight().entrySet().stream() + .sorted((a, b) -> !a.getValue().equals(b.getValue()) ? b.getValue() - a.getValue() : a.getKey().toString().compareTo(b.getKey().toString())) + .limit(10).forEach(e -> sender.sendMessage(" " + e.getValue() + ": " + e.getKey().x + ", " + e.getKey().z + (chunkProviderServer.isPositionTicking(e.getKey().toLong()) ? " (Ticking)" : " (Non-Ticking)"))); + } else { + List> info = list.entrySet().stream() + .filter(e -> names.contains(e.getKey())) + .map(e -> Pair.of(e.getKey(), e.getValue().left)) + .sorted((a, b) -> !a.getRight().equals(b.getRight()) ? b.getRight() - a.getRight() : a.getKey().toString().compareTo(b.getKey().toString())) + .collect(Collectors.toList()); + + if (info == null || info.size() == 0) { + sender.sendMessage(text("No entities found.", RED)); + return; + } + + int count = info.stream().mapToInt(Pair::getRight).sum(); + int nonTickingCount = nonEntityTicking.values().stream().mapToInt(Integer::intValue).sum(); + sender.sendMessage("Total Ticking: " + (count - nonTickingCount) + ", Total Non-Ticking: " + nonTickingCount); + info.forEach(e -> { + int nonTicking = nonEntityTicking.getOrDefault(e.getKey(), Integer.valueOf(0)).intValue(); + sender.sendMessage(" " + (e.getValue() - nonTicking) + " (" + nonTicking + ") " + ": " + e.getKey()); + }); + sender.sendMessage("* First number is ticking entities, second number is non-ticking entities"); + } + break; + } + } + + private void dumpHeap(CommandSender sender) { + java.nio.file.Path dir = java.nio.file.Paths.get("./dumps"); + String name = "heap-dump-" + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH.mm.ss").format(LocalDateTime.now()); + + Command.broadcastCommandMessage(sender, text("Writing JVM heap data...", YELLOW)); + + java.nio.file.Path file = CraftServer.dumpHeap(dir, name); + if (file != null) { + Command.broadcastCommandMessage(sender, text("Heap dump saved to " + file, GREEN)); + } else { + Command.broadcastCommandMessage(sender, text("Failed to write heap dump, see server log for details", RED)); + } + } + + private void doReload(CommandSender sender) { + Command.broadcastCommandMessage(sender, text("Please note that this command is not supported and may cause issues.", RED)); + Command.broadcastCommandMessage(sender, text("If you encounter any issues please use the /stop command to restart your server.", RED)); + + MinecraftServer server = ((CraftServer) sender.getServer()).getServer(); + server.paperConfigurations.reloadConfigs(server); + server.server.reloadCount++; + + Command.broadcastCommandMessage(sender, text("Paper config reload complete.", GREEN)); + } +} diff --git a/src/main/java/io/papermc/paper/command/PaperCommands.java b/src/main/java/io/papermc/paper/command/PaperCommands.java new file mode 100644 index 0000000000000000000000000000000000000000..82bce89dc174c8c4a4ab8efeffaaf22ddeb377fb --- /dev/null +++ b/src/main/java/io/papermc/paper/command/PaperCommands.java @@ -0,0 +1,25 @@ +package io.papermc.paper.command; + +import com.destroystokyo.paper.PaperCommand; +import net.minecraft.server.MinecraftServer; +import org.bukkit.command.Command; + +import java.util.HashMap; +import java.util.Map; + +public final class PaperCommands { + + private PaperCommands() { + } + + private static final Map COMMANDS = new HashMap<>(); + static { + COMMANDS.put("paper", new PaperCommand("paper")); + } + + public static void registerCommands(final MinecraftServer server) { + COMMANDS.forEach((s, command) -> { + server.server.getCommandMap().register(s, "Paper", command); + }); + } +} diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java index e476f93547f386ded0174693a6218d793ccc450b..393e465b0bac55d407f2ec66d7b11ed0537c9641 100644 --- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java +++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java @@ -191,6 +191,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface // Paper start paperConfigurations.initializeGlobalConfiguration(); paperConfigurations.initializeWorldDefaultsConfiguration(); + io.papermc.paper.command.PaperCommands.registerCommands(this); // Paper end this.setPvpAllowed(dedicatedserverproperties.pvp); diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java index f9ddb9835cad1b76eecd639dff8fa29472954421..268715e19a6fa7d26510d9d0a8f19b158835b279 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -2376,6 +2376,34 @@ public final class CraftServer implements Server { // Spigot end // Paper start + @SuppressWarnings({"rawtypes", "unchecked"}) + public static java.nio.file.Path dumpHeap(java.nio.file.Path dir, String name) { + try { + java.nio.file.Files.createDirectories(dir); + + javax.management.MBeanServer server = java.lang.management.ManagementFactory.getPlatformMBeanServer(); + java.nio.file.Path file; + + try { + Class clazz = Class.forName("openj9.lang.management.OpenJ9DiagnosticsMXBean"); + Object openj9Mbean = java.lang.management.ManagementFactory.newPlatformMXBeanProxy(server, "openj9.lang.management:type=OpenJ9Diagnostics", clazz); + java.lang.reflect.Method m = clazz.getMethod("triggerDumpToFile", String.class, String.class); + file = dir.resolve(name + ".phd"); + m.invoke(openj9Mbean, "heap", file.toString()); + } catch (ClassNotFoundException e) { + Class clazz = Class.forName("com.sun.management.HotSpotDiagnosticMXBean"); + Object hotspotMBean = java.lang.management.ManagementFactory.newPlatformMXBeanProxy(server, "com.sun.management:type=HotSpotDiagnostic", clazz); + java.lang.reflect.Method m = clazz.getMethod("dumpHeap", String.class, boolean.class); + file = dir.resolve(name + ".hprof"); + m.invoke(hotspotMBean, file.toString(), true); + } + + return file; + } catch (Throwable t) { + Bukkit.getLogger().log(Level.SEVERE, "Could not write heap", t); + return null; + } + } private Iterable adventure$audiences; @Override public Iterable audiences() {