#1034 Add debug sections for viewing DB data and Limbo data

This commit is contained in:
ljacqu 2017-03-07 22:08:04 +01:00
parent 4bb10c5d6d
commit 7eadb7f7f9
6 changed files with 357 additions and 4 deletions

View File

@ -20,7 +20,7 @@ public class DebugCommand implements ExecutableCommand {
private Factory<DebugSection> debugSectionFactory;
private Set<Class<? extends DebugSection>> sectionClasses =
ImmutableSet.of(PermissionGroups.class, TestEmailSender.class);
ImmutableSet.of(PermissionGroups.class, TestEmailSender.class, PlayerAuthViewer.class, LimboPlayerViewer.class);
private Map<String, DebugSection> sections;

View File

@ -0,0 +1,55 @@
package fr.xephi.authme.command.executable.authme.debug;
import org.bukkit.Location;
import java.math.RoundingMode;
import java.text.DecimalFormat;
/**
* Utilities used within the DebugSection implementations.
*/
final class DebugSectionUtils {
private DebugSectionUtils() {
}
/**
* Formats the given location in a human readable way. Null-safe.
*
* @param location the location to format
* @return the formatted location
*/
static String formatLocation(Location location) {
if (location == null) {
return "null";
}
String worldName = location.getWorld() == null ? "null" : location.getWorld().getName();
return formatLocation(location.getX(), location.getY(), location.getZ(), worldName);
}
/**
* Formats the given location in a human readable way.
*
* @param x the x coordinate
* @param y the y coordinate
* @param z the z coordinate
* @param world the world name
* @return the formatted location
*/
static String formatLocation(double x, double y, double z, String world) {
return "(" + round(x) + ", " + round(y) + ", " + round(z) + ") in '" + world + "'";
}
/**
* Rounds the given number to two decimals.
*
* @param number the number to round
* @return the rounded number
*/
private static String round(double number) {
DecimalFormat df = new DecimalFormat("#.##");
df.setRoundingMode(RoundingMode.HALF_UP);
return df.format(number);
}
}

View File

@ -0,0 +1,147 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.service.BukkitService;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation;
/**
* Shows the data stored in LimboPlayers and the equivalent properties on online players.
*/
class LimboPlayerViewer implements DebugSection {
@Inject
private LimboService limboService;
@Inject
private BukkitService bukkitService;
private Field limboServiceEntries;
@Override
public String getName() {
return "limbo";
}
@Override
public String getDescription() {
return "View LimboPlayers and player's \"limbo stats\"";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
if (arguments.isEmpty()) {
sender.sendMessage("/authme debug limbo <player>: show a player's limbo info");
sender.sendMessage("Available limbo records: " + getLimboKeys());
return;
}
LimboPlayer limbo = limboService.getLimboPlayer(arguments.get(0));
Player player = bukkitService.getPlayerExact(arguments.get(0));
if (limbo == null && player == null) {
sender.sendMessage("No limbo info and no player online with name '" + arguments.get(0) + "'");
return;
}
sender.sendMessage(ChatColor.GOLD + "Showing limbo / player info for '" + arguments.get(0) + "'");
new InfoDisplayer(sender, limbo, player)
.sendEntry("Is op", LimboPlayer::isOperator, Player::isOp)
.sendEntry("Walk speed", LimboPlayer::getWalkSpeed, Player::getWalkSpeed)
.sendEntry("Can fly", LimboPlayer::isCanFly, Player::getAllowFlight)
.sendEntry("Fly speed", LimboPlayer::getFlySpeed, Player::getFlySpeed)
.sendEntry("Location", l -> formatLocation(l.getLocation()), p -> formatLocation(p.getLocation()))
.sendEntry("Group", LimboPlayer::getGroup, p -> "");
sender.sendMessage("Note: group is only shown for LimboPlayer");
}
/**
* Gets the names of the LimboPlayers in the LimboService. As we don't want to expose this
* information in non-debug settings, this is done over reflections. Since this is not a
* crucial feature, we generously catch all Exceptions
*
* @return player names for which there is a LimboPlayer (or error message upon failure)
*/
@SuppressWarnings("unchecked")
private Set<String> getLimboKeys() {
// Lazy initialization
if (limboServiceEntries == null) {
try {
Field limboServiceEntries = LimboService.class.getDeclaredField("entries");
limboServiceEntries.setAccessible(true);
this.limboServiceEntries = limboServiceEntries;
} catch (Exception e) {
ConsoleLogger.logException("Could not retrieve LimboService entries field:", e);
return Collections.singleton("Error retrieving LimboPlayer collection");
}
}
try {
return (Set) ((Map) limboServiceEntries.get(limboService)).keySet();
} catch (Exception e) {
ConsoleLogger.logException("Could not retrieve LimboService values:", e);
return Collections.singleton("Error retrieving LimboPlayer values");
}
}
/**
* Displays the info for the given LimboPlayer and Player to the provided CommandSender.
*/
private static final class InfoDisplayer {
private final CommandSender sender;
private final Optional<LimboPlayer> limbo;
private final Optional<Player> player;
/**
* Constructor.
*
* @param sender command sender to send the information to
* @param limbo the limbo player to get data from
* @param player the player to get data from
*/
InfoDisplayer(CommandSender sender, LimboPlayer limbo, Player player) {
this.sender = sender;
this.limbo = Optional.ofNullable(limbo);
this.player = Optional.ofNullable(player);
if (limbo == null) {
sender.sendMessage("Note: no Limbo information available");
} else if (player == null) {
sender.sendMessage("Note: player is not online");
}
}
/**
* Displays a piece of information to the command sender.
*
* @param title the designation of the piece of information
* @param limboGetter getter for data retrieval on the LimboPlayer
* @param playerGetter getter for data retrieval on Player
* @param <T> the data type
* @return this instance (for chaining)
*/
<T> InfoDisplayer sendEntry(String title,
Function<LimboPlayer, T> limboGetter,
Function<Player, T> playerGetter) {
sender.sendMessage(
title + ": "
+ limbo.map(limboGetter).map(String::valueOf).orElse("--")
+ " / "
+ player.map(playerGetter).map(String::valueOf).orElse("--"));
return this;
}
}
}

View File

@ -0,0 +1,104 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.data.auth.PlayerAuth;
import fr.xephi.authme.datasource.DataSource;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.util.StringUtils;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.formatLocation;
/**
* Allows to view the data of a PlayerAuth in the database.
*/
class PlayerAuthViewer implements DebugSection {
@Inject
private DataSource dataSource;
@Override
public String getName() {
return "db";
}
@Override
public String getDescription() {
return "View player's data in the database";
}
@Override
public void execute(CommandSender sender, List<String> arguments) {
if (arguments.isEmpty()) {
sender.sendMessage("Enter player name to view his data in the database.");
sender.sendMessage("Example: /authme debug db Bobby");
return;
}
PlayerAuth auth = dataSource.getAuth(arguments.get(0));
if (auth == null) {
sender.sendMessage("No record exists for '" + arguments.get(0) + "'");
} else {
displayAuthToSender(auth, sender);
}
}
/**
* Outputs the PlayerAuth information to the given sender.
*
* @param auth the PlayerAuth to display
* @param sender the sender to send the messages to
*/
private void displayAuthToSender(PlayerAuth auth, CommandSender sender) {
sender.sendMessage(ChatColor.GOLD + "[AuthMe] Player " + auth.getNickname() + " / " + auth.getRealName());
sender.sendMessage("Email: " + auth.getEmail() + ". IP: " + auth.getIp() + ". Group: " + auth.getGroupId());
sender.sendMessage("Quit location: "
+ formatLocation(auth.getQuitLocX(), auth.getQuitLocY(), auth.getQuitLocZ(), auth.getWorld()));
sender.sendMessage("Last login: " + formatLastLogin(auth));
HashedPassword hashedPass = auth.getPassword();
sender.sendMessage("Hash / salt (partial): '" + safeSubstring(hashedPass.getHash(), 6)
+ "' / '" + safeSubstring(hashedPass.getSalt(), 4) + "'");
}
/**
* Fail-safe substring method. Guarantees not to show the entire String.
*
* @param str the string to transform
* @param length number of characters to show from the start of the String
* @return the first <code>length</code> characters of the string, or half of the string if it is shorter,
* or empty string if the string is null or empty
*/
private static String safeSubstring(String str, int length) {
if (StringUtils.isEmpty(str)) {
return "";
} else if (str.length() < length) {
return str.substring(0, str.length() / 2) + "...";
} else {
return str.substring(0, length) + "...";
}
}
/**
* Formats the last login date from the given PlayerAuth.
*
* @param auth the auth object
* @return the last login as human readable date
*/
private static String formatLastLogin(PlayerAuth auth) {
long lastLogin = auth.getLastLogin();
if (lastLogin == 0) {
return "Never (0)";
} else {
LocalDateTime date = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastLogin), ZoneId.systemDefault());
return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(date);
}
}
}

View File

@ -41,8 +41,8 @@ class TestEmailSender implements DebugSection {
@Override
public void execute(CommandSender sender, List<String> arguments) {
if (!sendMailSSL.hasAllInformation()) {
sender.sendMessage(ChatColor.RED + "You haven't set all required configurations in config.yml " +
"for sending emails. Please check your config.yml");
sender.sendMessage(ChatColor.RED + "You haven't set all required configurations in config.yml "
+ "for sending emails. Please check your config.yml");
return;
}
@ -69,7 +69,8 @@ class TestEmailSender implements DebugSection {
}
String email = auth.getEmail();
if (email == null || "your@email.com".equals(email)) {
sender.sendMessage(ChatColor.RED + "No email set for your account! Please use /authme debug mail <email>");
sender.sendMessage(ChatColor.RED + "No email set for your account!"
+ " Please use /authme debug mail <email>");
return null;
}
return email;

View File

@ -0,0 +1,46 @@
package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.TestHelper;
import org.bukkit.Location;
import org.junit.Test;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.Assert.assertThat;
/**
* Test for {@link DebugSectionUtils}.
*/
public class DebugSectionUtilsTest {
@Test
public void shouldFormatLocation() {
// given / when
String result = DebugSectionUtils.formatLocation(0.0, 10.248592, -18934.2349023, "Main");
// then
assertThat(result, equalTo("(0, 10.25, -18934.23) in 'Main'"));
}
@Test
public void shouldHandleNullWorld() {
// given
Location location = new Location(null, 3.7777, 2.14156, 1);
// when
String result = DebugSectionUtils.formatLocation(location);
// then
assertThat(result, equalTo("(3.78, 2.14, 1) in 'null'"));
}
@Test
public void shouldHandleNullLocation() {
// given / when / then
assertThat(DebugSectionUtils.formatLocation(null), equalTo("null"));
}
@Test
public void shouldHaveHiddenConstructor() {
TestHelper.validateHasOnlyPrivateEmptyConstructor(DebugSectionUtils.class);
}
}