Updates to debug report, debug command

This commit is contained in:
Vankka 2022-03-28 16:57:24 +03:00
parent bea4680739
commit 2d379dcb7c
No known key found for this signature in database
GPG Key ID: 6E50CB7A29B96AD0
4 changed files with 244 additions and 12 deletions

View File

@ -23,6 +23,10 @@ public interface GameCommandArguments {
<T> T get(String label, Class<T> type);
default String getString(String label) {
return get(label, String.class);
}
default Integer getInt(String label) {
return get(label, Integer.class);
}

View File

@ -23,6 +23,7 @@ import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.command.game.abstraction.GameCommand;
import com.discordsrv.common.command.game.abstraction.GameCommandArguments;
import com.discordsrv.common.command.game.abstraction.GameCommandExecutor;
import com.discordsrv.common.command.game.command.subcommand.DebugCommand;
import com.discordsrv.common.command.game.command.subcommand.LinkCommand;
import com.discordsrv.common.command.game.command.subcommand.ReloadCommand;
import com.discordsrv.common.command.game.command.subcommand.VersionCommand;
@ -38,6 +39,7 @@ public class DiscordSRVCommand implements GameCommandExecutor {
INSTANCE = GameCommand.literal("discordsrv")
.requiredPermission("discordsrv.player.command")
.executor(new DiscordSRVCommand(discordSRV))
.then(DebugCommand.get(discordSRV))
.then(LinkCommand.get(discordSRV))
.then(ReloadCommand.get(discordSRV))
.then(VersionCommand.get(discordSRV));

View File

@ -0,0 +1,136 @@
/*
* This file is part of DiscordSRV, licensed under the GPLv3 License
* Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.discordsrv.common.command.game.command.subcommand;
import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.command.game.abstraction.GameCommand;
import com.discordsrv.common.command.game.abstraction.GameCommandArguments;
import com.discordsrv.common.command.game.abstraction.GameCommandExecutor;
import com.discordsrv.common.command.game.sender.ICommandSender;
import com.discordsrv.common.debug.DebugReport;
import com.discordsrv.common.paste.Paste;
import com.discordsrv.common.paste.PasteService;
import com.discordsrv.common.paste.service.AESEncryptedPasteService;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Base64;
import java.util.Collections;
import java.util.Locale;
public class DebugCommand implements GameCommandExecutor {
private static GameCommand INSTANCE;
public static GameCommand get(DiscordSRV discordSRV) {
if (INSTANCE == null) {
DebugCommand debugCommand = new DebugCommand(discordSRV);
INSTANCE = GameCommand.literal("debug")
.requiredPermission("discordsrv.admin.debug")
.executor(debugCommand)
.then(
GameCommand.stringWord("zip")
.suggester((sender, previousArguments, currentInput) ->
"zip".startsWith(currentInput.toLowerCase(Locale.ROOT))
? Collections.singletonList("zip") : Collections.emptyList())
.executor(debugCommand)
);
}
return INSTANCE;
}
private static final String URL_FORMAT = "https://discordsrv.vankka.dev/debug/%s#%s";
private static final Base64.Encoder KEY_ENCODER = Base64.getUrlEncoder().withoutPadding();
private final DiscordSRV discordSRV;
private final PasteService pasteService;
public DebugCommand(DiscordSRV discordSRV) {
this.discordSRV = discordSRV;
this.pasteService = new AESEncryptedPasteService(null /* TODO: tbd */, 128);
}
@Override
public void execute(ICommandSender sender, GameCommandArguments arguments) {
boolean usePaste = !"zip".equals(arguments.getString("zip"));
discordSRV.scheduler().run(() -> {
DebugReport report = new DebugReport(discordSRV);
report.generate();
Throwable pasteError = usePaste ? paste(sender, report) : null;
if (usePaste && pasteError == null) {
// Success
return;
}
Throwable zipError = zip(sender, report);
if (zipError == null) {
// Success
if (usePaste) {
discordSRV.logger().warning("Failed to upload debug, zip generation succeeded", pasteError);
}
return;
}
if (pasteError != null) {
zipError.addSuppressed(pasteError);
}
discordSRV.logger().error(usePaste ? "Failed to upload & zip debug" : "Failed to zip debug", zipError);
sender.sendMessage(Component.text(
usePaste
? "Failed to upload debug report to paste & failed to generate zip"
: "Failed to create debug zip",
NamedTextColor.DARK_RED
));
});
}
private Throwable paste(ICommandSender sender, DebugReport report) {
try {
Paste paste = report.upload(pasteService);
String key = new String(KEY_ENCODER.encode(paste.decryptionKey()), StandardCharsets.UTF_8);
String url = String.format(URL_FORMAT, paste.id(), key);
sender.sendMessage(Component.text(url).clickEvent(ClickEvent.openUrl(url)));
return null;
} catch (Throwable e) {
return e;
}
}
private Throwable zip(ICommandSender sender, DebugReport report) {
try {
Path zip = report.zip();
Path relative = discordSRV.dataDirectory().resolve("../..").relativize(zip);
sender.sendMessage(
Component.text("Debug generated to zip: ", NamedTextColor.GRAY)
.append(Component.text(relative.toString(), NamedTextColor.GREEN))
);
return null;
} catch (Throwable e) {
return e;
}
}
}

View File

@ -22,20 +22,30 @@ import com.discordsrv.common.DiscordSRV;
import com.discordsrv.common.debug.file.DebugFile;
import com.discordsrv.common.debug.file.KeyValueDebugFile;
import com.discordsrv.common.debug.file.TextDebugFile;
import com.discordsrv.common.paste.Paste;
import com.discordsrv.common.paste.PasteService;
import com.discordsrv.common.plugin.Plugin;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileStore;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class DebugReport {
private static final int BIG_FILE_SPLIT_SIZE = 20000;
private final List<DebugFile> files = new ArrayList<>();
private final DiscordSRV discordSRV;
@ -53,18 +63,65 @@ public class DebugReport {
}
}
public Paste upload(PasteService service) throws Throwable {
files.sort(Comparator.comparing(DebugFile::order).reversed());
ArrayNode files = discordSRV.json().createArrayNode();
for (DebugFile file : this.files) {
int length = file.content().length();
if (length >= BIG_FILE_SPLIT_SIZE) {
ObjectNode node = discordSRV.json().createObjectNode();
node.put("name", file.name());
try {
Paste paste = service.uploadFile(convertToJson(file).toString().getBytes(StandardCharsets.UTF_8));
node.put("url", paste.url());
node.put("decryption_key", new String(Base64.getUrlEncoder().encode(paste.decryptionKey()), StandardCharsets.UTF_8));
node.put("length", length);
} catch (Throwable e) {
node.put("content", "Failed to upload file\n\n" + ExceptionUtils.getStackTrace(e));
}
files.add(node);
continue;
}
files.add(convertToJson(file));
}
return service.uploadFile(files.toString().getBytes(StandardCharsets.UTF_8));
}
public Path zip() throws Throwable {
Path zipPath = discordSRV.dataDirectory().resolve("debug-" + System.currentTimeMillis() + ".zip");
try (ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(zipPath.toFile()))) {
for (DebugFile file : files) {
zipOutputStream.putNextEntry(new ZipEntry(file.name()));
byte[] data = file.content().getBytes(StandardCharsets.UTF_8);
zipOutputStream.write(data, 0, data.length);
zipOutputStream.closeEntry();
}
}
return zipPath;
}
private ObjectNode convertToJson(DebugFile file) {
ObjectNode node = discordSRV.json().createObjectNode();
node.put("name", file.name());
node.put("content", file.content());
return node;
}
public void addFile(DebugFile file) {
files.add(file);
}
public List<DebugFile> getFiles() {
return files;
}
private DebugFile environment() {
Map<String, Object> values = new LinkedHashMap<>();
values.put("discordSRV", discordSRV.getClass().getSimpleName());
values.put("discordSRV", discordSRV.getClass().getName());
values.put("version", discordSRV.version());
values.put("gitRevision", discordSRV.gitRevision());
values.put("gitBranch", discordSRV.gitBranch());
values.put("status", discordSRV.status().name());
values.put("jdaStatus", discordSRV.jda().map(jda -> jda.getStatus().name()).orElse("JDA null"));
values.put("platformLogger", discordSRV.platformLogger().getClass().getName());
@ -78,17 +135,50 @@ public class DebugReport {
+ " " + System.getProperty("os.version")
+ " (" + System.getProperty("os.arch") + ")");
Runtime runtime = Runtime.getRuntime();
values.put("cores", runtime.availableProcessors());
values.put("freeMemory", runtime.freeMemory());
values.put("totalMemory", runtime.totalMemory());
long maxMemory = runtime.maxMemory();
values.put("maxMemory", maxMemory == Long.MAX_VALUE ? -1 : maxMemory);
try {
FileStore store = Files.getFileStore(discordSRV.dataDirectory());
values.put("usableSpace", store.getUsableSpace());
values.put("totalSpace", store.getTotalSpace());
} catch (IOException ignored) {}
boolean docker = false;
try {
docker = Files.readAllLines(Paths.get("/proc/1/cgroup"))
.stream().anyMatch(str -> str.contains("/docker/"));
} catch (IOException ignored) {}
values.put("docker", docker);
return new KeyValueDebugFile(10, "environment.json", values);
}
private DebugFile plugins() {
List<Plugin> plugins = discordSRV.pluginManager().getPlugins();
List<Plugin> plugins = discordSRV.pluginManager().getPlugins()
.stream()
.sorted(Comparator.comparing(plugin -> plugin.name().toLowerCase(Locale.ROOT)))
.collect(Collectors.toList());
int longestName = 0;
int longestVersion = 0;
for (Plugin plugin : plugins) {
longestName = Math.max(longestName, plugin.name().length());
longestVersion = Math.max(longestVersion, plugin.version().length());
}
longestName++;
longestVersion++;
StringBuilder builder = new StringBuilder("Plugins (" + plugins.size() + "):\n");
for (Plugin plugin : plugins) {
builder.append('\n')
.append(plugin.name())
.append(" v").append(plugin.version())
.append(StringUtils.rightPad(plugin.name(), longestName))
.append(" v").append(StringUtils.rightPad(plugin.version(), longestVersion))
.append(" ").append(plugin.authors());
}
return new TextDebugFile(5, "plugins.txt", builder.toString());