From bb601dca4b129c2e94e340a0fe572f0d8c34c8e3 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Tue, 7 Aug 2018 11:02:35 -0400 Subject: [PATCH] Add console tab complete, shutdown command, gracefully kick players. --- .../velocitypowered/proxy/VelocityServer.java | 16 ++++++++-- .../proxy/command/CommandManager.java | 32 +++++++++++++++++++ .../proxy/command/ServerCommand.java | 18 +++++++++++ .../proxy/command/ShutdownCommand.java | 20 ++++++++++++ .../proxy/command/VelocityCommand.java | 4 +-- .../proxy/console/VelocityConsole.java | 21 +++++++++--- 6 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/command/ShutdownCommand.java diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index a6c5a5275..3cff9b6bd 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -10,6 +10,7 @@ import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.natives.util.Natives; import com.velocitypowered.network.ConnectionManager; import com.velocitypowered.proxy.command.ServerCommand; +import com.velocitypowered.proxy.command.ShutdownCommand; import com.velocitypowered.proxy.command.VelocityCommand; import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; @@ -21,6 +22,7 @@ import com.velocitypowered.proxy.util.EncryptionUtils; import com.velocitypowered.proxy.util.ServerMap; import io.netty.bootstrap.Bootstrap; import net.kyori.text.Component; +import net.kyori.text.TextComponent; import net.kyori.text.serializer.ComponentSerializers; import net.kyori.text.serializer.GsonComponentSerializer; import org.apache.logging.log4j.LogManager; @@ -50,7 +52,8 @@ public class VelocityServer implements ProxyServer { private KeyPair serverKeyPair; private final ServerMap servers = new ServerMap(); private final CommandManager commandManager = new CommandManager(); - private final AtomicBoolean shutdown = new AtomicBoolean(false); + private final AtomicBoolean shutdownInProgress = new AtomicBoolean(false); + private boolean shutdown = false; private final Map connectionsByUuid = new ConcurrentHashMap<>(); private final Map connectionsByName = new ConcurrentHashMap<>(); @@ -69,6 +72,7 @@ public class VelocityServer implements ProxyServer { private VelocityServer() { commandManager.registerCommand("velocity", new VelocityCommand()); commandManager.registerCommand("server", new ServerCommand()); + commandManager.registerCommand("shutdown", new ShutdownCommand()); } public static VelocityServer getServer() { @@ -136,13 +140,19 @@ public class VelocityServer implements ProxyServer { } public boolean isShutdown() { - return shutdown.get(); + return shutdown; } public void shutdown() { - if (!shutdown.compareAndSet(false, true)) return; + if (!shutdownInProgress.compareAndSet(false, true)) return; logger.info("Shutting down the proxy..."); + + for (ConnectedPlayer player : ImmutableList.copyOf(connectionsByUuid.values())) { + player.close(TextComponent.of("Proxy shutting down.")); + } + this.cm.shutdown(); + shutdown = true; } public NettyHttpClient getHttpClient() { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/CommandManager.java b/proxy/src/main/java/com/velocitypowered/proxy/command/CommandManager.java index 5a3753089..60eeed2ab 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/CommandManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/CommandManager.java @@ -1,12 +1,15 @@ package com.velocitypowered.proxy.command; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import com.velocitypowered.api.command.CommandExecutor; import com.velocitypowered.api.command.CommandInvoker; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; public class CommandManager { private final Map executors = new HashMap<>(); @@ -45,4 +48,33 @@ public class CommandManager { throw new RuntimeException("Unable to invoke command " + cmdLine + " for " + invoker, e); } } + + public List offerSuggestions(CommandInvoker invoker, String cmdLine) { + Preconditions.checkNotNull(invoker, "invoker"); + Preconditions.checkNotNull(cmdLine, "cmdLine"); + + String[] split = cmdLine.split(" ", -1); + if (split.length == 0) { + return ImmutableList.of(); + } + + String command = split[0]; + if (split.length == 1) { + return executors.keySet().stream() + .filter(cmd -> cmd.regionMatches(true, 0, command, 0, command.length())) + .collect(Collectors.toList()); + } + + String[] actualArgs = Arrays.copyOfRange(split, 1, split.length); + CommandExecutor executor = executors.get(command); + if (executor == null) { + return ImmutableList.of(); + } + + try { + return executor.suggest(invoker, actualArgs); + } catch (Exception e) { + throw new RuntimeException("Unable to invoke suggestions for command " + command + " for " + invoker, e); + } + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java index bb66832ca..16a05d021 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java @@ -1,5 +1,6 @@ package com.velocitypowered.proxy.command; +import com.google.common.collect.ImmutableList; import com.velocitypowered.api.command.CommandExecutor; import com.velocitypowered.api.command.CommandInvoker; import com.velocitypowered.api.proxy.Player; @@ -9,6 +10,7 @@ import net.kyori.text.TextComponent; import net.kyori.text.format.TextColor; import javax.annotation.Nonnull; +import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -38,4 +40,20 @@ public class ServerCommand implements CommandExecutor { player.sendMessage(TextComponent.of("Available servers: " + serverList, TextColor.YELLOW)); } } + + @Override + public List suggest(@Nonnull CommandInvoker invoker, @Nonnull String[] currentArgs) { + if (currentArgs.length == 0) { + return VelocityServer.getServer().getAllServers().stream() + .map(ServerInfo::getName) + .collect(Collectors.toList()); + } else if (currentArgs.length == 1) { + return VelocityServer.getServer().getAllServers().stream() + .map(ServerInfo::getName) + .filter(name -> name.regionMatches(true, 0, currentArgs[0], 0, currentArgs[0].length())) + .collect(Collectors.toList()); + } else { + return ImmutableList.of(); + } + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/ShutdownCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/ShutdownCommand.java new file mode 100644 index 000000000..302a49248 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/ShutdownCommand.java @@ -0,0 +1,20 @@ +package com.velocitypowered.proxy.command; + +import com.velocitypowered.api.command.CommandExecutor; +import com.velocitypowered.api.command.CommandInvoker; +import com.velocitypowered.proxy.VelocityServer; +import net.kyori.text.TextComponent; +import net.kyori.text.format.TextColor; + +import javax.annotation.Nonnull; + +public class ShutdownCommand implements CommandExecutor { + @Override + public void execute(@Nonnull CommandInvoker invoker, @Nonnull String[] args) { + if (invoker != VelocityServer.getServer().getConsoleCommandInvoker()) { + invoker.sendMessage(TextComponent.of("You are not allowed to use this command.", TextColor.RED)); + return; + } + VelocityServer.getServer().shutdown(); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java index 20b77e6bc..272fe4777 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java @@ -16,7 +16,7 @@ public class VelocityCommand implements CommandExecutor { TextComponent thisIsVelocity = TextComponent.builder() .content("This is ") .append(TextComponent.of("Velocity " + implVersion, TextColor.DARK_AQUA)) - .append(TextComponent.of(", the next generation Minecraft: Java Edition proxy.", TextColor.WHITE)) + .append(TextComponent.of(", the next generation Minecraft: Java Edition proxy.").resetStyle()) .build(); TextComponent velocityInfo = TextComponent.builder() .content("Copyright 2018 Velocity Contributors. Velocity is freely licensed under the terms of the " + @@ -28,7 +28,7 @@ public class VelocityCommand implements CommandExecutor { .color(TextColor.GREEN) .clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://www.velocitypowered.com")) .build()) - .append(TextComponent.of(" or the ", TextColor.WHITE)) + .append(TextComponent.of(" or the ").resetStyle()) .append(TextComponent.builder("Velocity GitHub") .color(TextColor.GREEN) .clickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://github.com/astei/velocity")) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java index 2ac760e3e..835996148 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java @@ -1,9 +1,12 @@ package com.velocitypowered.proxy.console; import com.velocitypowered.proxy.VelocityServer; +import net.kyori.text.TextComponent; +import net.kyori.text.format.TextColor; import net.minecrell.terminalconsole.SimpleTerminalConsole; -import org.jline.reader.LineReader; -import org.jline.reader.LineReaderBuilder; +import org.jline.reader.*; + +import java.util.List; public final class VelocityConsole extends SimpleTerminalConsole { @@ -16,8 +19,14 @@ public final class VelocityConsole extends SimpleTerminalConsole { @Override protected LineReader buildReader(LineReaderBuilder builder) { return super.buildReader(builder - .appName("Velocity") - // TODO: Command completion + .appName("Velocity") + .completer((reader, parsedLine, list) -> { + List offers = server.getCommandManager().offerSuggestions(server.getConsoleCommandInvoker(), parsedLine.line()); + for (String offer : offers) { + if (offer.isEmpty()) continue; + list.add(new Candidate(offer)); + } + }) ); } @@ -28,7 +37,9 @@ public final class VelocityConsole extends SimpleTerminalConsole { @Override protected void runCommand(String command) { - this.server.getCommandManager().execute(this.server.getConsoleCommandInvoker(), command); + if (!this.server.getCommandManager().execute(this.server.getConsoleCommandInvoker(), command)) { + server.getConsoleCommandInvoker().sendMessage(TextComponent.of("Command not found.", TextColor.RED)); + } } @Override