From e24c38bbb095255a8371fc1108a41bf51c1863ae Mon Sep 17 00:00:00 2001 From: Minecrell Date: Sat, 21 Nov 2015 09:35:27 +0100 Subject: [PATCH] Add jline-based console with colors and tab-completion --- jsons/1.8-dev.json | 5 + jsons/1.8-rel.json | 9 +- .../dedicated/DedicatedServer.java.patch | 20 ++- .../util/ChatComponentStyle.java.patch | 10 ++ .../net/minecraft/util/ChatStyle.java.patch | 10 ++ .../minecraft/util/IChatComponent.java.patch | 10 ++ .../fml/common/launcher/FMLServerTweaker.java | 24 +++ .../console/ConsoleCommandCompleter.java | 96 ++++++++++ .../server/console/ConsoleFormatter.java | 63 +++++++ .../console/TerminalConsoleAppender.java | 168 ++++++++++++++++++ .../server/console/TerminalHandler.java | 70 ++++++++ src/main/resources/log4j2_server.xml | 61 +++++++ 12 files changed, 541 insertions(+), 5 deletions(-) create mode 100644 patches/minecraft/net/minecraft/util/ChatComponentStyle.java.patch create mode 100644 patches/minecraft/net/minecraft/util/ChatStyle.java.patch create mode 100644 patches/minecraft/net/minecraft/util/IChatComponent.java.patch create mode 100644 src/main/java/net/minecraftforge/server/console/ConsoleCommandCompleter.java create mode 100644 src/main/java/net/minecraftforge/server/console/ConsoleFormatter.java create mode 100644 src/main/java/net/minecraftforge/server/console/TerminalConsoleAppender.java create mode 100644 src/main/java/net/minecraftforge/server/console/TerminalHandler.java create mode 100644 src/main/resources/log4j2_server.xml diff --git a/jsons/1.8-dev.json b/jsons/1.8-dev.json index 97f9b7fde..bd8984dfe 100644 --- a/jsons/1.8-dev.json +++ b/jsons/1.8-dev.json @@ -8,6 +8,11 @@ { "name": "net.minecraft:launchwrapper:1.12" }, + { + "name": "jline:jline:2.13", + "children": ["sources"], + "url" : "http://repo.maven.apache.org/maven2" + }, { "name": "com.google.code.findbugs:jsr305:1.3.9", "children": ["sources"], diff --git a/jsons/1.8-rel.json b/jsons/1.8-rel.json index debf80a6c..266f76805 100644 --- a/jsons/1.8-rel.json +++ b/jsons/1.8-rel.json @@ -39,6 +39,13 @@ "name": "org.ow2.asm:asm-all:5.0.3", "serverreq":true }, + { + "name": "jline:jline:2.13", + "url" : "http://files.minecraftforge.net/maven/", + "checksums" : [ "TODO", "TODO" ], + "serverreq":true, + "clientreq":false + }, { "name": "com.typesafe.akka:akka-actor_2.11:2.3.3", "url" : "http://files.minecraftforge.net/maven/", @@ -126,4 +133,4 @@ } ] } -} \ No newline at end of file +} diff --git a/patches/minecraft/net/minecraft/server/dedicated/DedicatedServer.java.patch b/patches/minecraft/net/minecraft/server/dedicated/DedicatedServer.java.patch index 1af800c17..dd0510592 100644 --- a/patches/minecraft/net/minecraft/server/dedicated/DedicatedServer.java.patch +++ b/patches/minecraft/net/minecraft/server/dedicated/DedicatedServer.java.patch @@ -8,7 +8,15 @@ private static final String __OBFID = "CL_00001784"; public DedicatedServer(File p_i1508_1_) -@@ -113,6 +114,8 @@ +@@ -88,6 +89,7 @@ + private static final String __OBFID = "CL_00001786"; + public void run() + { ++ if (net.minecraftforge.server.console.TerminalHandler.handleCommands(DedicatedServer.this)) return; + BufferedReader bufferedreader = new BufferedReader(new InputStreamReader(System.in)); + String s4; + +@@ -113,6 +115,8 @@ field_155771_h.warn("To start the server with more ram, launch it as \"java -Xmx1024M -Xms1024M -jar minecraft_server.jar\""); } @@ -17,7 +25,7 @@ field_155771_h.info("Loading properties"); this.field_71340_o = new PropertyManager(new File("server.properties")); this.field_154332_n = new ServerEula(new File("eula.txt")); -@@ -204,6 +207,7 @@ +@@ -204,6 +208,7 @@ } else { @@ -25,7 +33,7 @@ this.func_152361_a(new DedicatedPlayerList(this)); long j = System.nanoTime(); -@@ -250,6 +254,7 @@ +@@ -250,6 +255,7 @@ this.func_71191_d((this.func_71207_Z() + 8) / 16 * 16); this.func_71191_d(MathHelper.func_76125_a(this.func_71207_Z(), 64, 256)); this.field_71340_o.func_73667_a("max-build-height", Integer.valueOf(this.func_71207_Z())); @@ -33,7 +41,7 @@ field_155771_h.info("Preparing level \"" + this.func_71270_I() + "\""); this.func_71247_a(this.func_71270_I(), this.func_71270_I(), k, worldtype, s2); long i1 = System.nanoTime() - j; -@@ -278,7 +283,7 @@ +@@ -278,10 +284,11 @@ thread1.start(); } @@ -42,3 +50,7 @@ } } } ++ public void func_145747_a(net.minecraft.util.IChatComponent message) { field_155771_h.info(message.func_150254_d()); } + + public void func_71235_a(WorldSettings.GameType p_71235_1_) + { diff --git a/patches/minecraft/net/minecraft/util/ChatComponentStyle.java.patch b/patches/minecraft/net/minecraft/util/ChatComponentStyle.java.patch new file mode 100644 index 000000000..c8a5e732d --- /dev/null +++ b/patches/minecraft/net/minecraft/util/ChatComponentStyle.java.patch @@ -0,0 +1,10 @@ +--- ../src-base/minecraft/net/minecraft/util/ChatComponentStyle.java ++++ ../src-work/minecraft/net/minecraft/util/ChatComponentStyle.java +@@ -81,7 +81,6 @@ + return stringbuilder.toString(); + } + +- @SideOnly(Side.CLIENT) + public final String func_150254_d() + { + StringBuilder stringbuilder = new StringBuilder(); diff --git a/patches/minecraft/net/minecraft/util/ChatStyle.java.patch b/patches/minecraft/net/minecraft/util/ChatStyle.java.patch new file mode 100644 index 000000000..2622b5ec4 --- /dev/null +++ b/patches/minecraft/net/minecraft/util/ChatStyle.java.patch @@ -0,0 +1,10 @@ +--- ../src-base/minecraft/net/minecraft/util/ChatStyle.java ++++ ../src-work/minecraft/net/minecraft/util/ChatStyle.java +@@ -230,7 +230,6 @@ + return this; + } + +- @SideOnly(Side.CLIENT) + public String func_150218_j() + { + if (this.func_150229_g()) diff --git a/patches/minecraft/net/minecraft/util/IChatComponent.java.patch b/patches/minecraft/net/minecraft/util/IChatComponent.java.patch new file mode 100644 index 000000000..2605247ba --- /dev/null +++ b/patches/minecraft/net/minecraft/util/IChatComponent.java.patch @@ -0,0 +1,10 @@ +--- ../src-base/minecraft/net/minecraft/util/IChatComponent.java ++++ ../src-work/minecraft/net/minecraft/util/IChatComponent.java +@@ -32,7 +32,6 @@ + + String func_150260_c(); + +- @SideOnly(Side.CLIENT) + String func_150254_d(); + + List func_150253_a(); diff --git a/src/main/java/net/minecraftforge/fml/common/launcher/FMLServerTweaker.java b/src/main/java/net/minecraftforge/fml/common/launcher/FMLServerTweaker.java index 7eeed4352..359b190bb 100644 --- a/src/main/java/net/minecraftforge/fml/common/launcher/FMLServerTweaker.java +++ b/src/main/java/net/minecraftforge/fml/common/launcher/FMLServerTweaker.java @@ -1,9 +1,28 @@ package net.minecraftforge.fml.common.launcher; +import java.io.File; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; + import net.minecraft.launchwrapper.LaunchClassLoader; import net.minecraftforge.fml.relauncher.FMLLaunchHandler; public class FMLServerTweaker extends FMLTweaker { + + @Override + public void acceptOptions(List args, File gameDir, File assetsDir, String profile) + { + super.acceptOptions(args, gameDir, assetsDir, profile); + + if (System.getProperty("log4j.configurationFile") == null) + { + System.setProperty("log4j.configurationFile", "log4j2_server.xml"); + ((LoggerContext) LogManager.getContext(false)).reconfigure(); + } + } + @Override public String getLaunchTarget() { @@ -16,6 +35,11 @@ public class FMLServerTweaker extends FMLTweaker { // The log4j2 queue is excluded so it is correctly visible from the obfuscated // and deobfuscated parts of the code. Without, the UI won't show anything classLoader.addClassLoaderExclusion("com.mojang.util.QueueLogAppender"); + + classLoader.addClassLoaderExclusion("jline."); + classLoader.addClassLoaderExclusion("org.fusesource."); + classLoader.addClassLoaderExclusion("net.minecraftforge.server.console.TerminalConsoleAppender"); + FMLLaunchHandler.configureForServerLaunch(classLoader, this); FMLLaunchHandler.appendCoreMods(); } diff --git a/src/main/java/net/minecraftforge/server/console/ConsoleCommandCompleter.java b/src/main/java/net/minecraftforge/server/console/ConsoleCommandCompleter.java new file mode 100644 index 000000000..af0216edb --- /dev/null +++ b/src/main/java/net/minecraftforge/server/console/ConsoleCommandCompleter.java @@ -0,0 +1,96 @@ +package net.minecraftforge.server.console; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import jline.console.completer.Completer; +import net.minecraft.server.dedicated.DedicatedServer; + +public final class ConsoleCommandCompleter implements Completer +{ + + private static final Logger logger = LogManager.getLogger(); + private final DedicatedServer server; + + public ConsoleCommandCompleter(DedicatedServer server) + { + this.server = checkNotNull(server, "server"); + } + + @Override + public int complete(String buffer, int cursor, List candidates) + { + int len = buffer.length(); + buffer = buffer.trim(); + if (buffer.isEmpty()) + { + return cursor; + } + + boolean prefix; + if (buffer.charAt(0) != '/') + { + buffer = '/' + buffer; + prefix = false; + } + else + { + prefix = true; + } + + final String input = buffer; + @SuppressWarnings("unchecked") + Future> tabComplete = this.server.callFromMainThread(new Callable>() { + + @Override + public List call() throws Exception + { + return ConsoleCommandCompleter.this.server.getTabCompletions(ConsoleCommandCompleter.this.server, input, + ConsoleCommandCompleter.this.server.getPosition()); + } + }); + try + { + List completions = tabComplete.get(); + if (prefix) + { + candidates.addAll(completions); + } + else + { + for (String completion : completions) + { + candidates.add(completion.charAt(0) == '/' ? completion.substring(1) : completion); + } + } + + int pos = buffer.lastIndexOf(' '); + if (pos == -1) + { + return cursor - len; + } + else + { + return cursor - len + pos; + } + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + } + catch (ExecutionException e) + { + logger.error("Failed to tab complete", e); + } + + return cursor; + } + +} diff --git a/src/main/java/net/minecraftforge/server/console/ConsoleFormatter.java b/src/main/java/net/minecraftforge/server/console/ConsoleFormatter.java new file mode 100644 index 000000000..51719e087 --- /dev/null +++ b/src/main/java/net/minecraftforge/server/console/ConsoleFormatter.java @@ -0,0 +1,63 @@ +package net.minecraftforge.server.console; + +import java.util.Map; +import java.util.regex.Pattern; + +import org.fusesource.jansi.Ansi; + +import com.google.common.base.Function; +import com.google.common.collect.ImmutableMap; + +import net.minecraft.util.EnumChatFormatting; + +public final class ConsoleFormatter implements Function +{ + + public ConsoleFormatter() + { + } + + private static final String RESET = Ansi.ansi().reset().toString(); + + private static final ImmutableMap REPLACEMENTS = ImmutableMap. builder() + .put(compile(EnumChatFormatting.BLACK), Ansi.ansi().reset().fg(Ansi.Color.BLACK).boldOff().toString()) + .put(compile(EnumChatFormatting.DARK_BLUE), Ansi.ansi().reset().fg(Ansi.Color.BLUE).boldOff().toString()) + .put(compile(EnumChatFormatting.DARK_GREEN), Ansi.ansi().reset().fg(Ansi.Color.GREEN).boldOff().toString()) + .put(compile(EnumChatFormatting.DARK_AQUA), Ansi.ansi().reset().fg(Ansi.Color.CYAN).boldOff().toString()) + .put(compile(EnumChatFormatting.DARK_RED), Ansi.ansi().reset().fg(Ansi.Color.RED).boldOff().toString()) + .put(compile(EnumChatFormatting.DARK_PURPLE), Ansi.ansi().reset().fg(Ansi.Color.MAGENTA).boldOff().toString()) + .put(compile(EnumChatFormatting.GOLD), Ansi.ansi().reset().fg(Ansi.Color.YELLOW).boldOff().toString()) + .put(compile(EnumChatFormatting.GRAY), Ansi.ansi().reset().fg(Ansi.Color.WHITE).boldOff().toString()) + .put(compile(EnumChatFormatting.DARK_GRAY), Ansi.ansi().reset().fg(Ansi.Color.BLACK).bold().toString()) + .put(compile(EnumChatFormatting.BLUE), Ansi.ansi().reset().fg(Ansi.Color.BLUE).bold().toString()) + .put(compile(EnumChatFormatting.GREEN), Ansi.ansi().reset().fg(Ansi.Color.GREEN).bold().toString()) + .put(compile(EnumChatFormatting.AQUA), Ansi.ansi().reset().fg(Ansi.Color.CYAN).bold().toString()) + .put(compile(EnumChatFormatting.RED), Ansi.ansi().reset().fg(Ansi.Color.RED).bold().toString()) + .put(compile(EnumChatFormatting.LIGHT_PURPLE), Ansi.ansi().reset().fg(Ansi.Color.MAGENTA).bold().toString()) + .put(compile(EnumChatFormatting.YELLOW), Ansi.ansi().reset().fg(Ansi.Color.YELLOW).bold().toString()) + .put(compile(EnumChatFormatting.WHITE), Ansi.ansi().reset().fg(Ansi.Color.WHITE).bold().toString()) + .put(compile(EnumChatFormatting.OBFUSCATED), Ansi.ansi().a(Ansi.Attribute.BLINK_SLOW).toString()) + .put(compile(EnumChatFormatting.BOLD), Ansi.ansi().a(Ansi.Attribute.UNDERLINE_DOUBLE).toString()) + .put(compile(EnumChatFormatting.STRIKETHROUGH), Ansi.ansi().a(Ansi.Attribute.STRIKETHROUGH_ON).toString()) + .put(compile(EnumChatFormatting.UNDERLINE), Ansi.ansi().a(Ansi.Attribute.UNDERLINE).toString()) + .put(compile(EnumChatFormatting.ITALIC), Ansi.ansi().a(Ansi.Attribute.ITALIC).toString()) + .put(compile(EnumChatFormatting.RESET), RESET) + .build(); + + private static Pattern compile(EnumChatFormatting formatting) + { + return Pattern.compile(formatting.toString(), Pattern.LITERAL | Pattern.CASE_INSENSITIVE); + } + + @Override + public String apply(String text) + { + for (Map.Entry entry : REPLACEMENTS.entrySet()) + { + text = entry.getKey().matcher(text).replaceAll(entry.getValue()); + } + + return text + RESET; + } + +} diff --git a/src/main/java/net/minecraftforge/server/console/TerminalConsoleAppender.java b/src/main/java/net/minecraftforge/server/console/TerminalConsoleAppender.java new file mode 100644 index 000000000..d32f25f13 --- /dev/null +++ b/src/main/java/net/minecraftforge/server/console/TerminalConsoleAppender.java @@ -0,0 +1,168 @@ +package net.minecraftforge.server.console; + +import static jline.TerminalFactory.OFF; +import static jline.console.ConsoleReader.RESET_LINE; +import static org.apache.logging.log4j.core.helpers.Booleans.parseBoolean; + +import java.io.IOException; +import java.io.PrintStream; +import java.io.Serializable; +import java.io.Writer; + +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.layout.PatternLayout; +import org.apache.logging.log4j.util.PropertiesUtil; +import org.fusesource.jansi.AnsiConsole; + +import com.google.common.base.Function; +import com.google.common.base.Functions; + +import jline.TerminalFactory; +import jline.console.ConsoleReader; + +@Plugin(name = "TerminalConsole", category = "Core", elementType = "appender", printObject = true) +public class TerminalConsoleAppender extends AbstractAppender +{ + + private static final boolean ENABLE_JLINE = PropertiesUtil.getProperties().getBooleanProperty("jline.enable", true); + + private static final PrintStream out = System.out; + + private static boolean initialized; + private static ConsoleReader reader; + + public static ConsoleReader getReader() + { + return reader; + } + + private static Function formatter = Functions.identity(); + + public static void setFormatter(Function format) + { + formatter = format != null ? format : Functions. identity(); + } + + protected TerminalConsoleAppender(String name, Filter filter, Layout layout, boolean ignoreExceptions) + { + super(name, filter, layout, ignoreExceptions); + } + + @PluginFactory + public static TerminalConsoleAppender createAppender(@PluginAttribute("name") String name, @PluginElement("Filters") Filter filter, + @PluginElement("Layout") Layout layout, @PluginAttribute("ignoreExceptions") String ignore) + { + + if (name == null) + { + LOGGER.error("No name provided for TerminalConsoleAppender"); + return null; + } + if (layout == null) + { + layout = PatternLayout.createLayout(null, null, null, null, null); + } + + boolean ignoreExceptions = parseBoolean(ignore, true); + + // This is handled by jline + System.setProperty("log4j.skipJansi", "true"); + return new TerminalConsoleAppender(name, filter, layout, ignoreExceptions); + } + + @Override + public void start() + { + // Initialize the reader if that hasn't happened yet + if (!initialized && reader == null) + { + initialized = true; + + if (ENABLE_JLINE) + { + final boolean hasConsole = System.console() != null; + if (hasConsole) + { + try + { + AnsiConsole.systemInstall(); + reader = new ConsoleReader(); + reader.setExpandEvents(false); + } + catch (Exception e) + { + LOGGER.warn("Failed to initialize terminal. Falling back to default.", e); + } + } + + if (reader == null) + { + // Eclipse doesn't support colors and characters like \r so enabling jline2 on it will + // just cause a lot of issues with empty lines and weird characters. + // Enable jline2 only on IntelliJ IDEA to prevent that. + // Also see: https://bugs.eclipse.org/bugs/show_bug.cgi?id=76936 + + if (hasConsole || System.getProperty("java.class.path").contains("idea_rt.jar")) + { + // Disable advanced jline features + TerminalFactory.configure(OFF); + TerminalFactory.reset(); + + try + { + reader = new ConsoleReader(); + reader.setExpandEvents(false); + } + catch (Exception e) + { + LOGGER.warn("Failed to initialize fallback terminal. Falling back to standard output console.", e); + } + } + else + { + LOGGER.warn("Disabling terminal, you're running in an unsupported environment."); + } + } + } + } + + super.start(); + } + + @Override + public void append(LogEvent event) + { + if (reader != null) + { + try + { + Writer out = reader.getOutput(); + out.write(RESET_LINE); + out.write(formatEvent(event)); + + reader.drawLine(); + reader.flush(); + } + catch (IOException ignored) + { + } + } + else + { + out.print(formatEvent(event)); + } + } + + protected String formatEvent(LogEvent event) + { + return formatter.apply(getLayout().toSerializable(event).toString()); + } + +} diff --git a/src/main/java/net/minecraftforge/server/console/TerminalHandler.java b/src/main/java/net/minecraftforge/server/console/TerminalHandler.java new file mode 100644 index 000000000..3c9f6af28 --- /dev/null +++ b/src/main/java/net/minecraftforge/server/console/TerminalHandler.java @@ -0,0 +1,70 @@ +package net.minecraftforge.server.console; + +import java.io.IOException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.google.common.base.Function; + +import jline.console.ConsoleReader; +import net.minecraft.server.dedicated.DedicatedServer; +import net.minecraft.util.EnumChatFormatting; + +public final class TerminalHandler +{ + + private static final Logger logger = LogManager.getLogger(); + + private TerminalHandler() + { + } + + public static boolean handleCommands(DedicatedServer server) + { + final ConsoleReader reader = TerminalConsoleAppender.getReader(); + if (reader != null) + { + TerminalConsoleAppender.setFormatter(new ConsoleFormatter()); + reader.addCompleter(new ConsoleCommandCompleter(server)); + + String line; + while (!server.isServerStopped() && server.isServerRunning()) + { + try + { + line = reader.readLine("> "); + + if (line != null) + { + line = line.trim(); + if (!line.isEmpty()) + { + server.addPendingCommand(line, server); + } + } + } + catch (IOException e) + { + logger.error("Exception handling console input", e); + } + } + + return true; + } + else + { + TerminalConsoleAppender.setFormatter(new Function() { + + @Override + public String apply(String text) + { + return EnumChatFormatting.getTextWithoutFormattingCodes(text); + } + + }); + return false; + } + } + +} diff --git a/src/main/resources/log4j2_server.xml b/src/main/resources/log4j2_server.xml new file mode 100644 index 000000000..49f8f3d04 --- /dev/null +++ b/src/main/resources/log4j2_server.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +