Improve server console implementation (#4821)

This commit is contained in:
mezz 2018-06-27 10:58:07 -07:00 committed by LexManos
parent 43895d773e
commit 1db28d072a
18 changed files with 1106 additions and 554 deletions

View file

@ -10,7 +10,7 @@
"name": "net.minecraft:launchwrapper:1.12" "name": "net.minecraft:launchwrapper:1.12"
}, },
{ {
"name": "jline:jline:2.13", "name": "org.jline:jline:3.5.1",
"children": ["sources"], "children": ["sources"],
"url" : "http://repo.maven.apache.org/maven2" "url" : "http://repo.maven.apache.org/maven2"
}, },

View file

@ -38,9 +38,14 @@
"clientreq":true "clientreq":true
}, },
{ {
"name": "jline:jline:2.13", "name": "org.jline:jline:3.5.1",
"url" : "http://files.minecraftforge.net/maven/", "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, "serverreq":true,
"clientreq":false "clientreq":false
}, },

View file

@ -37,7 +37,7 @@ public class FMLServerTweaker extends FMLTweaker {
if (System.getProperty("log4j.configurationFile") == null) if (System.getProperty("log4j.configurationFile") == null)
{ {
System.setProperty("log4j.configurationFile", "log4j2.xml"); System.setProperty("log4j.configurationFile", "log4j2_server.xml");
((LoggerContext) LogManager.getContext(false)).reconfigure(); ((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 // and deobfuscated parts of the code. Without, the UI won't show anything
classLoader.addClassLoaderExclusion("com.mojang.util.QueueLogAppender"); classLoader.addClassLoaderExclusion("com.mojang.util.QueueLogAppender");
classLoader.addClassLoaderExclusion("jline."); classLoader.addClassLoaderExclusion("org.jline.");
classLoader.addClassLoaderExclusion("org.fusesource."); classLoader.addClassLoaderExclusion("com.sun.jna.");
classLoader.addClassLoaderExclusion("net.minecraftforge.server.console.log4j.TerminalConsoleAppender"); classLoader.addClassLoaderExclusion("net.minecraftforge.server.terminalconsole.");
FMLLaunchHandler.configureForServerLaunch(classLoader, this); FMLLaunchHandler.configureForServerLaunch(classLoader, this);
FMLLaunchHandler.appendCoreMods(); FMLLaunchHandler.appendCoreMods();

View file

@ -66,6 +66,8 @@ import com.google.common.collect.Maps;
import com.google.common.collect.ObjectArrays; import com.google.common.collect.ObjectArrays;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
public class CoreModManager { public class CoreModManager {
private static final Attributes.Name COREMODCONTAINSFMLMOD = new Attributes.Name("FMLCorePluginContainsFMLMod"); private static final Attributes.Name COREMODCONTAINSFMLMOD = new Attributes.Name("FMLCorePluginContainsFMLMod");
@ -214,6 +216,16 @@ public class CoreModManager {
{ {
FMLLog.log.debug("Enabling runtime deobfuscation"); 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"); tweaker.injectCascadingTweak("net.minecraftforge.fml.common.launcher.FMLInjectionAndSortingTweaker");
try try

View file

@ -21,6 +21,9 @@ package net.minecraftforge.fml.relauncher;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
public class ServerLaunchWrapper { public class ServerLaunchWrapper {
/** /**
@ -38,6 +41,11 @@ public class ServerLaunchWrapper {
private void run(String[] args) 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; Class<?> launchwrapper = null;
try try
{ {

View file

@ -21,21 +21,21 @@ package net.minecraftforge.server.console;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import jline.console.completer.Completer;
import net.minecraft.server.dedicated.DedicatedServer; 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 static final Logger logger = LogManager.getLogger();
private final DedicatedServer server; private final DedicatedServer server;
@ -45,10 +45,9 @@ public final class ConsoleCommandCompleter implements Completer
} }
@Override @Override
public int complete(String buffer, int cursor, List<CharSequence> candidates) public void complete(LineReader reader, ParsedLine line, List<Candidate> candidates)
{ {
int len = buffer.length(); String buffer = line.line();
boolean prefix; boolean prefix;
if (buffer.isEmpty() || buffer.charAt(0) != '/') if (buffer.isEmpty() || buffer.charAt(0) != '/')
{ {
@ -61,44 +60,19 @@ public final class ConsoleCommandCompleter implements Completer
} }
final String input = buffer; final String input = buffer;
Future<List<String>> tabComplete = this.server.callFromMainThread(new Callable<List<String>>() { Future<List<String>> tabComplete = this.server.callFromMainThread(() -> this.server.getTabCompletions(this.server, input, this.server.getPosition(), false));
@Override
public List<String> call() throws Exception
{
return ConsoleCommandCompleter.this.server.getTabCompletions(ConsoleCommandCompleter.this.server, input,
ConsoleCommandCompleter.this.server.getPosition(), false/* we're not a command block */);
}
});
try try
{ {
List<String> completions = tabComplete.get(); for (String completion : tabComplete.get())
Collections.sort(completions);
if (prefix)
{ {
candidates.addAll(completions); if (!completion.isEmpty())
}
else
{
for (String completion : completions)
{ {
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) catch (InterruptedException e)
{ {
@ -108,8 +82,6 @@ public final class ConsoleCommandCompleter implements Completer
{ {
logger.error("Failed to tab complete", e); logger.error("Failed to tab complete", e);
} }
return cursor;
} }
} }

View file

@ -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<String, String>
{
public ConsoleFormatter()
{
}
private static final String RESET = Ansi.ansi().reset().toString();
private static final ImmutableMap<Pattern, String> REPLACEMENTS = ImmutableMap.<Pattern, String> 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<Pattern, String> entry : REPLACEMENTS.entrySet())
{
text = entry.getKey().matcher(text).replaceAll(entry.getValue());
}
return text + RESET;
}
}

View file

@ -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<String, String> formatter = Functions.identity();
public static void setFormatter(Function<String, String> format)
{
formatter = format != null ? format : Functions.identity();
}
protected TerminalConsoleAppender(String name, Filter filter, Layout<? extends Serializable> 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<? extends Serializable> 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());
}
}

View file

@ -19,63 +19,73 @@
package net.minecraftforge.server.console; 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.server.dedicated.DedicatedServer;
import net.minecraft.util.text.TextFormatting; import org.jline.reader.EndOfFileException;
import net.minecraftforge.server.console.log4j.TerminalConsoleAppender; import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.UserInterruptException;
import org.jline.terminal.Terminal;
public final class TerminalHandler public final class TerminalHandler
{ {
private static final Logger logger = LogManager.getLogger();
private TerminalHandler() private TerminalHandler()
{ {
} }
public static boolean handleCommands(DedicatedServer server) public static boolean handleCommands(DedicatedServer server)
{ {
final ConsoleReader reader = TerminalConsoleAppender.getReader(); final Terminal terminal = TerminalConsoleAppender.getTerminal();
if (reader != null) if (terminal == null)
{ return false;
TerminalConsoleAppender.setFormatter(new ConsoleFormatter());
reader.addCompleter(new ConsoleCommandCompleter(server));
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; String line;
while (!server.isServerStopped() && server.isServerRunning()) while (!server.isServerStopped() && server.isServerRunning())
{ {
try try
{ {
line = reader.readLine("> "); 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); server.initiateShutdown();
return false;
} }
finally
{
TerminalConsoleAppender.setReader(null);
}
return true;
} }
} }

View file

@ -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<String, String> formatter = Functions.identity();
public static void setFormatter(Function<String, String> format)
{
formatter = format != null ? format : Functions.identity();
}
protected TerminalConsoleAppender(String name, Filter filter, Layout<? extends Serializable> 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<? extends Serializable> 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());
}
}

View file

@ -0,0 +1,168 @@
/*
* TerminalConsoleAppender
* Copyright (c) 2017 Minecrell <https://github.com/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.
*
* <p>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}.</p>
*
* <p>{@link TerminalConsoleAppender#ANSI_OVERRIDE_PROPERTY} may be used
* to force the use of ANSI colors even in unsupported environments.</p>
*
* <p><b>Example usage:</b> {@code %highlightError{%level: %message}}</p>
*/
@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<PatternFormatter> formatters;
/**
* Construct the converter.
*
* @param formatters The pattern formatters to generate the text to highlight
*/
protected HighlightErrorConverter(List<PatternFormatter> 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<PatternFormatter> formatters = parser.parse(options[0]);
return new HighlightErrorConverter(formatters);
}
}

View file

@ -0,0 +1,222 @@
/*
* TerminalConsoleAppender
* Copyright (c) 2017 Minecrell <https://github.com/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 (§).
*
* <p>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.</p>
*
* <p>{@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.</p>
*
* <p><b>Example usage:</b> {@code %minecraftFormatting{%message}}<br>
* It can be configured to always strip formatting codes from the message:
* {@code %minecraftFormatting{%message}{strip}}</p>
*
* @see <a href="http://minecraft.gamepedia.com/Formatting_codes">
* Formatting Codes</a>
*/
@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.
*
* <p>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.</p>
*/
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<PatternFormatter> 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<PatternFormatter> 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<PatternFormatter> formatters = parser.parse(options[0]);
boolean strip = options.length > 1 && "strip".equals(options[1]);
return new MinecraftFormattingConverter(formatters, strip);
}
}

View file

@ -0,0 +1,374 @@
/*
* TerminalConsoleAppender
* Copyright (c) 2017 Minecrell <https://github.com/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.
*
* <p>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.</p>
*
* <p>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.</p>
*
* <p><b>Example usage:</b></p>
* <pre>{@code <TerminalConsole>
* <PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg%n"/>
* </TerminalConsole>
*
* <Console name="SysOut" target="SYSTEM_OUT"/>}</pre>
*
* <p>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.</p>
*
* <p>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.</p>
*
* <p>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:
* </p>
*
* <ul>
* <li>{@link TerminalConsoleAppender#JLINE_OVERRIDE_PROPERTY} - To enable the extended JLine
* input. By default this will also enable the ANSI escape codes.</li>
* <li>{@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.</li>
* </ul>
*/
@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.
* <p>
* <p>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.</p>
*/
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.
* <p>
* <p>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.</p>
*/
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.
*
* <p><b>Note:</b> The specified {@link LineReader} must be created with
* the terminal returned by {@link #getTerminal()}.</p>
*
* @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.
*
* <p>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.</p>
*
* @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<? extends Serializable> 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<? extends Serializable> layout,
@PluginAttribute(value = "ignoreExceptions", defaultBoolean = true) boolean ignoreExceptions)
{
if (layout == null)
{
layout = PatternLayout.createDefaultLayout();
}
return new TerminalConsoleAppender(name, filter, layout, ignoreExceptions);
}
}

View file

@ -0,0 +1,7 @@
@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
package net.minecraftforge.server.terminalconsole;
import javax.annotation.ParametersAreNonnullByDefault;
import mcp.MethodsReturnNonnullByDefault;

View file

@ -0,0 +1,171 @@
/*
* TerminalConsoleAppender
* Copyright (c) 2017 Minecrell <https://github.com/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.
*
* <p>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.</p>
*
* <p><b>Example usage:</b></p>
* <pre>{@code <PatternLayout>
* <LoggerNamePatternSelector defaultPattern="[%d{HH:mm:ss} %level] [%logger]: %msg%n">
* <!-- Log root (empty logger name), "Main", and net.minecrell.* without logger prefix -->
* <PatternMatch key=",Main,net.minecrell." pattern="[%d{HH:mm:ss} %level]: %msg%n"/>
* <PatternMatch key="com.example.Logger" pattern="EXAMPLE: %msg%n"/>
* </LoggerNamePatternSelector>
* </PatternLayout>}</pre>
*/
@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<LoggerNameSelector> 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<PatternFormatter> 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);
}
}

View file

@ -0,0 +1,7 @@
@ParametersAreNonnullByDefault
@MethodsReturnNonnullByDefault
package net.minecraftforge.server.terminalconsole.util;
import javax.annotation.ParametersAreNonnullByDefault;
import mcp.MethodsReturnNonnullByDefault;

View file

@ -1,13 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" packages="com.mojang.util,net.minecraftforge.server.console.log4j"> <Configuration status="warn" packages="net.minecraftforge.server.terminalconsole.util">
<Appenders> <Appenders>
<TerminalConsole name="Console"> <Console name="SysOut" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level] [%logger]: %msg%n"/> <PatternLayout>
</TerminalConsole> <LoggerNamePatternSelector defaultPattern="[%d{HH:mm:ss}] [%t/%level] [%logger]: %msg%n">
<!-- Keep a console appender open so log4j2 doesn't close our main out stream if we redirect System.out to the logger --> <!-- don't include the full logger name for Mojang's logs since they use full class names and it's very verbose -->
<Console name="SysOut" target="SYSTEM_OUT"/> <PatternMatch key="net.minecraft." pattern="[%d{HH:mm:ss}] [%t/%level] [minecraft/%logger{1}]: %msg%n"/>
<PatternMatch key="com.mojang." pattern="[%d{HH:mm:ss}] [%t/%level] [mojang/%logger{1}]: %msg%n"/>
</LoggerNamePatternSelector>
</PatternLayout>
</Console>
<Queue name="ServerGuiConsole" ignoreExceptions="true"> <Queue name="ServerGuiConsole" ignoreExceptions="true">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level] [%logger]: %msg%n" /> <PatternLayout>
<LoggerNamePatternSelector defaultPattern="[%d{HH:mm:ss}] [%t/%level] [%logger]: %msg%n">
<!-- don't include the full logger name for Mojang's logs since they use full class names and it's very verbose -->
<PatternMatch key="net.minecraft." pattern="[%d{HH:mm:ss}] [%t/%level] [minecraft/%logger{1}]: %msg%n"/>
<PatternMatch key="com.mojang." pattern="[%d{HH:mm:ss}] [%t/%level] [mojang/%logger{1}]: %msg%n"/>
</LoggerNamePatternSelector>
</PatternLayout>
</Queue> </Queue>
<RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz"> <RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level] [%logger]: %msg%n"/> <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level] [%logger]: %msg%n"/>
@ -33,9 +43,9 @@
<filters> <filters>
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL"/> <MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL"/>
</filters> </filters>
<AppenderRef ref="Console" level="info"/> <AppenderRef ref="SysOut" level="info"/>
<AppenderRef ref="File" level="info"/>
<AppenderRef ref="ServerGuiConsole" level="info"/> <AppenderRef ref="ServerGuiConsole" level="info"/>
<AppenderRef ref="File" level="info"/>
<AppenderRef ref="DebugFile"/> <AppenderRef ref="DebugFile"/>
</Root> </Root>
</Loggers> </Loggers>

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" packages="net.minecraftforge.server.terminalconsole">
<Appenders>
<TerminalConsole name="Console">
<PatternLayout>
<LoggerNamePatternSelector defaultPattern="%highlightError{[%d{HH:mm:ss}] [%t/%level] [%logger]: %minecraftFormatting{%msg}%n%xEx}">
<!-- don't include the full logger name for Mojang's logs since they use full class names and it's very verbose -->
<PatternMatch key="net.minecraft." pattern="%highlightError{[%d{HH:mm:ss}] [%t/%level] [minecraft/%logger{1}]: %minecraftFormatting{%msg}%n%xEx}"/>
<PatternMatch key="com.mojang." pattern="%highlightError{[%d{HH:mm:ss}] [%t/%level] [mojang/%logger{1}]: %minecraftFormatting{%msg}%n%xEx}"/>
</LoggerNamePatternSelector>
</PatternLayout>
</TerminalConsole>
<Queue name="ServerGuiConsole" ignoreExceptions="true">
<PatternLayout>
<LoggerNamePatternSelector defaultPattern="[%d{HH:mm:ss}] [%t/%level] [%logger]: %minecraftFormatting{%msg}{strip}%n">
<!-- don't include the full logger name for Mojang's logs since they use full class names and it's very verbose -->
<PatternMatch key="net.minecraft." pattern="[%d{HH:mm:ss}] [%t/%level] [minecraft/%logger{1}]: %minecraftFormatting{%msg}{strip}%n"/>
<PatternMatch key="com.mojang." pattern="[%d{HH:mm:ss}] [%t/%level] [mojang/%logger{1}]: %minecraftFormatting{%msg}{strip}%n"/>
</LoggerNamePatternSelector>
</PatternLayout>
</Queue>
<RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level] [%logger]: %minecraftFormatting{%msg}{strip}%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<OnStartupTriggeringPolicy/>
</Policies>
</RollingRandomAccessFile>
<RollingRandomAccessFile name="DebugFile" fileName="logs/debug.log" filePattern="logs/debug-%i.log.gz">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level] [%logger]: %minecraftFormatting{%msg}{strip}%n"/>
<Policies>
<OnStartupTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="200MB"/>
</Policies>
<DefaultRolloverStrategy max="5" fileIndex="min"/>
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<!-- make sure mojang's logging is set to 'info' so that their LOGGER.isDebugEnabled() behavior isn't active -->
<Logger level="info" name="com.mojang"/>
<Logger level="info" name="net.minecraft"/>
<Root level="all">
<filters>
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL"/>
</filters>
<AppenderRef ref="Console" level="info"/>
<AppenderRef ref="ServerGuiConsole" level="info"/>
<AppenderRef ref="File" level="info"/>
<AppenderRef ref="DebugFile"/>
</Root>
</Loggers>
</Configuration>