From 1db28d072ab46e7997cf8c1ce3c55466e2e5d940 Mon Sep 17 00:00:00 2001 From: mezz Date: Wed, 27 Jun 2018 10:58:07 -0700 Subject: [PATCH] Improve server console implementation (#4821) --- jsons/1.12.2-dev.json | 2 +- jsons/1.12.2-rel.json | 9 +- .../fml/common/launcher/FMLServerTweaker.java | 8 +- .../fml/relauncher/CoreModManager.java | 12 + .../fml/relauncher/ServerLaunchWrapper.java | 8 + .../console/ConsoleCommandCompleter.java | 54 +-- .../server/console/ConsoleFormatter.java | 82 ---- .../console/TerminalConsoleAppender.java | 197 --------- .../server/console/TerminalHandler.java | 72 ++-- .../log4j/TerminalConsoleAppender.java | 187 --------- .../HighlightErrorConverter.java | 168 ++++++++ .../MinecraftFormattingConverter.java | 222 +++++++++++ .../TerminalConsoleAppender.java | 374 ++++++++++++++++++ .../server/terminalconsole/package-info.java | 7 + .../util/LoggerNamePatternSelector.java | 171 ++++++++ .../terminalconsole/util/package-info.java | 7 + src/main/resources/log4j2.xml | 28 +- src/main/resources/log4j2_server.xml | 52 +++ 18 files changed, 1106 insertions(+), 554 deletions(-) delete mode 100644 src/main/java/net/minecraftforge/server/console/ConsoleFormatter.java delete mode 100644 src/main/java/net/minecraftforge/server/console/TerminalConsoleAppender.java delete mode 100644 src/main/java/net/minecraftforge/server/console/log4j/TerminalConsoleAppender.java create mode 100644 src/main/java/net/minecraftforge/server/terminalconsole/HighlightErrorConverter.java create mode 100644 src/main/java/net/minecraftforge/server/terminalconsole/MinecraftFormattingConverter.java create mode 100644 src/main/java/net/minecraftforge/server/terminalconsole/TerminalConsoleAppender.java create mode 100644 src/main/java/net/minecraftforge/server/terminalconsole/package-info.java create mode 100644 src/main/java/net/minecraftforge/server/terminalconsole/util/LoggerNamePatternSelector.java create mode 100644 src/main/java/net/minecraftforge/server/terminalconsole/util/package-info.java create mode 100644 src/main/resources/log4j2_server.xml diff --git a/jsons/1.12.2-dev.json b/jsons/1.12.2-dev.json index bd304fddf..6b7cc121f 100644 --- a/jsons/1.12.2-dev.json +++ b/jsons/1.12.2-dev.json @@ -10,7 +10,7 @@ "name": "net.minecraft:launchwrapper:1.12" }, { - "name": "jline:jline:2.13", + "name": "org.jline:jline:3.5.1", "children": ["sources"], "url" : "http://repo.maven.apache.org/maven2" }, diff --git a/jsons/1.12.2-rel.json b/jsons/1.12.2-rel.json index 26ef1aa8b..6688013fa 100644 --- a/jsons/1.12.2-rel.json +++ b/jsons/1.12.2-rel.json @@ -38,9 +38,14 @@ "clientreq":true }, { - "name": "jline:jline:2.13", + "name": "org.jline:jline:3.5.1", "url" : "http://files.minecraftforge.net/maven/", - "checksums" : [ "2d9530d0a25daffaffda7c35037b046b627bb171" ], + "checksums" : [ "51800e9d7a13608894a5a28eed0f5c7fa2f300fb" ], + "serverreq":true, + "clientreq":false + }, + { + "name": "net.java.dev.jna:jna:4.4.0", "serverreq":true, "clientreq":false }, 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 7c0a7f8f8..564b4b24a 100644 --- a/src/main/java/net/minecraftforge/fml/common/launcher/FMLServerTweaker.java +++ b/src/main/java/net/minecraftforge/fml/common/launcher/FMLServerTweaker.java @@ -37,7 +37,7 @@ public class FMLServerTweaker extends FMLTweaker { if (System.getProperty("log4j.configurationFile") == null) { - System.setProperty("log4j.configurationFile", "log4j2.xml"); + System.setProperty("log4j.configurationFile", "log4j2_server.xml"); ((LoggerContext) LogManager.getContext(false)).reconfigure(); } } @@ -55,9 +55,9 @@ public class FMLServerTweaker extends FMLTweaker { // 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.log4j.TerminalConsoleAppender"); + classLoader.addClassLoaderExclusion("org.jline."); + classLoader.addClassLoaderExclusion("com.sun.jna."); + classLoader.addClassLoaderExclusion("net.minecraftforge.server.terminalconsole."); FMLLaunchHandler.configureForServerLaunch(classLoader, this); FMLLaunchHandler.appendCoreMods(); diff --git a/src/main/java/net/minecraftforge/fml/relauncher/CoreModManager.java b/src/main/java/net/minecraftforge/fml/relauncher/CoreModManager.java index cb64d02e3..aadddbf52 100644 --- a/src/main/java/net/minecraftforge/fml/relauncher/CoreModManager.java +++ b/src/main/java/net/minecraftforge/fml/relauncher/CoreModManager.java @@ -66,6 +66,8 @@ import com.google.common.collect.Maps; import com.google.common.collect.ObjectArrays; import com.google.common.collect.Sets; import com.google.common.primitives.Ints; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; public class CoreModManager { private static final Attributes.Name COREMODCONTAINSFMLMOD = new Attributes.Name("FMLCorePluginContainsFMLMod"); @@ -214,6 +216,16 @@ public class CoreModManager { { FMLLog.log.debug("Enabling runtime deobfuscation"); } + else + { + if (System.getProperty("log4j.configurationFile") == null) + { + FMLLog.log.info("Detected deobfuscated environment, loading log configs for colored console logs."); + // use server logging configs in deobfuscated environment so developers get nicely colored console logs + System.setProperty("log4j.configurationFile", "log4j2_server.xml"); + ((LoggerContext) LogManager.getContext(false)).reconfigure(); + } + } tweaker.injectCascadingTweak("net.minecraftforge.fml.common.launcher.FMLInjectionAndSortingTweaker"); try diff --git a/src/main/java/net/minecraftforge/fml/relauncher/ServerLaunchWrapper.java b/src/main/java/net/minecraftforge/fml/relauncher/ServerLaunchWrapper.java index e21494964..2ef4f24f5 100644 --- a/src/main/java/net/minecraftforge/fml/relauncher/ServerLaunchWrapper.java +++ b/src/main/java/net/minecraftforge/fml/relauncher/ServerLaunchWrapper.java @@ -21,6 +21,9 @@ package net.minecraftforge.fml.relauncher; import java.lang.reflect.Method; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; + public class ServerLaunchWrapper { /** @@ -38,6 +41,11 @@ public class ServerLaunchWrapper { private void run(String[] args) { + if (System.getProperty("log4j.configurationFile") == null) + { + // Set this early so we don't need to reconfigure later + System.setProperty("log4j.configurationFile", "log4j2_server.xml"); + } Class launchwrapper = null; try { diff --git a/src/main/java/net/minecraftforge/server/console/ConsoleCommandCompleter.java b/src/main/java/net/minecraftforge/server/console/ConsoleCommandCompleter.java index 541ecec86..73f69c76b 100644 --- a/src/main/java/net/minecraftforge/server/console/ConsoleCommandCompleter.java +++ b/src/main/java/net/minecraftforge/server/console/ConsoleCommandCompleter.java @@ -21,21 +21,21 @@ package net.minecraftforge.server.console; import static com.google.common.base.Preconditions.checkNotNull; -import java.util.Collections; 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; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; -public final class ConsoleCommandCompleter implements Completer +final class ConsoleCommandCompleter implements Completer { - private static final Logger logger = LogManager.getLogger(); private final DedicatedServer server; @@ -45,10 +45,9 @@ public final class ConsoleCommandCompleter implements Completer } @Override - public int complete(String buffer, int cursor, List candidates) + public void complete(LineReader reader, ParsedLine line, List candidates) { - int len = buffer.length(); - + String buffer = line.line(); boolean prefix; if (buffer.isEmpty() || buffer.charAt(0) != '/') { @@ -61,44 +60,19 @@ public final class ConsoleCommandCompleter implements Completer } final String input = buffer; - Future> tabComplete = this.server.callFromMainThread(new Callable>() { + Future> tabComplete = this.server.callFromMainThread(() -> this.server.getTabCompletions(this.server, input, this.server.getPosition(), false)); - @Override - public List call() throws Exception - { - return ConsoleCommandCompleter.this.server.getTabCompletions(ConsoleCommandCompleter.this.server, input, - ConsoleCommandCompleter.this.server.getPosition(), false/* we're not a command block */); - } - }); try { - List completions = tabComplete.get(); - Collections.sort(completions); - if (prefix) + for (String completion : tabComplete.get()) { - candidates.addAll(completions); - } - else - { - for (String completion : completions) + if (!completion.isEmpty()) { - candidates.add(completion.charAt(0) == '/' ? completion.substring(1) : completion); + boolean hasPrefix = prefix || completion.charAt(0) != '/'; + Candidate candidate = new Candidate(hasPrefix ? completion : completion.substring(1)); + candidates.add(candidate); } } - - int pos = buffer.lastIndexOf(' '); - if (pos == -1) - { - return cursor - len; - } - else if (prefix) - { - return cursor - len + pos + 1; - } - else - { - return cursor - len + pos; - } } catch (InterruptedException e) { @@ -108,8 +82,6 @@ public final class ConsoleCommandCompleter implements Completer { 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 deleted file mode 100644 index 638b9e8ef..000000000 --- a/src/main/java/net/minecraftforge/server/console/ConsoleFormatter.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Minecraft Forge - * Copyright (c) 2016. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation version 2.1 - * of the License. - * - * This library 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -package net.minecraftforge.server.console; - -import java.util.Map; -import java.util.regex.Pattern; - -import org.fusesource.jansi.Ansi; - -import java.util.function.Function; -import com.google.common.collect.ImmutableMap; - -import net.minecraft.util.text.TextFormatting; - -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(TextFormatting.BLACK), Ansi.ansi().reset().fg(Ansi.Color.BLACK).boldOff().toString()) - .put(compile(TextFormatting.DARK_BLUE), Ansi.ansi().reset().fg(Ansi.Color.BLUE).boldOff().toString()) - .put(compile(TextFormatting.DARK_GREEN), Ansi.ansi().reset().fg(Ansi.Color.GREEN).boldOff().toString()) - .put(compile(TextFormatting.DARK_AQUA), Ansi.ansi().reset().fg(Ansi.Color.CYAN).boldOff().toString()) - .put(compile(TextFormatting.DARK_RED), Ansi.ansi().reset().fg(Ansi.Color.RED).boldOff().toString()) - .put(compile(TextFormatting.DARK_PURPLE), Ansi.ansi().reset().fg(Ansi.Color.MAGENTA).boldOff().toString()) - .put(compile(TextFormatting.GOLD), Ansi.ansi().reset().fg(Ansi.Color.YELLOW).boldOff().toString()) - .put(compile(TextFormatting.GRAY), Ansi.ansi().reset().fg(Ansi.Color.WHITE).boldOff().toString()) - .put(compile(TextFormatting.DARK_GRAY), Ansi.ansi().reset().fg(Ansi.Color.BLACK).bold().toString()) - .put(compile(TextFormatting.BLUE), Ansi.ansi().reset().fg(Ansi.Color.BLUE).bold().toString()) - .put(compile(TextFormatting.GREEN), Ansi.ansi().reset().fg(Ansi.Color.GREEN).bold().toString()) - .put(compile(TextFormatting.AQUA), Ansi.ansi().reset().fg(Ansi.Color.CYAN).bold().toString()) - .put(compile(TextFormatting.RED), Ansi.ansi().reset().fg(Ansi.Color.RED).bold().toString()) - .put(compile(TextFormatting.LIGHT_PURPLE), Ansi.ansi().reset().fg(Ansi.Color.MAGENTA).bold().toString()) - .put(compile(TextFormatting.YELLOW), Ansi.ansi().reset().fg(Ansi.Color.YELLOW).bold().toString()) - .put(compile(TextFormatting.WHITE), Ansi.ansi().reset().fg(Ansi.Color.WHITE).bold().toString()) - .put(compile(TextFormatting.OBFUSCATED), Ansi.ansi().a(Ansi.Attribute.BLINK_SLOW).toString()) - .put(compile(TextFormatting.BOLD), Ansi.ansi().a(Ansi.Attribute.UNDERLINE_DOUBLE).toString()) - .put(compile(TextFormatting.STRIKETHROUGH), Ansi.ansi().a(Ansi.Attribute.STRIKETHROUGH_ON).toString()) - .put(compile(TextFormatting.UNDERLINE), Ansi.ansi().a(Ansi.Attribute.UNDERLINE).toString()) - .put(compile(TextFormatting.ITALIC), Ansi.ansi().a(Ansi.Attribute.ITALIC).toString()) - .put(compile(TextFormatting.RESET), RESET) - .build(); - - private static Pattern compile(TextFormatting 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 deleted file mode 100644 index 15c995e4d..000000000 --- a/src/main/java/net/minecraftforge/server/console/TerminalConsoleAppender.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Minecraft Forge - * Copyright (c) 2016. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation version 2.1 - * of the License. - * - * This library 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -package net.minecraftforge.server.console; - -import static jline.TerminalFactory.OFF; -import static jline.console.ConsoleReader.RESET_LINE; - -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.core.util.Booleans; -import org.apache.logging.log4j.util.PropertiesUtil; -import org.fusesource.jansi.AnsiConsole; - -import java.util.function.Function; -import com.google.common.base.Functions; - -import jline.TerminalFactory; -import jline.console.ConsoleReader; - -import javax.annotation.Nullable; - -/** - * TODO 1.13 remove this class - * @deprecated use the version in the log4j sub-package: {@link net.minecraftforge.server.console.log4j.TerminalConsoleAppender} - * This is being moved into its own package so that the log configuration doesn't load this whole package, - * which has references to Minecraft in it that can load lots of extra classes. - */ -@Deprecated -@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 - @Nullable - public static TerminalConsoleAppender createAppender(@PluginAttribute("name") String name, @PluginElement("Filters") Filter filter, - @PluginElement("Layout") Layout layout, @PluginAttribute(value = "ignoreExceptions", defaultBoolean = true) String ignore) - { - - if (name == null) - { - LOGGER.error("No name provided for TerminalConsoleAppender"); - return null; - } - if (layout == null) - { - layout = PatternLayout.newBuilder().build(); - } - - boolean ignoreExceptions = Booleans.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 index 3f5628860..c57b09cc9 100644 --- a/src/main/java/net/minecraftforge/server/console/TerminalHandler.java +++ b/src/main/java/net/minecraftforge/server/console/TerminalHandler.java @@ -19,63 +19,73 @@ package net.minecraftforge.server.console; -import java.io.IOException; +import net.minecraftforge.server.terminalconsole.TerminalConsoleAppender; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import jline.console.ConsoleReader; import net.minecraft.server.dedicated.DedicatedServer; -import net.minecraft.util.text.TextFormatting; -import net.minecraftforge.server.console.log4j.TerminalConsoleAppender; +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 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)); + final Terminal terminal = TerminalConsoleAppender.getTerminal(); + if (terminal == null) + return false; + LineReader reader = LineReaderBuilder.builder() + .appName("Forge") + .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.isServerStopped() && server.isServerRunning()) { try { line = reader.readLine("> "); - if (line == null) - { - break; - } - - line = line.trim(); - if (!line.isEmpty()) - { - server.addPendingCommand(line, server); - } } - catch (IOException e) + catch (EndOfFileException ignored) { - logger.error("Exception handling console input", e); + // Continue reading after EOT + continue; + } + + if (line == null) + break; + + line = line.trim(); + if (!line.isEmpty()) + { + server.addPendingCommand(line, server); } } - - return true; } - else + catch (UserInterruptException e) { - TerminalConsoleAppender.setFormatter(TextFormatting::getTextWithoutFormattingCodes); - return false; + server.initiateShutdown(); } + finally + { + TerminalConsoleAppender.setReader(null); + } + + return true; } } diff --git a/src/main/java/net/minecraftforge/server/console/log4j/TerminalConsoleAppender.java b/src/main/java/net/minecraftforge/server/console/log4j/TerminalConsoleAppender.java deleted file mode 100644 index aaedafaa1..000000000 --- a/src/main/java/net/minecraftforge/server/console/log4j/TerminalConsoleAppender.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Minecraft Forge - * Copyright (c) 2016. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation version 2.1 - * of the License. - * - * This library 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 - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -package net.minecraftforge.server.console.log4j; - -import javax.annotation.Nullable; -import java.io.IOException; -import java.io.PrintStream; -import java.io.Serializable; -import java.io.Writer; -import java.util.function.Function; - -import static jline.TerminalFactory.OFF; -import static jline.console.ConsoleReader.RESET_LINE; - -import com.google.common.base.Functions; -import jline.TerminalFactory; -import jline.console.ConsoleReader; -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.core.util.Booleans; -import org.apache.logging.log4j.util.PropertiesUtil; -import org.fusesource.jansi.AnsiConsole; - -@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 - @Nullable - public static TerminalConsoleAppender createAppender(@PluginAttribute("name") String name, @PluginElement("Filters") Filter filter, - @PluginElement("Layout") Layout layout, @PluginAttribute(value = "ignoreExceptions", defaultBoolean = true) String ignore) - { - - if (name == null) - { - LOGGER.error("No name provided for TerminalConsoleAppender"); - return null; - } - if (layout == null) - { - layout = PatternLayout.newBuilder().build(); - } - - boolean ignoreExceptions = Booleans.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/terminalconsole/HighlightErrorConverter.java b/src/main/java/net/minecraftforge/server/terminalconsole/HighlightErrorConverter.java new file mode 100644 index 000000000..78347e3ea --- /dev/null +++ b/src/main/java/net/minecraftforge/server/terminalconsole/HighlightErrorConverter.java @@ -0,0 +1,168 @@ +/* + * TerminalConsoleAppender + * Copyright (c) 2017 Minecrell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package net.minecraftforge.server.terminalconsole; + +import javax.annotation.Nullable; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.layout.PatternLayout; +import org.apache.logging.log4j.core.pattern.ConverterKeys; +import org.apache.logging.log4j.core.pattern.HighlightConverter; +import org.apache.logging.log4j.core.pattern.LogEventPatternConverter; +import org.apache.logging.log4j.core.pattern.PatternConverter; +import org.apache.logging.log4j.core.pattern.PatternFormatter; +import org.apache.logging.log4j.core.pattern.PatternParser; +import org.apache.logging.log4j.util.PerformanceSensitive; + +import java.util.List; + +/** + * A simplified version of {@link HighlightConverter} that uses + * {@link TerminalConsoleAppender} to detect if Ansi escape codes can be used + * to highlight errors and warnings in the console. + * + *

If configured, it will mark all logged errors with a red color and all + * warnings with a yellow color. It can be only used together with + * {@link TerminalConsoleAppender}.

+ * + *

{@link TerminalConsoleAppender#ANSI_OVERRIDE_PROPERTY} may be used + * to force the use of ANSI colors even in unsupported environments.

+ * + *

Example usage: {@code %highlightError{%level: %message}}

+ */ +@Plugin(name = "highlightError", category = PatternConverter.CATEGORY) +@ConverterKeys({ "highlightError" }) +@PerformanceSensitive("allocation") +public class HighlightErrorConverter extends LogEventPatternConverter +{ + private static final String ANSI_RESET = "\u001B[39;0m"; + private static final String ANSI_ERROR = "\u001B[31;1m"; + private static final String ANSI_WARN = "\u001B[33;1m"; + + private final List formatters; + + /** + * Construct the converter. + * + * @param formatters The pattern formatters to generate the text to highlight + */ + protected HighlightErrorConverter(List formatters) + { + super("highlightError", null); + this.formatters = formatters; + } + + @Override + public void format(LogEvent event, StringBuilder toAppendTo) + { + if (TerminalConsoleAppender.isAnsiSupported()) + { + Level level = event.getLevel(); + if (level.isMoreSpecificThan(Level.ERROR)) + { + format(ANSI_ERROR, event, toAppendTo); + return; + } + else if (level.isMoreSpecificThan(Level.WARN)) + { + format(ANSI_WARN, event, toAppendTo); + return; + } + } + + //noinspection ForLoopReplaceableByForEach + for (int i = 0, size = formatters.size(); i < size; i++) + { + formatters.get(i).format(event, toAppendTo); + } + } + + private void format(String style, LogEvent event, StringBuilder toAppendTo) + { + int start = toAppendTo.length(); + toAppendTo.append(style); + int end = toAppendTo.length(); + + //noinspection ForLoopReplaceableByForEach + for (int i = 0, size = formatters.size(); i < size; i++) + { + formatters.get(i).format(event, toAppendTo); + } + + if (toAppendTo.length() == end) + { + // No content so we don't need to append the ANSI escape code + toAppendTo.setLength(start); + } + else + { + // Append reset code after the line + toAppendTo.append(ANSI_RESET); + } + } + + @Override + public boolean handlesThrowable() + { + for (final PatternFormatter formatter : formatters) + { + if (formatter.handlesThrowable()) + { + return true; + } + } + return false; + } + + /** + * Gets a new instance of the {@link HighlightErrorConverter} with the + * specified options. + * + * @param config The current configuration + * @param options The pattern options + * @return The new instance + */ + @Nullable + public static HighlightErrorConverter newInstance(Configuration config, String[] options) + { + if (options.length != 1) + { + LOGGER.error("Incorrect number of options on highlightError. Expected 1 received " + options.length); + return null; + } + if (options[0] == null) + { + LOGGER.error("No pattern supplied on highlightError"); + return null; + } + + PatternParser parser = PatternLayout.createPatternParser(config); + List formatters = parser.parse(options[0]); + return new HighlightErrorConverter(formatters); + } + +} diff --git a/src/main/java/net/minecraftforge/server/terminalconsole/MinecraftFormattingConverter.java b/src/main/java/net/minecraftforge/server/terminalconsole/MinecraftFormattingConverter.java new file mode 100644 index 000000000..b01efba56 --- /dev/null +++ b/src/main/java/net/minecraftforge/server/terminalconsole/MinecraftFormattingConverter.java @@ -0,0 +1,222 @@ +/* + * TerminalConsoleAppender + * Copyright (c) 2017 Minecrell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package net.minecraftforge.server.terminalconsole; + +import javax.annotation.Nullable; + +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.layout.PatternLayout; +import org.apache.logging.log4j.core.pattern.ConverterKeys; +import org.apache.logging.log4j.core.pattern.LogEventPatternConverter; +import org.apache.logging.log4j.core.pattern.PatternConverter; +import org.apache.logging.log4j.core.pattern.PatternFormatter; +import org.apache.logging.log4j.core.pattern.PatternParser; +import org.apache.logging.log4j.util.PerformanceSensitive; +import org.apache.logging.log4j.util.PropertiesUtil; + +import java.util.List; + +/** + * Replaces Minecraft formatting codes in the result of a pattern with + * appropriate ANSI escape codes. The implementation will only replace valid + * color codes using the section sign (§). + * + *

The {@link MinecraftFormattingConverter} can be only used together with + * {@link TerminalConsoleAppender} to detect if the current console supports + * color output. When running in an unsupported environment, it will + * automatically strip all formatting codes instead.

+ * + *

{@link TerminalConsoleAppender#ANSI_OVERRIDE_PROPERTY} may be used + * to force the use of ANSI colors even in unsupported environments. As an + * alternative, {@link #KEEP_FORMATTING_PROPERTY} may be used to keep the + * raw Minecraft formatting codes.

+ * + *

Example usage: {@code %minecraftFormatting{%message}}
+ * It can be configured to always strip formatting codes from the message: + * {@code %minecraftFormatting{%message}{strip}}

+ * + * @see + * Formatting Codes + */ +@Plugin(name = "minecraftFormatting", category = PatternConverter.CATEGORY) +@ConverterKeys({ "minecraftFormatting" }) +@PerformanceSensitive("allocation") +public class MinecraftFormattingConverter extends LogEventPatternConverter +{ + /** + * System property that allows disabling the replacement of Minecraft + * formatting codes entirely, keeping them in the console output. For + * some applications they might be easier and more accurate for parsing + * in applications like certain control panels. + * + *

If this system property is not set, or set to any value except + * {@code true}, all Minecraft formatting codes will be replaced + * or stripped from the console output.

+ */ + public static final String KEEP_FORMATTING_PROPERTY = TerminalConsoleAppender.PROPERTY_PREFIX + ".keepMinecraftFormatting"; + + private static final boolean KEEP_FORMATTING = PropertiesUtil.getProperties().getBooleanProperty(KEEP_FORMATTING_PROPERTY); + + private static final String ANSI_RESET = "\u001B[39;0m"; + + private static final char COLOR_CHAR = '\u00A7'; // § + private static final String LOOKUP = "0123456789abcdefklmnor"; + + private static final String[] ansiCodes = new String[] { + "\u001B[0;30;22m", // Black §0 + "\u001B[0;34;22m", // Dark Blue §1 + "\u001B[0;32;22m", // Dark Green §2 + "\u001B[0;36;22m", // Dark Aqua §3 + "\u001B[0;31;22m", // Dark Red §4 + "\u001B[0;35;22m", // Dark Purple §5 + "\u001B[0;33;22m", // Gold §6 + "\u001B[0;37;22m", // Gray §7 + "\u001B[0;30;1m", // Dark Gray §8 + "\u001B[0;34;1m", // Blue §9 + "\u001B[0;32;1m", // Green §a + "\u001B[0;36;1m", // Aqua §b + "\u001B[0;31;1m", // Red §c + "\u001B[0;35;1m", // Light Purple §d + "\u001B[0;33;1m", // Yellow §e + "\u001B[0;37;1m", // White §f + "\u001B[5m", // Obfuscated §k + "\u001B[21m", // Bold §l + "\u001B[9m", // Strikethrough §m + "\u001B[4m", // Underline §n + "\u001B[3m", // Italic §o + ANSI_RESET, // Reset §r + }; + + private final boolean ansi; + private final List formatters; + + /** + * Construct the converter. + * + * @param formatters The pattern formatters to generate the text to manipulate + * @param strip If true, the converter will strip all formatting codes + */ + protected MinecraftFormattingConverter(List formatters, boolean strip) + { + super("minecraftFormatting", null); + this.formatters = formatters; + this.ansi = !strip; + } + + @Override + public void format(LogEvent event, StringBuilder toAppendTo) + { + int start = toAppendTo.length(); + //noinspection ForLoopReplaceableByForEach + for (int i = 0, size = formatters.size(); i < size; i++) + { + formatters.get(i).format(event, toAppendTo); + } + + if (KEEP_FORMATTING || toAppendTo.length() == start) + { + // Skip replacement if disabled or if the content is empty + return; + } + + String content = toAppendTo.substring(start); + format(content, toAppendTo, start, ansi && TerminalConsoleAppender.isAnsiSupported()); + } + + private static void format(String s, StringBuilder result, int start, boolean ansi) + { + int next = s.indexOf(COLOR_CHAR); + int last = s.length() - 1; + if (next == -1 || next == last) + { + return; + } + + result.setLength(start + next); + + int pos = next; + int format; + do { + if (pos != next) + { + result.append(s, pos, next); + } + + format = LOOKUP.indexOf(s.charAt(next + 1)); + if (format != -1) + { + if (ansi) + { + result.append(ansiCodes[format]); + } + pos = next += 2; + } + else + { + next++; + } + + next = s.indexOf(COLOR_CHAR, next); + } while (next != -1 && next < last); + + result.append(s, pos, s.length()); + if (ansi) + { + result.append(ANSI_RESET); + } + } + + /** + * Gets a new instance of the {@link MinecraftFormattingConverter} with the + * specified options. + * + * @param config The current configuration + * @param options The pattern options + * @return The new instance + * + * @see MinecraftFormattingConverter + */ + @Nullable + public static MinecraftFormattingConverter newInstance(Configuration config, String[] options) + { + if (options.length < 1 || options.length > 2) + { + LOGGER.error("Incorrect number of options on minecraftFormatting. Expected at least 1, max 2 received " + options.length); + return null; + } + if (options[0] == null) + { + LOGGER.error("No pattern supplied on minecraftFormatting"); + return null; + } + + PatternParser parser = PatternLayout.createPatternParser(config); + List formatters = parser.parse(options[0]); + boolean strip = options.length > 1 && "strip".equals(options[1]); + return new MinecraftFormattingConverter(formatters, strip); + } + +} diff --git a/src/main/java/net/minecraftforge/server/terminalconsole/TerminalConsoleAppender.java b/src/main/java/net/minecraftforge/server/terminalconsole/TerminalConsoleAppender.java new file mode 100644 index 000000000..c1dcf8264 --- /dev/null +++ b/src/main/java/net/minecraftforge/server/terminalconsole/TerminalConsoleAppender.java @@ -0,0 +1,374 @@ +/* + * TerminalConsoleAppender + * Copyright (c) 2017 Minecrell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package net.minecraftforge.server.terminalconsole; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.io.PrintStream; +import java.io.Serializable; + +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Core; +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.config.plugins.validation.constraints.Required; +import org.apache.logging.log4j.core.layout.PatternLayout; +import org.apache.logging.log4j.util.PropertiesUtil; +import org.jline.reader.LineReader; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; + +/** + * An {@link Appender} that uses the JLine 3.x {@link Terminal} to print messages + * to the console. + * + *

The JLine {@link Terminal} extends the regular console output with support + * for Ansi escape codes on Windows. Additionally, it's {@link LineReader} + * interface can be used to implement enhanced console input, with an + * persistent input line, as well as command history and command completion.

+ * + *

The {@code TerminalConsole} appender replaces the default {@code Console} + * appender in your log4j configuration. By default, log4j will automatically + * close the standard output when the original {@code Console} appender is + * removed. Consequently, it is necessary to keep an unused {@code Console} + * appender.

+ * + *

Example usage:

+ *
{@code  
+ *     
+ * 
+ *
+ * }
+ * + *

To use the enhanced console input it is necessary to set the + * {@link LineReader} using {@link #setReader(LineReader)}. The appender will + * then automatically redraw the current prompt. When creating the + * {@link LineReader} it's important to use the {@link Terminal} + * returned by {@link #getTerminal()}. Additionally, the reader should + * be removed from the appender as soon as it's no longer accepting + * input (for example when the user interrupted input using CTRL + C.

+ * + *

By default, the JLine {@link Terminal} is enabled when the application + * is started with an attached terminal session. Usually, this is only the + * case if the application is started from the command line, not if it gets + * started by another application.

+ * + *

In some cases, it might be possible to support a subset of the features + * in these unsupported environments (e.g. only ANSI color codes). In these + * cases, the system properties may be used to override the default behaviour: + *

+ * + *
    + *
  • {@link TerminalConsoleAppender#JLINE_OVERRIDE_PROPERTY} - To enable the extended JLine + * input. By default this will also enable the ANSI escape codes.
  • + *
  • {@link TerminalConsoleAppender#ANSI_OVERRIDE_PROPERTY} - To enable the output of ANSI + * escape codes. May be used to force the use of ANSI escape codes + * if JLine is disabled or to disable them if it is enabled.
  • + *
+ */ +@Plugin(name = TerminalConsoleAppender.PLUGIN_NAME, category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE, printObject = true) +public class TerminalConsoleAppender extends AbstractAppender +{ + public static final String PLUGIN_NAME = "TerminalConsole"; + /** + * The prefix used for all system properties in TerminalConsoleAppender. + */ + public static final String PROPERTY_PREFIX = "terminal"; + /** + * System property that allows overriding the default detection of the + * console to force enable or force disable the use of JLine. In some + * environments the automatic detection might not work properly. + *

+ *

If this system property is not set, or set to an invalid value + * (neither {@code true} nor {@code false}) then we will attempt + * to detect the best option automatically.

+ */ + public static final String JLINE_OVERRIDE_PROPERTY = PROPERTY_PREFIX + ".jline"; + /** + * System property that allows overriding the use of ANSI escape codes + * for console formatting even though running in an unsupported + * environment. By default, ANSI color codes are only enabled if JLine + * is enabled. Some systems might be able to handle ANSI escape codes + * but are not capable of JLine's extended input mechanism. + *

+ *

If this system property is not set, or set to an invalid value + * (neither {@code true} nor {@code false}) then we will attempt + * to detect the best option automatically.

+ */ + public static final String ANSI_OVERRIDE_PROPERTY = PROPERTY_PREFIX + ".ansi"; + public static final Boolean ANSI_OVERRIDE = getOptionalBooleanProperty(ANSI_OVERRIDE_PROPERTY); + + /** + * We grab the standard output {@link PrintStream} early, otherwise we + * might cause infinite loops later if the application redirects + * {@link System#out} to Log4J. + */ + private static final PrintStream stdout = System.out; + + private static boolean initialized; + @Nullable + private static Terminal terminal; + @Nullable + private static LineReader reader; + + /** + * Returns the {@link Terminal} that is used to print messages to the + * console. Returns {@code null} in unsupported environments, unless + * overridden using the {@link TerminalConsoleAppender#JLINE_OVERRIDE_PROPERTY} system + * property. + * + * @return The terminal, or null if not supported + * @see TerminalConsoleAppender + */ + @Nullable + public static Terminal getTerminal() + { + return terminal; + } + + /** + * Returns the currently configured {@link LineReader} that is used to + * read input from the console. May be null if no {@link LineReader} + * was configured by the environment. + * + * @return The current line reader, or null if none + */ + @Nullable + public static LineReader getReader() + { + return reader; + } + + /** + * Sets the {@link LineReader} that is used to read input from the console. + * Setting the {@link LineReader} will allow the appender to automatically + * redraw the input line when a new log message is added. + * + *

Note: The specified {@link LineReader} must be created with + * the terminal returned by {@link #getTerminal()}.

+ * + * @param newReader The new line reader + */ + public static void setReader(@Nullable LineReader newReader) + { + if (newReader != null && newReader.getTerminal() != terminal) + { + throw new IllegalArgumentException("Reader was not created with TerminalConsoleAppender.getTerminal()"); + } + + reader = newReader; + } + + /** + * Returns whether ANSI escapes codes should be written to the console + * output. + * + *

The return value is {@code true} by default if the JLine terminal + * is enabled and {@code false} otherwise. It may be overridden using + * the {@link TerminalConsoleAppender#ANSI_OVERRIDE_PROPERTY} system property.

+ * + * @return true if ANSI escapes codes should be written to the console + */ + public static boolean isAnsiSupported() + { + return ANSI_OVERRIDE != null ? ANSI_OVERRIDE : terminal != null; + } + + /** + * Constructs a new {@link TerminalConsoleAppender}. + * + * @param name The name of the appender + * @param filter The filter, can be {@code null} + * @param layout The layout to use + * @param ignoreExceptions If {@code true} exceptions encountered when + * appending events are logged, otherwise they are propagated to the + * caller + */ + protected TerminalConsoleAppender(String name, @Nullable Filter filter, Layout layout, boolean ignoreExceptions) + { + super(name, filter, layout, ignoreExceptions); + initializeTerminal(); + } + + private static void initializeTerminal() + { + if (!initialized) + { + initialized = true; + + // A system property can be used to override our automatic detection + Boolean jlineOverride = getOptionalBooleanProperty(JLINE_OVERRIDE_PROPERTY); + + // By default, we disable JLine if there is no terminal attached + // (e.g. if the program output is redirected to a file or if it's + // started by some kind of control panel) + + // The same applies to IDEs, they usually provide only a very basic + // console implementation without support for ANSI escape codes + // (used for colors) or characters like \r. + + // There are two exceptions: + // 1. IntelliJ IDEA supports colors and control characters + // (We try to detect it using an additional JAR it adds to the classpath) + // 2. The system property forces the use of JLine. + boolean dumb = jlineOverride == Boolean.TRUE || System.getProperty("java.class.path").contains("idea_rt.jar"); + + if (jlineOverride != Boolean.FALSE) + { + try + { + terminal = TerminalBuilder.builder().dumb(dumb).build(); + } + catch (IllegalStateException e) + { + // Unless disabled using one of the exceptions above, + // JLine throws an exception before creating a dumb terminal + // Dumb terminals are used if there is no real terminal attached + // to the application. + + if (LOGGER.isDebugEnabled()) + { + // Log with stacktrace + LOGGER.warn("Disabling terminal, you're running in an unsupported environment.", e); + } + else + { + LOGGER.warn("Disabling terminal, you're running in an unsupported environment."); + } + } + catch (IOException e) + { + LOGGER.error("Failed to initialize terminal. Falling back to standard output", e); + } + } + } + } + + @Nullable + private static Boolean getOptionalBooleanProperty(String name) + { + String value = PropertiesUtil.getProperties().getStringProperty(name); + if (value == null) + { + return null; + } + + if (value.equalsIgnoreCase("true")) + { + return Boolean.TRUE; + } + else if (value.equalsIgnoreCase("false")) + { + return Boolean.FALSE; + } + else + { + LOGGER.warn("Invalid value for boolean input property '{}': {}", name, value); + return null; + } + } + + @Override + public void append(LogEvent event) + { + if (terminal != null) + { + if (reader != null) + { + // Draw the prompt line again if a reader is available + reader.callWidget(LineReader.CLEAR); + terminal.writer().print(getLayout().toSerializable(event)); + reader.callWidget(LineReader.REDRAW_LINE); + reader.callWidget(LineReader.REDISPLAY); + } + else + { + terminal.writer().print(getLayout().toSerializable(event)); + } + + terminal.writer().flush(); + } + else + { + stdout.print(getLayout().toSerializable(event)); + } + } + + /** + * Closes the JLine {@link Terminal} (if available) and restores the original + * terminal settings. + * + * @throws IOException If an I/O error occurs + */ + public static void close() throws IOException + { + if (initialized) + { + initialized = false; + if (terminal != null) + { + try + { + terminal.close(); + } + finally + { + terminal = null; + } + } + } + } + + /** + * Creates a new {@link TerminalConsoleAppender}. + * + * @param name The name of the appender + * @param filter The filter, can be {@code null} + * @param layout The layout, can be {@code null} + * @param ignoreExceptions If {@code true} exceptions encountered when + * appending events are logged, otherwise they are propagated to the + * caller + * @return The new appender + */ + @PluginFactory + public static TerminalConsoleAppender createAppender( + @Required(message = "No name provided for TerminalConsoleAppender") @PluginAttribute("name") String name, + @PluginElement("Filter") @Nullable Filter filter, + @PluginElement("Layout") @Nullable Layout layout, + @PluginAttribute(value = "ignoreExceptions", defaultBoolean = true) boolean ignoreExceptions) + { + if (layout == null) + { + layout = PatternLayout.createDefaultLayout(); + } + + return new TerminalConsoleAppender(name, filter, layout, ignoreExceptions); + } +} diff --git a/src/main/java/net/minecraftforge/server/terminalconsole/package-info.java b/src/main/java/net/minecraftforge/server/terminalconsole/package-info.java new file mode 100644 index 000000000..fabc845d6 --- /dev/null +++ b/src/main/java/net/minecraftforge/server/terminalconsole/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package net.minecraftforge.server.terminalconsole; + +import javax.annotation.ParametersAreNonnullByDefault; + +import mcp.MethodsReturnNonnullByDefault; \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/server/terminalconsole/util/LoggerNamePatternSelector.java b/src/main/java/net/minecraftforge/server/terminalconsole/util/LoggerNamePatternSelector.java new file mode 100644 index 000000000..88d58d08d --- /dev/null +++ b/src/main/java/net/minecraftforge/server/terminalconsole/util/LoggerNamePatternSelector.java @@ -0,0 +1,171 @@ +/* + * TerminalConsoleAppender + * Copyright (c) 2017 Minecrell + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package net.minecraftforge.server.terminalconsole.util; + +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.Node; +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.PluginConfiguration; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required; +import org.apache.logging.log4j.core.layout.PatternLayout; +import org.apache.logging.log4j.core.layout.PatternMatch; +import org.apache.logging.log4j.core.layout.PatternSelector; +import org.apache.logging.log4j.core.pattern.PatternFormatter; +import org.apache.logging.log4j.core.pattern.PatternParser; +import org.apache.logging.log4j.util.PerformanceSensitive; + +import java.util.ArrayList; +import java.util.List; + +/** + * A {@link PatternSelector} that selects patterns based on the logger name. + * Can be used to log messages from different loggers using different patterns. + * + *

Multiple logger names may be separated using comma in the + * {@link PatternMatch#getKey() PatternMatch "key"}. The pattern will be applied + * if the logger name matches at least one of them.

+ * + *

Example usage:

+ *
{@code  
+ *     
+ *         
+ *         
+ *         
+ *     
+ * }
+ */ +@Plugin(name = "LoggerNamePatternSelector", category = Node.CATEGORY, elementType = PatternSelector.ELEMENT_TYPE) +@PerformanceSensitive("allocation") +public class LoggerNamePatternSelector implements PatternSelector +{ + private static class LoggerNameSelector + { + private final String name; + private final boolean isPackage; + private final PatternFormatter[] formatters; + + LoggerNameSelector(String name, PatternFormatter[] formatters) + { + this.name = name; + this.isPackage = name.endsWith("."); + this.formatters = formatters; + } + + PatternFormatter[] get() + { + return this.formatters; + } + + boolean test(String s) + { + return this.isPackage ? s.startsWith(this.name) : s.equals(this.name); + } + + } + + private final PatternFormatter[] defaultFormatters; + private final List formatters = new ArrayList<>(); + + /** + * Constructs a new {@link LoggerNamePatternSelector}. + * + * @param defaultPattern The default pattern to use if no logger name matches + * @param properties The pattern match rules to use + * @param alwaysWriteExceptions Write exceptions even if pattern does not + * include exception conversion + * @param disableAnsi If true, disable all ANSI escape codes + * @param noConsoleNoAnsi If true and {@link System#console()} is null, + * disable ANSI escape codes + * @param config The configuration + */ + protected LoggerNamePatternSelector(String defaultPattern, PatternMatch[] properties, + boolean alwaysWriteExceptions, boolean disableAnsi, boolean noConsoleNoAnsi, Configuration config) + { + PatternParser parser = PatternLayout.createPatternParser(config); + this.defaultFormatters = toArray(parser.parse(defaultPattern, alwaysWriteExceptions, disableAnsi, noConsoleNoAnsi)); + for (PatternMatch property : properties) + { + PatternFormatter[] formatters = toArray(parser.parse(property.getPattern(), alwaysWriteExceptions, disableAnsi, noConsoleNoAnsi)); + for (String name : property.getKey().split(",")) + { + this.formatters.add(new LoggerNameSelector(name, formatters)); + } + } + } + + private static PatternFormatter[] toArray(List formatters) + { + return formatters.toArray(new PatternFormatter[formatters.size()]); + } + + @Override + public PatternFormatter[] getFormatters(LogEvent event) + { + final String loggerName = event.getLoggerName(); + if (loggerName != null) + { + //noinspection ForLoopReplaceableByForEach + for (int i = 0; i < this.formatters.size(); i++) + { + LoggerNameSelector selector = this.formatters.get(i); + if (selector.test(loggerName)) + { + return selector.get(); + } + } + } + + return this.defaultFormatters; + } + + /** + * Creates a new {@link LoggerNamePatternSelector}. + * + * @param defaultPattern The default pattern to use if no logger name matches + * @param properties The pattern match rules to use + * @param alwaysWriteExceptions Write exceptions even if pattern does not + * include exception conversion + * @param disableAnsi If true, disable all ANSI escape codes + * @param noConsoleNoAnsi If true and {@link System#console()} is null, + * disable ANSI escape codes + * @param config The configuration + * @return The new pattern selector + */ + @PluginFactory + public static LoggerNamePatternSelector createSelector( + @Required(message = "Default pattern is required") @PluginAttribute(value = "defaultPattern") String defaultPattern, + @PluginElement("PatternMatch") PatternMatch[] properties, + @PluginAttribute(value = "alwaysWriteExceptions", defaultBoolean = true) boolean alwaysWriteExceptions, + @PluginAttribute("disableAnsi") boolean disableAnsi, + @PluginAttribute("noConsoleNoAnsi") boolean noConsoleNoAnsi, + @PluginConfiguration Configuration config) + { + return new LoggerNamePatternSelector(defaultPattern, properties, alwaysWriteExceptions, disableAnsi, noConsoleNoAnsi, config); + } + +} diff --git a/src/main/java/net/minecraftforge/server/terminalconsole/util/package-info.java b/src/main/java/net/minecraftforge/server/terminalconsole/util/package-info.java new file mode 100644 index 000000000..583a294b0 --- /dev/null +++ b/src/main/java/net/minecraftforge/server/terminalconsole/util/package-info.java @@ -0,0 +1,7 @@ +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +package net.minecraftforge.server.terminalconsole.util; + +import javax.annotation.ParametersAreNonnullByDefault; + +import mcp.MethodsReturnNonnullByDefault; \ No newline at end of file diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index ae512a627..c78f0ea72 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -1,13 +1,23 @@ - + - - - - - + + + + + + + + + - + + + + + + + @@ -33,9 +43,9 @@ - - + + diff --git a/src/main/resources/log4j2_server.xml b/src/main/resources/log4j2_server.xml new file mode 100644 index 000000000..06eab23b2 --- /dev/null +++ b/src/main/resources/log4j2_server.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +