package launcher.helper; import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.io.Writer; import java.nio.file.Path; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Collections; import java.util.Locale; import java.util.Objects; import java.util.Set; import java.util.concurrent.BlockingDeque; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.atomic.AtomicBoolean; import launcher.Launcher; import launcher.LauncherAPI; import org.fusesource.jansi.Ansi; import org.fusesource.jansi.AnsiConsole; import org.fusesource.jansi.AnsiOutputStream; public final class LogHelper implements Runnable { @LauncherAPI public static final String DEBUG_PROPERTY = "launcher.debug"; @LauncherAPI public static final boolean JANSI; // Output settings private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy.MM.dd HH:mm:ss", Locale.US); private static final AtomicBoolean DEBUG_ENABLED = new AtomicBoolean(Boolean.getBoolean(DEBUG_PROPERTY)); private static final Set<Output> OUTPUTS = Collections.newSetFromMap(new ConcurrentHashMap<>(2)); private static final Output STD_OUTPUT; // Logger queue private static final BlockingDeque<String> MESSAGES = new LinkedBlockingDeque<>(); private LogHelper() { } @Override public void run() { try { writeLoop(); } catch (InterruptedException ignored) { // Do nothing } } private void writeLoop() throws InterruptedException { while (!Thread.interrupted()) { String message = MESSAGES.take(); for (Output output : OUTPUTS) { output.println(message); } } } @LauncherAPI public static void addOutput(Output output) { OUTPUTS.add(Objects.requireNonNull(output, "output")); } @LauncherAPI public static void addOutput(Path file) throws IOException { addOutput(new FileOutput(file)); } @LauncherAPI public static void debug(String message) { if (isDebugEnabled()) { log(Level.DEBUG, message, false); } } @LauncherAPI public static void debug(String format, Object... args) { debug(String.format(format, args)); } @LauncherAPI public static void error(Throwable exc) { error(isDebugEnabled() ? toString(exc) : exc.toString()); } @LauncherAPI public static void error(String message) { log(Level.ERROR, message, false); } @LauncherAPI public static void error(String format, Object... args) { error(String.format(format, args)); } @LauncherAPI public static void info(String message) { log(Level.INFO, message, false); } @LauncherAPI public static void info(String format, Object... args) { info(String.format(format, args)); } @LauncherAPI public static boolean isDebugEnabled() { return DEBUG_ENABLED.get(); } @LauncherAPI public static void log(Level level, String message, boolean sub) { String dateTime = DATE_TIME_FORMATTER.format(LocalDateTime.now()); println(JANSI ? ansiFormatLog(level, dateTime, message, sub) : formatLog(level, message, dateTime, sub)); } @LauncherAPI public static void printVersion(String product) { println(JANSI ? ansiFormatVersion(product) : formatVersion(product)); } @LauncherAPI public static void println(String message) { MESSAGES.add(message); } @LauncherAPI public static boolean removeOutput(Output output) { return OUTPUTS.remove(output); } @LauncherAPI public static boolean removeStdOutput() { return removeOutput(STD_OUTPUT); } @LauncherAPI public static void setDebugEnabled(boolean debugEnabled) { DEBUG_ENABLED.set(debugEnabled); } @LauncherAPI public static void subDebug(String message) { if (isDebugEnabled()) { log(Level.DEBUG, message, true); } } @LauncherAPI public static void subDebug(String format, Object... args) { subDebug(String.format(format, args)); } @LauncherAPI public static void subInfo(String message) { log(Level.INFO, message, true); } @LauncherAPI public static void subInfo(String format, Object... args) { subInfo(String.format(format, args)); } @LauncherAPI public static void subWarning(String message) { log(Level.WARNING, message, true); } @LauncherAPI public static void subWarning(String format, Object... args) { subWarning(String.format(format, args)); } @LauncherAPI public static String toString(Throwable exc) { try (StringWriter sw = new StringWriter(IOHelper.BUFFER_SIZE)) { try (PrintWriter pw = new PrintWriter(sw)) { exc.printStackTrace(pw); } return sw.toString(); } catch (IOException e) { throw new InternalError(e); } } @LauncherAPI public static void warning(String message) { log(Level.WARNING, message, false); } @LauncherAPI public static void warning(String format, Object... args) { warning(String.format(format, args)); } private static String ansiFormatLog(Level level, String dateTime, String message, boolean sub) { Ansi.Color levelColor; boolean bright = level != Level.DEBUG; switch (level) { case WARNING: levelColor = Ansi.Color.YELLOW; break; case ERROR: levelColor = Ansi.Color.RED; break; default: // INFO, DEBUG, Unknown levelColor = Ansi.Color.WHITE; break; } // Date-time Ansi ansi = new Ansi(IOHelper.BUFFER_SIZE); ansi.fg(Ansi.Color.WHITE).a(dateTime); // Level ansi.fgBright(Ansi.Color.WHITE).a(" [").bold(); if (bright) { ansi.fgBright(levelColor); } else { ansi.fg(levelColor); } ansi.a(level).boldOff().fgBright(Ansi.Color.WHITE).a("] "); // Message if (bright) { ansi.fgBright(levelColor); } else { ansi.fg(levelColor); } if (sub) { ansi.a(' ').a(Ansi.Attribute.ITALIC); } ansi.a(message); // Finish with reset code return ansi.reset().toString(); } private static String ansiFormatVersion(String product) { return new Ansi(IOHelper.BUFFER_SIZE).bold(). // Setup fgBright(Ansi.Color.MAGENTA).a("sashok724's "). // sashok724's fgBright(Ansi.Color.CYAN).a(product). // Product fgBright(Ansi.Color.WHITE).a(" v").fgBright(Ansi.Color.BLUE).a(Launcher.VERSION). // Version fgBright(Ansi.Color.WHITE).a(" (build #").fgBright(Ansi.Color.RED).a(Launcher.BUILD).fgBright(Ansi.Color.WHITE).a(')'). // Build# reset().toString(); // To string } private static String formatLog(Level level, String message, String dateTime, boolean sub) { if (sub) { message = ' ' + message; } return dateTime + " [" + level.name + "] " + message; } private static String formatVersion(String product) { return String.format("sashok724's %s v%s (build #%s)", product, Launcher.VERSION, Launcher.BUILD); } static { // Use JAnsi if available boolean jansi; try { Class.forName("org.fusesource.jansi.Ansi"); AnsiConsole.systemInstall(); jansi = true; } catch (ClassNotFoundException ignored) { jansi = false; } JANSI = jansi; // Add std writer STD_OUTPUT = System.out::println; addOutput(STD_OUTPUT); // Add file log writer String logFile = System.getProperty("launcher.logFile"); if (logFile != null) { try { addOutput(IOHelper.toPath(logFile)); } catch (IOException e) { error(e); } } // Start logging thread CommonHelper.newThread("LogHelper Thread", true, new LogHelper()).start(); } private static final class FileOutput implements Output, AutoCloseable { private final Writer writer; private FileOutput(Path file) throws IOException { writer = JANSI ? IOHelper.newWriter(new AnsiOutputStream(IOHelper.newOutput(file, true))) : IOHelper.newWriter(file, true); } @Override public void close() throws IOException { writer.close(); } @Override public void println(String message) { try { writer.write(message + System.lineSeparator()); writer.flush(); } catch (IOException ignored) { // Do nothing? } } } @LauncherAPI @FunctionalInterface public interface Output { void println(String message); } @LauncherAPI public enum Level { DEBUG("DEBUG"), INFO("INFO"), WARNING("WARN"), ERROR("ERROR"); public final String name; Level(String name) { this.name = name; } @Override public String toString() { return name; } } }