Add `/ess dump` command to generate a debug dump output (#4361)

Co-authored-by: MD <1917406+mdcfe@users.noreply.github.com>

Command usage: /essentials dump [config] [discord] [kits] [log]

Either of the optional args can be used to add the given data to the dump.

Related: EssentialsX/Website#51
This commit is contained in:
Josh Roy 2021-08-19 12:35:19 -07:00 committed by GitHub
parent 1179caab88
commit 3692740762
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 360 additions and 62 deletions

View File

@ -7,6 +7,7 @@ import org.bukkit.Material;
import org.bukkit.event.EventPriority;
import org.spongepowered.configurate.CommentedConfigurationNode;
import java.io.File;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.util.List;
@ -17,6 +18,8 @@ import java.util.function.Predicate;
import java.util.regex.Pattern;
public interface ISettings extends IConf {
File getConfigFile();
boolean areSignsDisabled();
IText getAnnounceNewPlayerFormat();

View File

@ -31,6 +31,10 @@ public class Kits implements IConf {
kits = _getKits();
}
public File getFile() {
return config.getFile();
}
private CommentedConfigurationNode _getKits() {
final CommentedConfigurationNode section = config.getSection("kits");
if (section != null) {

View File

@ -145,6 +145,11 @@ public class Settings implements net.ess3.api.ISettings {
reloadConfig();
}
@Override
public File getConfigFile() {
return config.getFile();
}
@Override
public boolean getRespawnAtHome() {
return config.getBoolean("respawn-at-home", false);

View File

@ -3,11 +3,7 @@ package com.earth2me.essentials.commands;
import com.earth2me.essentials.CommandSource;
import com.earth2me.essentials.User;
import com.earth2me.essentials.utils.DateUtil;
import com.google.common.base.Charsets;
import com.google.common.io.CharStreams;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.earth2me.essentials.utils.PasteUtil;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.inventory.ItemStack;
@ -17,25 +13,16 @@ import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import static com.earth2me.essentials.I18n.tl;
public class Commandcreatekit extends EssentialsCommand {
private static final String PASTE_URL = "https://paste.gg/";
private static final String PASTE_UPLOAD_URL = "https://api.paste.gg/v1/pastes";
private static final Gson GSON = new Gson();
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
public Commandcreatekit() {
super("createkit");
}
@ -81,7 +68,7 @@ public class Commandcreatekit extends EssentialsCommand {
}
private void uploadPaste(final CommandSource sender, final String kitName, final long delay, final List<String> list) {
executorService.submit(() -> {
ess.runTaskAsynchronously(() -> {
try {
final StringWriter sw = new StringWriter();
final YamlConfigurationLoader loader = YamlConfigurationLoader.builder().sink(() -> new BufferedWriter(sw)).indent(2).nodeStyle(NodeStyle.BLOCK).build();
@ -95,52 +82,27 @@ public class Commandcreatekit extends EssentialsCommand {
final String fileContents = sw.toString();
final HttpURLConnection connection = (HttpURLConnection) new URL(PASTE_UPLOAD_URL).openConnection();
connection.setRequestMethod("POST");
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setRequestProperty("User-Agent", "EssentialsX plugin");
connection.setRequestProperty("Content-Type", "application/json");
final JsonObject body = new JsonObject();
final JsonArray files = new JsonArray();
final JsonObject file = new JsonObject();
final JsonObject content = new JsonObject();
content.addProperty("format", "text");
content.addProperty("value", fileContents);
file.add("content", content);
files.add(file);
body.add("files", files);
try (final OutputStream os = connection.getOutputStream()) {
os.write(body.toString().getBytes(Charsets.UTF_8));
}
// Error
if (connection.getResponseCode() >= 400) {
final CompletableFuture<PasteUtil.PasteResult> future = PasteUtil.createPaste(Collections.singletonList(new PasteUtil.PasteFile("kit_" + kitName + ".yml", fileContents)));
future.thenAccept(result -> {
if (result != null) {
final String separator = tl("createKitSeparator");
final String delayFormat = delay <= 0 ? "0" : DateUtil.formatDateDiff(System.currentTimeMillis() + (delay * 1000));
sender.sendMessage(separator);
sender.sendMessage(tl("createKitSuccess", kitName, delayFormat, result.getPasteUrl()));
sender.sendMessage(separator);
if (ess.getSettings().isDebug()) {
ess.getLogger().info(sender.getSender().getName() + " created a kit: " + result.getPasteUrl());
}
}
});
future.exceptionally(throwable -> {
sender.sendMessage(tl("createKitFailed", kitName));
final String message = CharStreams.toString(new InputStreamReader(connection.getErrorStream(), Charsets.UTF_8));
ess.getLogger().severe("Error creating kit: " + message);
return;
}
// Read URL
final JsonObject object = GSON.fromJson(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8), JsonObject.class);
final String pasteUrl = PASTE_URL + object.get("result").getAsJsonObject().get("id").getAsString();
connection.disconnect();
final String separator = tl("createKitSeparator");
String delayFormat = "0";
if (delay > 0) {
delayFormat = DateUtil.formatDateDiff(System.currentTimeMillis() + (delay * 1000));
}
sender.sendMessage(separator);
sender.sendMessage(tl("createKitSuccess", kitName, delayFormat, pasteUrl));
sender.sendMessage(separator);
if (ess.getSettings().isDebug()) {
ess.getLogger().info(sender.getSender().getName() + " created a kit: " + pasteUrl);
}
} catch (final Exception e) {
ess.getLogger().log(Level.SEVERE, "Error creating kit: ", throwable);
return null;
});
} catch (Exception e) {
sender.sendMessage(tl("createKitFailed", kitName));
e.printStackTrace();
ess.getLogger().log(Level.SEVERE, "Error creating kit: ", e);
}
});
}

View File

@ -10,10 +10,14 @@ import com.earth2me.essentials.utils.DateUtil;
import com.earth2me.essentials.utils.EnumUtil;
import com.earth2me.essentials.utils.FloatUtil;
import com.earth2me.essentials.utils.NumberUtil;
import com.earth2me.essentials.utils.PasteUtil;
import com.earth2me.essentials.utils.VersionUtil;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.Server;
@ -25,13 +29,22 @@ import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginManager;
import org.bukkit.scheduler.BukkitRunnable;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -103,6 +116,9 @@ public class Commandessentials extends EssentialsCommand {
case "commands":
runCommands(server, sender, commandLabel, args);
break;
case "dump":
runDump(server, sender, commandLabel, args);
break;
// Data commands
case "reload":
@ -156,6 +172,168 @@ public class Commandessentials extends EssentialsCommand {
}
}
// Generates a paste of useful information
private void runDump(Server server, CommandSource sender, String commandLabel, String[] args) {
sender.sendMessage(tl("dumpCreating"));
final JsonObject dump = new JsonObject();
final JsonObject meta = new JsonObject();
meta.addProperty("timestamp", Instant.now().toEpochMilli());
meta.addProperty("sender", sender.getPlayer() != null ? sender.getPlayer().getName() : null);
meta.addProperty("senderUuid", sender.getPlayer() != null ? sender.getPlayer().getUniqueId().toString() : null);
dump.add("meta", meta);
final JsonObject serverData = new JsonObject();
serverData.addProperty("bukkit-version", Bukkit.getBukkitVersion());
serverData.addProperty("server-version", Bukkit.getVersion());
serverData.addProperty("server-brand", Bukkit.getName());
final JsonObject supportStatus = new JsonObject();
final VersionUtil.SupportStatus status = VersionUtil.getServerSupportStatus();
supportStatus.addProperty("status", status.name());
supportStatus.addProperty("supported", status.isSupported());
supportStatus.addProperty("trigger", VersionUtil.getSupportStatusClass());
serverData.add("support-status", supportStatus);
dump.add("server-data", serverData);
final JsonObject environment = new JsonObject();
environment.addProperty("java-version", System.getProperty("java.version"));
environment.addProperty("operating-system", System.getProperty("os.name"));
environment.addProperty("uptime", DateUtil.formatDateDiff(ManagementFactory.getRuntimeMXBean().getStartTime()));
environment.addProperty("allocated-memory", (Runtime.getRuntime().totalMemory() / 1024 / 1024) + "MB");
dump.add("environment", environment);
final JsonObject essData = new JsonObject();
essData.addProperty("version", ess.getDescription().getVersion());
final JsonObject updateData = new JsonObject();
updateData.addProperty("id", ess.getUpdateChecker().getVersionIdentifier());
updateData.addProperty("branch", ess.getUpdateChecker().getVersionBranch());
updateData.addProperty("dev", ess.getUpdateChecker().isDevBuild());
essData.add("update-data", updateData);
final JsonObject econLayer = new JsonObject();
econLayer.addProperty("enabled", !ess.getSettings().isEcoDisabled());
econLayer.addProperty("selected-layer", EconomyLayers.isLayerSelected());
final EconomyLayer layer = EconomyLayers.getSelectedLayer();
econLayer.addProperty("name", layer == null ? "null" : layer.getName());
econLayer.addProperty("layer-version", layer == null ? "null" : layer.getPluginVersion());
econLayer.addProperty("backend-name", layer == null ? "null" : layer.getBackendName());
essData.add("economy-layer", econLayer);
final JsonArray addons = new JsonArray();
final JsonArray plugins = new JsonArray();
final ArrayList<Plugin> alphabetical = new ArrayList<>();
Collections.addAll(alphabetical, Bukkit.getPluginManager().getPlugins());
alphabetical.sort(Comparator.comparing(o -> o.getName().toUpperCase(Locale.ENGLISH)));
for (final Plugin plugin : alphabetical) {
final JsonObject pluginData = new JsonObject();
final PluginDescriptionFile info = plugin.getDescription();
final String name = info.getName();
pluginData.addProperty("name", name);
pluginData.addProperty("version", info.getVersion());
pluginData.addProperty("description", info.getDescription());
pluginData.addProperty("main", info.getMain());
pluginData.addProperty("enabled", plugin.isEnabled());
pluginData.addProperty("official", plugin == ess || officialPlugins.contains(name));
pluginData.addProperty("unsupported", warnPlugins.contains(name));
final JsonArray authors = new JsonArray();
info.getAuthors().forEach(authors::add);
pluginData.add("authors", authors);
if (name.startsWith("Essentials") && !name.equals("Essentials")) {
addons.add(pluginData);
}
plugins.add(pluginData);
}
essData.add("addons", addons);
dump.add("ess-data", essData);
dump.add("plugins", plugins);
final List<PasteUtil.PasteFile> files = new ArrayList<>();
files.add(new PasteUtil.PasteFile("dump.json", dump.toString()));
final Plugin essDiscord = Bukkit.getPluginManager().getPlugin("EssentialsDiscord");
// Further operations will be heavy IO
ess.runTaskAsynchronously(() -> {
boolean config = false;
boolean discord = false;
boolean kits = false;
boolean log = false;
for (final String arg : args) {
if (arg.equals("*")) {
config = true;
discord = true;
kits = true;
log = true;
break;
} else if (arg.equalsIgnoreCase("config")) {
config = true;
} else if (arg.equalsIgnoreCase("discord")) {
discord = true;
} else if (arg.equalsIgnoreCase("kits")) {
kits = true;
} else if (arg.equalsIgnoreCase("log")) {
log = true;
}
}
if (config) {
try {
files.add(new PasteUtil.PasteFile("config.yml", new String(Files.readAllBytes(ess.getSettings().getConfigFile().toPath()), StandardCharsets.UTF_8)));
} catch (IOException e) {
sender.sendMessage(tl("dumpErrorUpload", "config.yml", e.getMessage()));
}
}
if (discord && essDiscord != null && essDiscord.isEnabled()) {
try {
files.add(new PasteUtil.PasteFile("discord-config.yml",
new String(Files.readAllBytes(essDiscord.getDataFolder().toPath().resolve("config.yml")), StandardCharsets.UTF_8)
.replaceAll("[MN][A-Za-z\\d]{23}\\.[\\w-]{6}\\.[\\w-]{27}", "<censored token>")));
} catch (IOException e) {
sender.sendMessage(tl("dumpErrorUpload", "discord-config.yml", e.getMessage()));
}
}
if (kits) {
try {
files.add(new PasteUtil.PasteFile("kits.yml", new String(Files.readAllBytes(ess.getKits().getFile().toPath()), StandardCharsets.UTF_8)));
} catch (IOException e) {
sender.sendMessage(tl("dumpErrorUpload", "kits.yml", e.getMessage()));
}
}
if (log) {
try {
files.add(new PasteUtil.PasteFile("latest.log", new String(Files.readAllBytes(Paths.get("logs", "latest.log")), StandardCharsets.UTF_8)
.replaceAll("(?m)^\\[\\d\\d:\\d\\d:\\d\\d] \\[.+/(?:DEBUG|TRACE)]: .+\\s(?:[A-Za-z.]+:.+\\s(?:\\t.+\\s)*)?\\s*(?:\"[A-Za-z]+\" : .+[\\s}\\]]+)*", "")
.replaceAll("(?:[0-9]{1,3}\\.){3}[0-9]{1,3}", "<censored ip address>")));
} catch (IOException e) {
sender.sendMessage(tl("dumpErrorUpload", "latest.log", e.getMessage()));
}
}
final CompletableFuture<PasteUtil.PasteResult> future = PasteUtil.createPaste(files);
future.thenAccept(result -> {
if (result != null) {
final String dumpUrl = "https://essentialsx.net/dump.html?id=" + result.getPasteId();
sender.sendMessage(tl("dumpUrl", dumpUrl));
sender.sendMessage(tl("dumpDeleteKey", result.getDeletionKey()));
if (sender.isPlayer()) {
ess.getLogger().info(tl("dumpConsoleUrl", dumpUrl));
ess.getLogger().info(tl("dumpDeleteKey", result.getDeletionKey()));
}
}
files.clear();
});
future.exceptionally(throwable -> {
sender.sendMessage(tl("dumpError", throwable.getMessage()));
return null;
});
});
}
// Resets the given player's user data.
private void runReset(final Server server, final CommandSource sender, final String commandLabel, final String[] args) throws Exception {
if (args.length < 2) {
@ -491,6 +669,7 @@ public class Commandessentials extends EssentialsCommand {
final List<String> options = Lists.newArrayList();
options.add("reload");
options.add("version");
options.add("dump");
options.add("commands");
options.add("debug");
options.add("reset");
@ -534,6 +713,16 @@ public class Commandessentials extends EssentialsCommand {
return Lists.newArrayList("ignoreUFCache");
}
break;
case "dump":
final List<String> list = Lists.newArrayList("config", "kits", "log", "discord", "*");
for (String arg : args) {
if (arg.equals("*")) {
list.clear();
return list;
}
list.remove(arg.toLowerCase(Locale.ENGLISH));
}
return list;
}
return Collections.emptyList();

View File

@ -0,0 +1,127 @@
package com.earth2me.essentials.utils;
import com.google.common.base.Charsets;
import com.google.common.io.CharStreams;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public final class PasteUtil {
private static final String PASTE_URL = "https://paste.gg/";
private static final String PASTE_UPLOAD_URL = "https://api.paste.gg/v1/pastes";
private static final ExecutorService PASTE_EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
private static final Gson GSON = new Gson();
private PasteUtil() {
}
/**
* Creates an anonymous paste containing the provided files.
*
* @param pages The files to include in the paste.
* @return The result of the paste, including the paste URL and deletion key.
*/
public static CompletableFuture<PasteResult> createPaste(List<PasteFile> pages) {
final CompletableFuture<PasteResult> future = new CompletableFuture<>();
PASTE_EXECUTOR_SERVICE.submit(() -> {
try {
final HttpURLConnection connection = (HttpURLConnection) new URL(PASTE_UPLOAD_URL).openConnection();
connection.setRequestMethod("POST");
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setRequestProperty("User-Agent", "EssentialsX plugin");
connection.setRequestProperty("Content-Type", "application/json");
final JsonObject body = new JsonObject();
final JsonArray files = new JsonArray();
for (final PasteFile page : pages) {
final JsonObject file = new JsonObject();
final JsonObject content = new JsonObject();
file.addProperty("name", page.getName());
content.addProperty("format", "text");
content.addProperty("value", page.getContents());
file.add("content", content);
files.add(file);
}
body.add("files", files);
try (final OutputStream os = connection.getOutputStream()) {
os.write(body.toString().getBytes(Charsets.UTF_8));
}
if (connection.getResponseCode() >= 400) {
//noinspection UnstableApiUsage
future.completeExceptionally(new Error(CharStreams.toString(new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8))));
return;
}
// Read URL
final JsonObject object = GSON.fromJson(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8), JsonObject.class);
final String pasteId = object.get("result").getAsJsonObject().get("id").getAsString();
final String pasteUrl = PASTE_URL + pasteId;
final JsonElement deletionKey = object.get("result").getAsJsonObject().get("deletion_key");
connection.disconnect();
final PasteResult result = new PasteResult(pasteId, pasteUrl, deletionKey != null ? deletionKey.getAsString() : null);
future.complete(result);
} catch (Exception e) {
future.completeExceptionally(e);
}
});
return future;
}
public static class PasteFile {
private final String name;
private final String contents;
public PasteFile(final String name, final String contents) {
this.name = name;
this.contents = contents;
}
public String getName() {
return name;
}
public String getContents() {
return contents;
}
}
public static class PasteResult {
private final String pasteId;
private final String pasteUrl;
private final @Nullable String deletionKey;
protected PasteResult(String pasteId, final String pasteUrl, final @Nullable String deletionKey) {
this.pasteId = pasteId;
this.pasteUrl = pasteUrl;
this.deletionKey = deletionKey;
}
public String getPasteUrl() {
return pasteUrl;
}
public @Nullable String getDeletionKey() {
return deletionKey;
}
public String getPasteId() {
return pasteId;
}
}
}

View File

@ -260,6 +260,12 @@ disposalCommandUsage=/<command>
distance=\u00a76Distance\: {0}
dontMoveMessage=\u00a76Teleportation will commence in\u00a7c {0}\u00a76. Don''t move.
downloadingGeoIp=Downloading GeoIP database... this might take a while (country\: 1.7 MB, city\: 30MB)
dumpConsoleUrl=A server dump was created: \u00a7c{0}
dumpCreating=\u00a76Creating server dump...
dumpDeleteKey=\u00a76If you want to delete this dump at a later date, use the following deletion key: \u00a7c{0}
dumpError=\u00a74Error while creating dump \u00a7c{0}\u00a74.
dumpErrorUpload=\u00a74Error while uploading \u00a7c{0}\u00a74: \u00a7c{1}
dumpUrl=\u00a76Created server dump: \u00a7c{0}
duplicatedUserdata=Duplicated userdata\: {0} and {1}.
durability=\u00a76This tool has \u00a7c{0}\u00a76 uses left.
east=E
@ -309,6 +315,8 @@ essentialsCommandUsage6=/<command> cleanup
essentialsCommandUsage6Description=Cleans up old userdata
essentialsCommandUsage7=/<command> homes
essentialsCommandUsage7Description=Manages user homes
essentialsCommandUsage8=/<command> dump [*] [config] [discord] [kits] [log]
essentialsCommandUsage8Description=Generates a server dump with the requested information
essentialsHelp1=The file is broken and Essentials can''t open it. Essentials is now disabled. If you can''t fix the file yourself, go to http\://tiny.cc/EssentialsChat
essentialsHelp2=The file is broken and Essentials can''t open it. Essentials is now disabled. If you can''t fix the file yourself, either type /essentialshelp in game or go to http\://tiny.cc/EssentialsChat
essentialsReload=\u00a76Essentials reloaded\u00a7c {0}.