From 050784dfab46569dd9a2f6ee71271c12fa85883e Mon Sep 17 00:00:00 2001 From: Presiareen Date: Tue, 3 May 2022 20:44:52 +0800 Subject: [PATCH] improve server side command line input (#415) * improve server side command line input * prevent multiline logs from covering typed commands * reduce text in console * resolve conflicts caused by multilanguage Co-authored-by: Magix --- build.gradle | 32 ++++--- .../java/emu/grasscutter/Grasscutter.java | 86 ++++++++++++++----- .../utils/JlineLogbackAppender.java | 20 +++++ src/main/resources/logback.xml | 2 +- 4 files changed, 104 insertions(+), 36 deletions(-) create mode 100644 src/main/java/emu/grasscutter/utils/JlineLogbackAppender.java diff --git a/build.gradle b/build.gradle index 737dc73f5..6da62b032 100644 --- a/build.gradle +++ b/build.gradle @@ -60,27 +60,31 @@ repositories { dependencies { implementation fileTree(dir: 'lib', include: ['*.jar']) - implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.32' - implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.2.9' + implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.32' + implementation group: 'ch.qos.logback', name: 'logback-core', version: '1.2.9' implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.9' - - implementation group: 'io.netty', name: 'netty-all', version: '4.1.71.Final' - - implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.8' - implementation group: 'com.google.protobuf', name: 'protobuf-java', version: '3.18.2' - - implementation group: 'org.reflections', name: 'reflections', version: '0.10.2' - implementation group: 'dev.morphia.morphia', name: 'morphia-core', version: '2.2.6' + implementation group: 'org.jline', name: 'jline', version: '3.21.0' + implementation group: 'org.jline', name: 'jline-terminal-jna', version: '3.21.0' + implementation group: 'net.java.dev.jna', name: 'jna', version: '5.10.0' + + implementation group: 'io.netty', name: 'netty-all', version: '4.1.71.Final' + + implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.8' + implementation group: 'com.google.protobuf', name: 'protobuf-java', version: '3.18.2' + + implementation group: 'org.reflections', name: 'reflections', version: '0.10.2' + + implementation group: 'dev.morphia.morphia', name: 'morphia-core', version: '2.2.6' implementation group: 'org.greenrobot', name: 'eventbus-java', version: '3.3.1' implementation group: 'org.danilopianini', name: 'java-quadtree', version: '0.1.9' implementation group: 'org.quartz-scheduler', name: 'quartz', version: '2.3.2' implementation group: 'org.quartz-scheduler', name: 'quartz-jobs', version: '2.3.2' - + implementation group: 'org.luaj', name: 'luaj-jse', version: '3.0.1' - + protobuf files('proto/') } @@ -107,8 +111,8 @@ jar { duplicatesStrategy = DuplicatesStrategy.INCLUDE from('src/main/java') { - include '*.xml' - } + include '*.xml' + } destinationDir = file(".") } diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index d4529aabb..58e54dd4d 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -1,10 +1,9 @@ package emu.grasscutter; -import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.FileWriter; -import java.io.InputStreamReader; +import java.io.IOError; import java.net.InetSocketAddress; import java.util.Calendar; @@ -13,6 +12,12 @@ import emu.grasscutter.plugin.PluginManager; import emu.grasscutter.plugin.api.ServerHook; import emu.grasscutter.scripts.ScriptLoader; import emu.grasscutter.utils.Utils; +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; +import org.jline.terminal.TerminalBuilder; import org.reflections.Reflections; import org.slf4j.LoggerFactory; @@ -30,8 +35,9 @@ import emu.grasscutter.utils.Crypto; public final class Grasscutter { private static final Logger log = (Logger) LoggerFactory.getLogger(Grasscutter.class); private static Config config; + private static LineReader consoleLineReader = null; private static Language language; - + private static final Gson gson = new GsonBuilder().setPrettyPrinting().create(); private static final File configFile = new File("./config.json"); @@ -108,11 +114,12 @@ public final class Grasscutter { // Enable all plugins. pluginManager.enablePlugins(); - - // Open console. - startConsole(); + // Hook into shutdown event. Runtime.getRuntime().addShutdownHook(new Thread(Grasscutter::onShutdown)); + + // Open console. + startConsole(); } /** @@ -122,7 +129,7 @@ public final class Grasscutter { // Disable all plugins. pluginManager.disablePlugins(); } - + public static void loadConfig() { try (FileReader file = new FileReader(configFile)) { config = gson.fromJson(file, Config.class); @@ -167,23 +174,40 @@ public final class Grasscutter { } public static void startConsole() { - String input; + // Console should not start in dispatch only mode. + if (getConfig().RunMode == ServerRunMode.DISPATCH_ONLY) { + getLogger().info(language.Dispatch_mode_not_support_command); + return; + } + getLogger().info(language.Start_done); - try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) { - while ((input = br.readLine()) != null) { - try { - if (getConfig().RunMode == ServerRunMode.DISPATCH_ONLY) { - getLogger().error(language.Dispatch_mode_not_support_command); - return; - } - - CommandMap.getInstance().invoke(null, input); - } catch (Exception e) { - Grasscutter.getLogger().error(language.Command_error, e); + String input = null; + boolean isLastInterrupted = false; + while (true) { + try { + input = consoleLineReader.readLine("> "); + } catch (UserInterruptException e) { + if (!isLastInterrupted) { + isLastInterrupted = true; + Grasscutter.getLogger().info("Press Ctrl-C again to shutdown."); + continue; + } else { + Runtime.getRuntime().exit(0); } + } catch (EndOfFileException e) { + Grasscutter.getLogger().info("EOF detected."); + continue; + } catch (IOError e) { + Grasscutter.getLogger().error("An IO error occurred.", e); + continue; + } + + isLastInterrupted = false; + try { + CommandMap.getInstance().invoke(null, input); + } catch (Exception e) { + Grasscutter.getLogger().error(language.Command_error, e); } - } catch (Exception e) { - Grasscutter.getLogger().error(language.Error, e); } } @@ -199,6 +223,26 @@ public final class Grasscutter { return log; } + public static LineReader getConsole() { + if (consoleLineReader == null) { + Terminal terminal = null; + try { + terminal = TerminalBuilder.builder().jna(true).build(); + } catch (Exception e) { + try { + // Fallback to a dumb jline terminal. + terminal = TerminalBuilder.builder().dumb(true).build(); + } catch (Exception ignored) { + // When dumb is true, build() never throws. + } + } + consoleLineReader = LineReaderBuilder.builder() + .terminal(terminal) + .build(); + } + return consoleLineReader; + } + public static Gson getGsonFactory() { return gson; } diff --git a/src/main/java/emu/grasscutter/utils/JlineLogbackAppender.java b/src/main/java/emu/grasscutter/utils/JlineLogbackAppender.java new file mode 100644 index 000000000..4027d726e --- /dev/null +++ b/src/main/java/emu/grasscutter/utils/JlineLogbackAppender.java @@ -0,0 +1,20 @@ +package emu.grasscutter.utils; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.ConsoleAppender; +import emu.grasscutter.Grasscutter; +import org.jline.reader.LineReader; + +import java.util.Arrays; + +public class JlineLogbackAppender extends ConsoleAppender { + @Override + protected void append(ILoggingEvent eventObject) { + if (!started) { + return; + } + Arrays.stream( + new String(encoder.encode(eventObject)).split("\n") + ).forEach(Grasscutter.getConsole()::printAbove); + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 477c1ac5b..91d3f133c 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -1,5 +1,5 @@ - + [%d{HH:mm:ss}] [%highlight(%level)] %msg%n