From d671e6f9baa04e24af9751c81d428b61757ae3c0 Mon Sep 17 00:00:00 2001 From: Minecrell Date: Fri, 9 Jun 2017 19:03:43 +0200 Subject: [PATCH] Use TerminalConsoleAppender for console improvements Rewrite console improvements (console colors, tab completion, persistent input line, ...) using JLine 3.x and TerminalConsoleAppender. New features: - Support console colors for Vanilla commands - Add console colors for warnings and errors - Server can now be turned off safely using CTRL + C. JLine catches the signal and the implementation shuts down the server cleanly. - Support console colors and persistent input line when running in IntelliJ IDEA Other changes: - Update JLine to 3.3.1 (from 2.12.1) - Server starts 1-2 seconds faster thanks to optimizations in Log4j configuration diff --git a/pom.xml b/pom.xml index 17bc80776..4dd5fed25 100644 --- a/pom.xml +++ b/pom.xml @@ -41,15 +41,9 @@ compile - jline - jline - 2.12.1 - compile - - - org.ow2.asm - asm - 6.2 + net.sf.jopt-simple + jopt-simple + 5.0.4 compile @@ -70,6 +64,32 @@ 3.0.3 compile + + + net.minecrell + terminalconsoleappender + 1.0.0 + + + net.java.dev.jna + jna + 4.4.0 + runtime + + + + + org.apache.logging.log4j + log4j-core + 2.8.1 + runtime + + junit @@ -203,10 +223,18 @@ META-INF/services/java.sql.Driver + + + + com.github.edwgiz + maven-shade-plugin.log4j2-cachefile-transformer + 2.8.1 + + org.apache.maven.plugins diff --git a/src/main/java/com/destroystokyo/paper/console/TerminalConsoleCommandSender.java b/src/main/java/com/destroystokyo/paper/console/TerminalConsoleCommandSender.java new file mode 100644 index 000000000..685deaa0e --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/console/TerminalConsoleCommandSender.java @@ -0,0 +1,17 @@ +package com.destroystokyo.paper.console; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bukkit.craftbukkit.command.CraftConsoleCommandSender; + +public class TerminalConsoleCommandSender extends CraftConsoleCommandSender { + + private static final Logger LOGGER = LogManager.getRootLogger(); + + @Override + public void sendRawMessage(String message) { + // TerminalConsoleAppender supports color codes directly in log messages + LOGGER.info(message); + } + +} diff --git a/src/main/java/com/destroystokyo/paper/console/TerminalHandler.java b/src/main/java/com/destroystokyo/paper/console/TerminalHandler.java new file mode 100644 index 000000000..626bfeec8 --- /dev/null +++ b/src/main/java/com/destroystokyo/paper/console/TerminalHandler.java @@ -0,0 +1,61 @@ +package com.destroystokyo.paper.console; + +import net.minecraft.server.DedicatedServer; +import net.minecrell.terminalconsole.TerminalConsoleAppender; +import org.bukkit.craftbukkit.command.ConsoleCommandCompleter; +import org.jline.reader.EndOfFileException; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; +import org.jline.reader.UserInterruptException; +import org.jline.terminal.Terminal; + +public class TerminalHandler { + + private TerminalHandler() { + } + + public static boolean handleCommands(DedicatedServer server) { + final Terminal terminal = TerminalConsoleAppender.getTerminal(); + if (terminal == null) { + return false; + } + + LineReader reader = LineReaderBuilder.builder() + .appName("Paper") + .terminal(terminal) + .completer(new ConsoleCommandCompleter(server)) + .build(); + reader.setOpt(LineReader.Option.DISABLE_EVENT_EXPANSION); + reader.unsetOpt(LineReader.Option.INSERT_TAB); + + TerminalConsoleAppender.setReader(reader); + + try { + String line; + while (!server.isStopped() && server.isRunning()) { + try { + line = reader.readLine("> "); + } catch (EndOfFileException ignored) { + // Continue reading after EOT + continue; + } + + if (line == null) { + break; + } + + line = line.trim(); + if (!line.isEmpty()) { + server.issueCommand(line, server); + } + } + } catch (UserInterruptException e) { + server.safeShutdown(); + } finally { + TerminalConsoleAppender.setReader(null); + } + + return true; + } + +} diff --git a/src/main/java/net/minecraft/server/DedicatedServer.java b/src/main/java/net/minecraft/server/DedicatedServer.java index a3d58b5ce..069eb7d68 100644 --- a/src/main/java/net/minecraft/server/DedicatedServer.java +++ b/src/main/java/net/minecraft/server/DedicatedServer.java @@ -79,7 +79,10 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer if (!org.bukkit.craftbukkit.Main.useConsole) { return; } - jline.console.ConsoleReader bufferedreader = reader; + // Paper start - Use TerminalConsoleAppender implementation + if (com.destroystokyo.paper.console.TerminalHandler.handleCommands(DedicatedServer.this)) return; + BufferedReader bufferedreader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)); + // Paper end // CraftBukkit end String s; @@ -87,11 +90,17 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer try { // CraftBukkit start - JLine disabling compatibility while (!DedicatedServer.this.isStopped() && DedicatedServer.this.isRunning()) { + // Paper start - code is not used for jline + /* if (org.bukkit.craftbukkit.Main.useJline) { s = bufferedreader.readLine(">", null); } else { s = bufferedreader.readLine(); } + */ + s = bufferedreader.readLine(); + // Paper end + if (s != null && s.trim().length() > 0) { // Trim to filter lines which are just spaces DedicatedServer.this.issueCommand(s, DedicatedServer.this.getServerCommandListener()); } @@ -112,6 +121,9 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer } global.addHandler(new org.bukkit.craftbukkit.util.ForwardLogHandler()); + // Paper start - Not needed with TerminalConsoleAppender + final org.apache.logging.log4j.Logger logger = LogManager.getRootLogger(); + /* final org.apache.logging.log4j.core.Logger logger = ((org.apache.logging.log4j.core.Logger) LogManager.getRootLogger()); for (org.apache.logging.log4j.core.Appender appender : logger.getAppenders().values()) { if (appender instanceof org.apache.logging.log4j.core.appender.ConsoleAppender) { @@ -120,6 +132,8 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer } new Thread(new org.bukkit.craftbukkit.util.TerminalConsoleWriterThread(System.out, this.reader)).start(); + */ + // Paper end System.setOut(new PrintStream(new LoggerOutputStream(logger, Level.INFO), true)); System.setErr(new PrintStream(new LoggerOutputStream(logger, Level.WARN), true)); diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index f679c6bc2..39a8b1d69 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -57,7 +57,6 @@ import org.apache.commons.lang3.Validate; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; // CraftBukkit start -import jline.console.ConsoleReader; import joptsimple.OptionSet; import org.bukkit.Bukkit; import org.bukkit.craftbukkit.CraftServer; @@ -142,7 +141,7 @@ public abstract class MinecraftServer implements IAsyncTaskHandler, IMojangStati public OptionSet options; public org.bukkit.command.ConsoleCommandSender console; public org.bukkit.command.RemoteConsoleCommandSender remoteConsole; - public ConsoleReader reader; + //public ConsoleReader reader; // Paper public static int currentTick = 0; // Paper - Further improve tick loop public final Thread primaryThread; public java.util.Queue processQueue = new java.util.concurrent.ConcurrentLinkedQueue(); @@ -183,7 +182,9 @@ public abstract class MinecraftServer implements IAsyncTaskHandler, IMojangStati this.ac.a((IResourcePackListener) this.al); // CraftBukkit start this.options = options; + // Paper start - Handled by TerminalConsoleAppender // Try to see if we're actually running in a terminal, disable jline if not + /* if (System.console() == null && System.getProperty("jline.terminal") == null) { System.setProperty("jline.terminal", "jline.UnsupportedTerminal"); Main.useJline = false; @@ -204,6 +205,8 @@ public abstract class MinecraftServer implements IAsyncTaskHandler, IMojangStati LOGGER.warn((String) null, ex); } } + */ + // Paper end Runtime.getRuntime().addShutdownHook(new org.bukkit.craftbukkit.util.ServerShutdownThread(this)); this.serverThread = primaryThread = new Thread(this, "Server thread"); // Moved from main @@ -804,7 +807,7 @@ public abstract class MinecraftServer implements IAsyncTaskHandler, IMojangStati } finally { // CraftBukkit start - Restore terminal to original settings try { - reader.getTerminal().restore(); + net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender } catch (Exception ignored) { } // CraftBukkit end @@ -1285,7 +1288,7 @@ public abstract class MinecraftServer implements IAsyncTaskHandler, IMojangStati } public void sendMessage(IChatBaseComponent ichatbasecomponent) { - MinecraftServer.LOGGER.info(ichatbasecomponent.getString()); + MinecraftServer.LOGGER.info(org.bukkit.craftbukkit.util.CraftChatMessage.fromComponent(ichatbasecomponent, net.minecraft.server.EnumChatFormat.WHITE));// Paper - Log message with colors } public KeyPair G() { diff --git a/src/main/java/net/minecraft/server/PlayerList.java b/src/main/java/net/minecraft/server/PlayerList.java index 5e833c378..0f39fa49f 100644 --- a/src/main/java/net/minecraft/server/PlayerList.java +++ b/src/main/java/net/minecraft/server/PlayerList.java @@ -78,8 +78,7 @@ public abstract class PlayerList { public PlayerList(MinecraftServer minecraftserver) { this.cserver = minecraftserver.server = new CraftServer(minecraftserver, this); - minecraftserver.console = org.bukkit.craftbukkit.command.ColouredConsoleSender.getInstance(); - minecraftserver.reader.addCompleter(new org.bukkit.craftbukkit.command.ConsoleCommandCompleter(minecraftserver.server)); + minecraftserver.console = new com.destroystokyo.paper.console.TerminalConsoleCommandSender(); // Paper // CraftBukkit end this.k = new GameProfileBanList(PlayerList.a); diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java index 470e334f7..9fe7c6a0d 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -137,8 +137,8 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.HashMap; -import jline.console.ConsoleReader; import org.bukkit.Keyed; +import org.apache.commons.lang.StringUtils; import org.bukkit.NamespacedKey; import org.bukkit.block.data.BlockData; import org.bukkit.craftbukkit.block.data.CraftBlockData; @@ -1141,9 +1141,13 @@ public final class CraftServer implements Server { return logger; } + // Paper start - JLine update + /* public ConsoleReader getReader() { return console.reader; } + */ + // Paper end @Override public PluginCommand getPluginCommand(String name) { diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java index aad208f47..ac38028d7 100644 --- a/src/main/java/org/bukkit/craftbukkit/Main.java +++ b/src/main/java/org/bukkit/craftbukkit/Main.java @@ -14,7 +14,7 @@ import java.util.logging.Logger; import joptsimple.OptionParser; import joptsimple.OptionSet; import net.minecraft.server.MinecraftServer; -import org.fusesource.jansi.AnsiConsole; +import net.minecrell.terminalconsole.TerminalConsoleAppender; // Paper public class Main { public static boolean useJline = true; @@ -176,6 +176,8 @@ public class Main { } try { + // Paper start - Handled by TerminalConsoleAppender + /* // This trick bypasses Maven Shade's clever rewriting of our getProperty call when using String literals String jline_UnsupportedTerminal = new String(new char[] {'j','l','i','n','e','.','U','n','s','u','p','p','o','r','t','e','d','T','e','r','m','i','n','a','l'}); String jline_terminal = new String(new char[] {'j','l','i','n','e','.','t','e','r','m','i','n','a','l'}); @@ -193,10 +195,18 @@ public class Main { // This ensures the terminal literal will always match the jline implementation System.setProperty(jline.TerminalFactory.JLINE_TERMINAL, jline.UnsupportedTerminal.class.getName()); } + */ + if (options.has("nojline")) { + System.setProperty(TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY, "false"); + useJline = false; + } + // Paper end if (options.has("noconsole")) { useConsole = false; + useJline = false; // Paper + System.setProperty(TerminalConsoleAppender.JLINE_OVERRIDE_PROPERTY, "false"); // Paper } if (Main.class.getPackage().getImplementationVendor() != null && System.getProperty("IReallyKnowWhatIAmDoingISwear") == null) { diff --git a/src/main/java/org/bukkit/craftbukkit/command/ColouredConsoleSender.java b/src/main/java/org/bukkit/craftbukkit/command/ColouredConsoleSender.java deleted file mode 100644 index 26a2fb894..000000000 --- a/src/main/java/org/bukkit/craftbukkit/command/ColouredConsoleSender.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.bukkit.craftbukkit.command; - -import java.util.EnumMap; -import java.util.Map; - -import org.fusesource.jansi.Ansi; -import org.fusesource.jansi.Ansi.Attribute; -import jline.Terminal; - -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.command.ConsoleCommandSender; -import org.bukkit.craftbukkit.CraftServer; - -public class ColouredConsoleSender extends CraftConsoleCommandSender { - private final Terminal terminal; - private final Map replacements = new EnumMap(ChatColor.class); - private final ChatColor[] colors = ChatColor.values(); - - protected ColouredConsoleSender() { - super(); - this.terminal = ((CraftServer) getServer()).getReader().getTerminal(); - - replacements.put(ChatColor.BLACK, Ansi.ansi().a(Attribute.RESET).fg(Ansi.Color.BLACK).boldOff().toString()); - replacements.put(ChatColor.DARK_BLUE, Ansi.ansi().a(Attribute.RESET).fg(Ansi.Color.BLUE).boldOff().toString()); - replacements.put(ChatColor.DARK_GREEN, Ansi.ansi().a(Attribute.RESET).fg(Ansi.Color.GREEN).boldOff().toString()); - replacements.put(ChatColor.DARK_AQUA, Ansi.ansi().a(Attribute.RESET).fg(Ansi.Color.CYAN).boldOff().toString()); - replacements.put(ChatColor.DARK_RED, Ansi.ansi().a(Attribute.RESET).fg(Ansi.Color.RED).boldOff().toString()); - replacements.put(ChatColor.DARK_PURPLE, Ansi.ansi().a(Attribute.RESET).fg(Ansi.Color.MAGENTA).boldOff().toString()); - replacements.put(ChatColor.GOLD, Ansi.ansi().a(Attribute.RESET).fg(Ansi.Color.YELLOW).boldOff().toString()); - replacements.put(ChatColor.GRAY, Ansi.ansi().a(Attribute.RESET).fg(Ansi.Color.WHITE).boldOff().toString()); - replacements.put(ChatColor.DARK_GRAY, Ansi.ansi().a(Attribute.RESET).fg(Ansi.Color.BLACK).bold().toString()); - replacements.put(ChatColor.BLUE, Ansi.ansi().a(Attribute.RESET).fg(Ansi.Color.BLUE).bold().toString()); - replacements.put(ChatColor.GREEN, Ansi.ansi().a(Attribute.RESET).fg(Ansi.Color.GREEN).bold().toString()); - replacements.put(ChatColor.AQUA, Ansi.ansi().a(Attribute.RESET).fg(Ansi.Color.CYAN).bold().toString()); - replacements.put(ChatColor.RED, Ansi.ansi().a(Attribute.RESET).fg(Ansi.Color.RED).bold().toString()); - replacements.put(ChatColor.LIGHT_PURPLE, Ansi.ansi().a(Attribute.RESET).fg(Ansi.Color.MAGENTA).bold().toString()); - replacements.put(ChatColor.YELLOW, Ansi.ansi().a(Attribute.RESET).fg(Ansi.Color.YELLOW).bold().toString()); - replacements.put(ChatColor.WHITE, Ansi.ansi().a(Attribute.RESET).fg(Ansi.Color.WHITE).bold().toString()); - replacements.put(ChatColor.MAGIC, Ansi.ansi().a(Attribute.BLINK_SLOW).toString()); - replacements.put(ChatColor.BOLD, Ansi.ansi().a(Attribute.UNDERLINE_DOUBLE).toString()); - replacements.put(ChatColor.STRIKETHROUGH, Ansi.ansi().a(Attribute.STRIKETHROUGH_ON).toString()); - replacements.put(ChatColor.UNDERLINE, Ansi.ansi().a(Attribute.UNDERLINE).toString()); - replacements.put(ChatColor.ITALIC, Ansi.ansi().a(Attribute.ITALIC).toString()); - replacements.put(ChatColor.RESET, Ansi.ansi().a(Attribute.RESET).toString()); - } - - @Override - public void sendMessage(String message) { - if (terminal.isAnsiSupported()) { - if (!conversationTracker.isConversingModaly()) { - String result = message; - for (ChatColor color : colors) { - if (replacements.containsKey(color)) { - result = result.replaceAll("(?i)" + color.toString(), replacements.get(color)); - } else { - result = result.replaceAll("(?i)" + color.toString(), ""); - } - } - System.out.println(result + Ansi.ansi().reset().toString()); - } - } else { - super.sendMessage(message); - } - } - - public static ConsoleCommandSender getInstance() { - if (Bukkit.getConsoleSender() != null) { - return Bukkit.getConsoleSender(); - } else { - return new ColouredConsoleSender(); - } - } -} diff --git a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java index 33e8ea02c..1e3aae3b8 100644 --- a/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java +++ b/src/main/java/org/bukkit/craftbukkit/command/ConsoleCommandCompleter.java @@ -8,17 +8,27 @@ import java.util.logging.Level; import org.bukkit.craftbukkit.CraftServer; import org.bukkit.craftbukkit.util.Waitable; -import jline.console.completer.Completer; +// Paper start - JLine update +import net.minecraft.server.DedicatedServer; // Paper +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; +// Paper end import org.bukkit.event.server.TabCompleteEvent; public class ConsoleCommandCompleter implements Completer { - private final CraftServer server; + private final DedicatedServer server; // Paper - CraftServer -> DedicatedServer - public ConsoleCommandCompleter(CraftServer server) { + public ConsoleCommandCompleter(DedicatedServer server) { // Paper - CraftServer -> DedicatedServer this.server = server; } - public int complete(final String buffer, final int cursor, final List candidates) { + // Paper start - Change method signature for JLine update + public void complete(LineReader reader, ParsedLine line, List candidates) { + final CraftServer server = this.server.server; + final String buffer = line.line(); + // Paper end Waitable> waitable = new Waitable>() { @Override protected List evaluate() { @@ -30,25 +40,37 @@ public class ConsoleCommandCompleter implements Completer { return tabEvent.isCancelled() ? Collections.EMPTY_LIST : tabEvent.getCompletions(); } }; - this.server.getServer().processQueue.add(waitable); + server.getServer().processQueue.add(waitable); // Paper - Remove "this." try { List offers = waitable.get(); if (offers == null) { - return cursor; + return; // Paper - Method returns void } - candidates.addAll(offers); + // Paper start - JLine update + for (String completion : offers) { + if (completion.isEmpty()) { + continue; + } + + candidates.add(new Candidate(completion)); + } + // Paper end + + // Paper start - JLine handles cursor now + /* final int lastSpace = buffer.lastIndexOf(' '); if (lastSpace == -1) { return cursor - buffer.length(); } else { return cursor - (buffer.length() - lastSpace - 1); } + */ + // Paper end } catch (ExecutionException e) { - this.server.getLogger().log(Level.WARNING, "Unhandled exception when tab completing", e); + server.getLogger().log(Level.WARNING, "Unhandled exception when tab completing", e); // Paper - Remove "this." } catch (InterruptedException e) { Thread.currentThread().interrupt(); } - return cursor; } } diff --git a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java index 984df4083..bbb5a84f3 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java +++ b/src/main/java/org/bukkit/craftbukkit/util/ServerShutdownThread.java @@ -20,7 +20,7 @@ public class ServerShutdownThread extends Thread { ex.printStackTrace(); } finally { try { - server.reader.getTerminal().restore(); + net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender } catch (Exception e) { } } diff --git a/src/main/java/org/bukkit/craftbukkit/util/TerminalConsoleWriterThread.java b/src/main/java/org/bukkit/craftbukkit/util/TerminalConsoleWriterThread.java deleted file mode 100644 index b64097113..000000000 --- a/src/main/java/org/bukkit/craftbukkit/util/TerminalConsoleWriterThread.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.bukkit.craftbukkit.util; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.logging.Level; -import java.util.logging.Logger; -import jline.console.ConsoleReader; -import com.mojang.util.QueueLogAppender; -import org.bukkit.craftbukkit.Main; -import org.fusesource.jansi.Ansi; -import org.fusesource.jansi.Ansi.Erase; - -public class TerminalConsoleWriterThread implements Runnable { - final private ConsoleReader reader; - final private OutputStream output; - - public TerminalConsoleWriterThread(OutputStream output, ConsoleReader reader) { - this.output = output; - this.reader = reader; - } - - public void run() { - String message; - - // Using name from log4j config in vanilla jar - while (true) { - message = QueueLogAppender.getNextLogEvent("TerminalConsole"); - if (message == null) { - continue; - } - - try { - if (Main.useJline) { - reader.print(Ansi.ansi().eraseLine(Erase.ALL).toString() + ConsoleReader.RESET_LINE); - reader.flush(); - output.write(message.getBytes()); - output.flush(); - - try { - reader.drawLine(); - } catch (Throwable ex) { - reader.getCursorBuffer().clear(); - } - reader.flush(); - } else { - output.write(message.getBytes()); - output.flush(); - } - } catch (IOException ex) { - Logger.getLogger(TerminalConsoleWriterThread.class.getName()).log(Level.SEVERE, null, ex); - } - } - } -} diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index 5cee8f00e..08b6bb7f9 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -1,12 +1,11 @@ - + - - - - + + + - + @@ -19,7 +18,6 @@ - -- 2.18.0