From 3bf4184fd2a8242a0dbfe14ac8654414dbedf7b2 Mon Sep 17 00:00:00 2001 From: Marc Baloup Date: Fri, 20 Aug 2021 01:48:49 +0200 Subject: [PATCH] Add CLI modules for standalone apps used in Pandacube (web api, launcher) --- CLI/.classpath | 27 +++ CLI/.gitignore | 1 + CLI/.project | 23 +++ .../org.eclipse.core.resources.prefs | 3 + CLI/.settings/org.eclipse.jdt.core.prefs | 8 + CLI/.settings/org.eclipse.m2e.core.prefs | 4 + CLI/pom.xml | 51 ++++++ .../pandacube/lib/cli/BrigadierCommand.java | 144 ++++++++++++++++ .../lib/cli/BrigadierDispatcher.java | 156 ++++++++++++++++++ .../pandacube/lib/cli/ConsoleInterface.java | 143 ++++++++++++++++ .../java/fr/pandacube/lib/cli/CoreLogger.java | 31 ++++ pom.xml | 3 +- 12 files changed, 593 insertions(+), 1 deletion(-) create mode 100644 CLI/.classpath create mode 100644 CLI/.gitignore create mode 100644 CLI/.project create mode 100644 CLI/.settings/org.eclipse.core.resources.prefs create mode 100644 CLI/.settings/org.eclipse.jdt.core.prefs create mode 100644 CLI/.settings/org.eclipse.m2e.core.prefs create mode 100644 CLI/pom.xml create mode 100644 CLI/src/main/java/fr/pandacube/lib/cli/BrigadierCommand.java create mode 100644 CLI/src/main/java/fr/pandacube/lib/cli/BrigadierDispatcher.java create mode 100644 CLI/src/main/java/fr/pandacube/lib/cli/ConsoleInterface.java create mode 100644 CLI/src/main/java/fr/pandacube/lib/cli/CoreLogger.java diff --git a/CLI/.classpath b/CLI/.classpath new file mode 100644 index 0000000..d4e0e69 --- /dev/null +++ b/CLI/.classpath @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CLI/.gitignore b/CLI/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/CLI/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/CLI/.project b/CLI/.project new file mode 100644 index 0000000..f8a4694 --- /dev/null +++ b/CLI/.project @@ -0,0 +1,23 @@ + + + pandalib-cli + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/CLI/.settings/org.eclipse.core.resources.prefs b/CLI/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..e9441bb --- /dev/null +++ b/CLI/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding/=UTF-8 diff --git a/CLI/.settings/org.eclipse.jdt.core.prefs b/CLI/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..1b835cb --- /dev/null +++ b/CLI/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=16 +org.eclipse.jdt.core.compiler.compliance=16 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=16 diff --git a/CLI/.settings/org.eclipse.m2e.core.prefs b/CLI/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 0000000..f897a7f --- /dev/null +++ b/CLI/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/CLI/pom.xml b/CLI/pom.xml new file mode 100644 index 0000000..8504951 --- /dev/null +++ b/CLI/pom.xml @@ -0,0 +1,51 @@ + + 4.0.0 + + + fr.pandacube.lib + pandalib-parent + 1.0-SNAPSHOT + + + pandalib-cli + + PandaLib-CLI + + + + minecraft-libraries + Minecraft Libraries + https://libraries.minecraft.net + + + + + + fr.pandacube.lib + pandalib-core + ${project.version} + compile + + + + fr.pandacube.bungeecord + bungeecord-log + ${bungeecord.version} + compile + + + fr.pandacube.bungeecord + bungeecord-config + ${bungeecord.version} + compile + + + + com.mojang + brigadier + 1.0.17 + + + + + diff --git a/CLI/src/main/java/fr/pandacube/lib/cli/BrigadierCommand.java b/CLI/src/main/java/fr/pandacube/lib/cli/BrigadierCommand.java new file mode 100644 index 0000000..72a24be --- /dev/null +++ b/CLI/src/main/java/fr/pandacube/lib/cli/BrigadierCommand.java @@ -0,0 +1,144 @@ +package fr.pandacube.lib.cli; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.context.ParsedCommandNode; +import com.mojang.brigadier.suggestion.Suggestion; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.LiteralCommandNode; + +import fr.pandacube.lib.core.chat.ChatStatic; +import fr.pandacube.lib.core.commands.SuggestionsSupplier; +import fr.pandacube.lib.core.util.Log; +import fr.pandacube.lib.core.util.ReflexionUtil; + +public abstract class BrigadierCommand extends ChatStatic { + + private LiteralCommandNode commandNode; + + public BrigadierCommand() { + LiteralArgumentBuilder builder = buildCommand(); + String[] aliases = getAliases(); + if (aliases == null) + aliases = new String[0]; + + commandNode = BrigadierDispatcher.instance.register(builder); + + + for (String alias : aliases) { + BrigadierDispatcher.instance.register(literal(alias) + .requires(commandNode.getRequirement()) + .executes(commandNode.getCommand()) + .redirect(commandNode) + ); + + } + + } + + protected abstract LiteralArgumentBuilder buildCommand(); + + protected String[] getAliases() { + return new String[0]; + } + + + + + public static LiteralArgumentBuilder literal(String name) { + return LiteralArgumentBuilder.literal(name); + } + + public static RequiredArgumentBuilder argument(String name, ArgumentType type) { + return RequiredArgumentBuilder.argument(name, type); + } + + + public static boolean isLiteralParsed(CommandContext context, String literal) { + for (ParsedCommandNode node : context.getNodes()) { + if (!(node.getNode() instanceof LiteralCommandNode)) + continue; + if (((LiteralCommandNode)node.getNode()).getLiteral().equals(literal)) + return true; + } + return false; + } + + public static T tryGetArgument(CommandContext context, String argument, Class type) { + return tryGetArgument(context, argument, type, null); + } + + public static T tryGetArgument(CommandContext context, String argument, Class type, T deflt) { + try { + return context.getArgument(argument, type); + } catch (IllegalArgumentException e) { + return deflt; + } + } + + + + + protected static SuggestionProvider wrapSuggestions(SuggestionsSupplier suggestions) { + return (context, builder) -> { + Object sender = context.getSource(); + String message = builder.getInput(); + try { + int tokenStartPos = builder.getStart(); + + List results = Collections.emptyList(); + + int firstSpacePos = message.indexOf(" "); + String[] args = (firstSpacePos + 1 > tokenStartPos - 1) ? new String[0] + : message.substring(firstSpacePos + 1, tokenStartPos - 1).split(" ", -1); + args = Arrays.copyOf(args, args.length + 1); + args[args.length - 1] = message.substring(tokenStartPos); + + results = suggestions.getSuggestions(sender, args.length - 1, args[args.length - 1], args); + + for (String s : results) { + if (s != null) + builder.suggest(s); + } + } catch (Throwable e) { + Log.severe("Error while tab-completing '" + message/* + "' for " + sender.getName()*/, e); + } + return completableFutureSuggestionsKeepsOriginalOrdering(builder); + }; + } + + + + + public static CompletableFuture completableFutureSuggestionsKeepsOriginalOrdering(SuggestionsBuilder builder) { + return CompletableFuture.completedFuture( + BrigadierDispatcher.createSuggestionsOriginalOrdering(builder.getInput(), getSuggestionsFromSuggestionsBuilder(builder)) + ); + } + + @SuppressWarnings("unchecked") + private static List getSuggestionsFromSuggestionsBuilder(SuggestionsBuilder builder) { + try { + return (List) ReflexionUtil.getDeclaredFieldValue(SuggestionsBuilder.class, builder, "result"); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + + + + + + + +} \ No newline at end of file diff --git a/CLI/src/main/java/fr/pandacube/lib/cli/BrigadierDispatcher.java b/CLI/src/main/java/fr/pandacube/lib/cli/BrigadierDispatcher.java new file mode 100644 index 0000000..c5a7ef6 --- /dev/null +++ b/CLI/src/main/java/fr/pandacube/lib/cli/BrigadierDispatcher.java @@ -0,0 +1,156 @@ +package fr.pandacube.lib.cli; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.ParseResults; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContextBuilder; +import com.mojang.brigadier.context.StringRange; +import com.mojang.brigadier.context.SuggestionContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestion; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; + +import fr.pandacube.lib.core.util.Log; +import jline.console.completer.Completer; + +public class BrigadierDispatcher implements Completer { + + public static final BrigadierDispatcher instance = new BrigadierDispatcher(); + + + + + + + + private CommandDispatcher dispatcher; + + private Object sender = new Object(); + + public BrigadierDispatcher() { + dispatcher = new CommandDispatcher<>(); + } + + + + /* package */ LiteralCommandNode register(LiteralArgumentBuilder node) { + return dispatcher.register(node); + } + + + public int execute(String commandWithoutSlash) { + ParseResults parsed = dispatcher.parse(commandWithoutSlash, sender); + + try { + return dispatcher.execute(parsed); + } catch (CommandSyntaxException e) { + Log.severe("Erreur d’utilisation de la commande : " + e.getMessage()); + return 0; + } catch (Throwable e) { + Log.severe("Erreur lors de l’exécution de la commande : ", e); + return 0; + } + + } + + + @Override + public int complete(String buffer, int cursor, List candidates) { + + String bufferBeforeCursor = buffer.substring(0, cursor); + + Suggestions completeResult = getSuggestions(bufferBeforeCursor); + + completeResult.getList().stream().map(s -> s.getText()).forEach(candidates::add); + + return completeResult.getRange().getStart(); + } + + /* package */ Suggestions getSuggestions(String buffer) { + ParseResults parsed = dispatcher.parse(buffer, sender); + try { + CompletableFuture futureSuggestions = buildSuggestionBrigadier(parsed); + return futureSuggestions.join(); + } catch (Throwable e) { + Log.severe("Erreur d’exécution des suggestions :\n" + e.getMessage(), e); + return Suggestions.empty().join(); + } + } + + + + CompletableFuture buildSuggestionBrigadier(ParseResults parsed) { + int cursor = parsed.getReader().getTotalLength(); + final CommandContextBuilder context = parsed.getContext(); + + final SuggestionContext nodeBeforeCursor = context.findSuggestionContext(cursor); + final CommandNode parent = nodeBeforeCursor.parent; + final int start = Math.min(nodeBeforeCursor.startPos, cursor); + + final String fullInput = parsed.getReader().getString(); + final String truncatedInput = fullInput.substring(0, cursor); + @SuppressWarnings("unchecked") final CompletableFuture[] futures = new CompletableFuture[parent.getChildren().size()]; + int i = 0; + for (final CommandNode node : parent.getChildren()) { + CompletableFuture future = Suggestions.empty(); + try { + future = node.listSuggestions(context.build(truncatedInput), new SuggestionsBuilder(truncatedInput, start)); + } catch (final CommandSyntaxException ignored) { + } + futures[i++] = future; + } + + final CompletableFuture result = new CompletableFuture<>(); + CompletableFuture.allOf(futures).thenRun(() -> { + final List suggestions = new ArrayList<>(); + for (final CompletableFuture future : futures) { + suggestions.add(future.join()); + } + result.complete(mergeSuggestionsOriginalOrdering(fullInput, suggestions)); + }); + return result; + } + + // inspired from com.mojang.brigadier.suggestion.Suggestions#merge, but without the sorting part + public static Suggestions mergeSuggestionsOriginalOrdering(String input, Collection suggestions) { + if (suggestions.isEmpty()) { + return new Suggestions(StringRange.at(0), new ArrayList<>(0)); + } else if (suggestions.size() == 1) { + return suggestions.iterator().next(); + } + + final List texts = new ArrayList<>(); + for (final Suggestions suggestions1 : suggestions) { + texts.addAll(suggestions1.getList()); + } + return createSuggestionsOriginalOrdering(input, texts); + } + + // inspired from com.mojang.brigadier.suggestion.Suggestions#create, but without the sorting part + public static Suggestions createSuggestionsOriginalOrdering(String command, Collection suggestions) { + if (suggestions.isEmpty()) { + return new Suggestions(StringRange.at(0), new ArrayList<>(0)); + } + int start = Integer.MAX_VALUE; + int end = Integer.MIN_VALUE; + for (final Suggestion suggestion : suggestions) { + start = Math.min(suggestion.getRange().getStart(), start); + end = Math.max(suggestion.getRange().getEnd(), end); + } + final StringRange range = new StringRange(start, end); + final List texts = new ArrayList<>(suggestions.size()); + for (final Suggestion suggestion : suggestions) { + texts.add(suggestion.expand(command, range)); + } + return new Suggestions(range, texts); + } + +} diff --git a/CLI/src/main/java/fr/pandacube/lib/cli/ConsoleInterface.java b/CLI/src/main/java/fr/pandacube/lib/cli/ConsoleInterface.java new file mode 100644 index 0000000..f6fac5a --- /dev/null +++ b/CLI/src/main/java/fr/pandacube/lib/cli/ConsoleInterface.java @@ -0,0 +1,143 @@ +package fr.pandacube.lib.cli; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.logging.ErrorManager; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +import fr.pandacube.lib.core.util.Log; +import jline.console.ConsoleReader; +import net.md_5.bungee.log.ConciseFormatter; + +public class ConsoleInterface extends Handler { + + + public static final String ANSI_RESET = "\u001B[0m"; + + public static final String ANSI_BLACK = "\u001B[30m"; + public static final String ANSI_DARK_RED = "\u001B[31m"; + public static final String ANSI_DARK_GREEN = "\u001B[32m"; + public static final String ANSI_GOLD = "\u001B[33m"; + public static final String ANSI_DARK_BLUE = "\u001B[34m"; + public static final String ANSI_DARK_PURPLE = "\u001B[35m"; + public static final String ANSI_DARK_AQUA = "\u001B[36m"; + public static final String ANSI_GRAY = "\u001B[37m"; + + public static final String ANSI_DARK_GRAY = "\u001B[30;1m"; + public static final String ANSI_RED = "\u001B[31;1m"; + public static final String ANSI_GREEN = "\u001B[32;1m"; + public static final String ANSI_YELLOW = "\u001B[33;1m"; + public static final String ANSI_BLUE = "\u001B[34;1m"; + public static final String ANSI_LIGHT_PURPLE = "\u001B[35;1m"; + public static final String ANSI_AQUA = "\u001B[36;1m"; + public static final String ANSI_WHITE = "\u001B[37;1m"; + + public static final String ANSI_BOLD = "\u001B[1m"; + + public static final String ANSI_CLEAR_SCREEN = "\u001B[2J\u001B[1;1H"; + + + + + private ConsoleReader reader; + private PrintWriter out; + + + + public ConsoleInterface() throws IOException { + reader = new ConsoleReader(); + reader.setBellEnabled(false); + reader.setPrompt("\r"+ANSI_LIGHT_PURPLE+">"); + out = new PrintWriter(reader.getOutput()); + reader.addCompleter(BrigadierDispatcher.instance); + + // configuration du formatteur pour le logger + System.setProperty("net.md_5.bungee.log-date-format", "yyyy-MM-dd HH:mm:ss"); + setFormatter(new ConciseFormatter(true)); + } + + + + + public ConsoleReader getConsoleReader() { + return reader; + } + + + + + public void loop() { + + int i = 0; + String line; + try { + while((line = reader.readLine()) != null) { + if (line.trim().equals("")) + continue; + String cmdLine = line; + new Thread(() -> { + BrigadierDispatcher.instance.execute(cmdLine); + }, "CLICmdThread #"+(i++)).start(); + + } + } catch (IOException e) { + Log.severe(e); + } + + + + + + + + } + + + + + private synchronized void println(String str) { + try { + out.println('\r'+ANSI_RESET+str); + out.flush(); + reader.drawLine(); + reader.flush(); + } catch (IOException e) { + Log.severe(e); + } + } + + + + + + @Override + public void close() throws SecurityException { } + + @Override + public void flush() { } + + @Override + public void publish(LogRecord record) { + if (!isLoggable(record)) + return; + + String formattedMessage; + + try { + formattedMessage = getFormatter().format(record); + } catch (Exception ex) { + reportError(null, ex, ErrorManager.FORMAT_FAILURE); + return; + } + + try { + println(formattedMessage.trim()); + } catch (Exception ex) { + reportError(null, ex, ErrorManager.WRITE_FAILURE); + return; + } + } + + +} diff --git a/CLI/src/main/java/fr/pandacube/lib/cli/CoreLogger.java b/CLI/src/main/java/fr/pandacube/lib/cli/CoreLogger.java new file mode 100644 index 0000000..f8bbbaf --- /dev/null +++ b/CLI/src/main/java/fr/pandacube/lib/cli/CoreLogger.java @@ -0,0 +1,31 @@ +package fr.pandacube.lib.cli; + +import java.io.PrintStream; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import net.md_5.bungee.log.LoggingOutputStream; + +public class CoreLogger extends Logger { + + + public CoreLogger(ConsoleInterface cli) { + super("CoreLogger", null); + setLevel(Level.ALL); + setUseParentHandlers(false); + addHandler(cli); + System.setErr(new PrintStream(new LoggingOutputStream(this, Level.SEVERE), true)); + System.setOut(new PrintStream(new LoggingOutputStream(this, Level.INFO), true)); + } + + + @Override + public void log(LogRecord record) { + record.setLongThreadID(Thread.currentThread().getId()); + + super.log(record); + } + + +} diff --git a/pom.xml b/pom.xml index 25f0ebc..77d4c06 100644 --- a/pom.xml +++ b/pom.xml @@ -47,8 +47,9 @@ - Core Bungee + CLI + Core Paper