diff --git a/build.gradle b/build.gradle index 07bfa953..9242bb5b 100644 --- a/build.gradle +++ b/build.gradle @@ -81,10 +81,10 @@ dependencies { compile 'com.fasterxml.jackson.core:jackson-core:2.10.2' compile 'com.fasterxml.jackson.core:jackson-databind:2.10.2' compile 'com.fasterxml.jackson.core:jackson-annotations:2.10.2' - compile 'org.antlr:antlr4:4.8-1' - antlr 'org.antlr:antlr4:4.8-1' + compile 'org.antlr:antlr4:4.7.1' + antlr 'org.antlr:antlr4:4.7.1' - runtime 'org.antlr:antlr4-runtime:4.8-1' + runtime 'org.antlr:antlr4-runtime:4.7.1' runtime 'org.apache.logging.log4j:log4j-api:2.13.0' runtime 'org.apache.logging.log4j:log4j-core:2.13.0' runtime 'org.apache.logging.log4j:log4j-slf4j-impl:2.13.0' diff --git a/src/main/java/de/fearnixx/jeak/Main.java b/src/main/java/de/fearnixx/jeak/Main.java index 24e3a628..4bbc832e 100644 --- a/src/main/java/de/fearnixx/jeak/Main.java +++ b/src/main/java/de/fearnixx/jeak/Main.java @@ -1,6 +1,6 @@ package de.fearnixx.jeak; -import de.fearnixx.jeak.commandline.CommandLine; +import de.fearnixx.jeak.commandline.CLIService; import de.fearnixx.jeak.plugin.persistent.PluginManager; import de.fearnixx.jeak.reflect.JeakBotPlugin; import de.fearnixx.jeak.test.AbstractTestPlugin; @@ -15,11 +15,12 @@ import java.io.File; import java.lang.management.ManagementFactory; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.util.*; +import java.util.concurrent.*; +import java.util.function.Consumer; +import java.util.function.Supplier; public class Main implements Runnable { @@ -32,7 +33,7 @@ public class Main implements Runnable { private final Executor mainExecutor = Executors.newSingleThreadExecutor( new NamePatternThreadFactory("main-%d")); private final JeakBot jeakBot = new JeakBot(); - private final CommandLine cmd = new CommandLine(System.in, System.out); + private boolean cliTerminated = false; public static void main(String[] arguments) { for (String arg : arguments) { @@ -93,12 +94,52 @@ public static Main getInstance() { } public void run() { - discoverPlugins(); + populatePluginSources(); startBot(); - runLoop(); + + CLIService cliService = CLIService.getInstance(); + cliService.registerCommand("stop", ctx -> { + ctx.getMessageConsumer().accept("Bye bye!"); + synchronized (this) { + cliTerminated = true; + jeakBot.shutdown(); + } + }); + cliService.registerCommand("sysInfo", ctx -> { + dumpSysInfo(ctx.getMessageConsumer()); + }); + + ExecutorService execSvc = Executors.newSingleThreadExecutor(); + + + + try (Scanner sysInScanner = new Scanner(System.in)) { + Supplier> next = () -> execSvc.submit(sysInScanner::nextLine); + Future futConsoleLine = next.get(); + while (true) { + synchronized (this) { + if (cliTerminated) break; + } + + try { + String line = futConsoleLine.get(1, TimeUnit.SECONDS); + CLIService.getInstance().receiveLine(line); + futConsoleLine = next.get(); + + } catch (InterruptedException e) { + logger.warn("Interrupted while reading system in? Shutting down..."); + jeakBot.shutdown(); + } catch (ExecutionException e) { + logger.error("PANIC! Failed to read from system in!", e); + jeakBot.shutdown(); + } catch (TimeoutException e) { + // We're just going to ignore this ^^. Probably the admin doesn't want to talk with us anyways. + } + } + } } - private void discoverPlugins() { + private void populatePluginSources() { pluginManager.addSource(new File("plugins")); pluginManager.addSource(new File("libraries")); } @@ -114,9 +155,7 @@ private void startBot() { jeakBot.setConfig(botConfig); jeakBot.setPluginManager(pluginManager); - - jeakBot.onShutdown(this::onBotShutdown); - + jeakBot.onShutdown(bot -> internalShutdown()); mainExecutor.execute(jeakBot); } @@ -126,21 +165,13 @@ private IConfig createConfig(File confFile) { return new FileConfig(configLoader, confFile); } - private void runLoop() { - cmd.run(); - } - - private void onBotShutdown(JeakBot bot) { - internalShutdown(); - } - public void shutdown() { jeakBot.shutdown(); internalShutdown(); } private void internalShutdown() { - cmd.kill(); + cliTerminated = true; try { Thread.sleep(1200); } catch (InterruptedException e) { @@ -181,4 +212,29 @@ private void internalShutdown() { System.exit(0); } + + public void dumpSysInfo(Consumer acceptor) { + final Consumer dump = str -> { + acceptor.accept(str); + logger.info("[SysInfo] {}", str); + }; + + try { + var netIfIterator = NetworkInterface.getNetworkInterfaces().asIterator(); + while (netIfIterator.hasNext()) { + NetworkInterface netIf = netIfIterator.next(); + dump.accept(String.format("[IF] Name=%s MAC=%s", netIf.getDisplayName(), Arrays.toString(netIf.getHardwareAddress()))); + for (InterfaceAddress ifAddr : netIf.getInterfaceAddresses()) { + String addrStr = Arrays.toString(ifAddr.getAddress().getAddress()); + String prefix = Integer.toString(ifAddr.getNetworkPrefixLength()); + String hostname = ifAddr.getAddress().getHostName(); + String broadcast = Arrays.toString(ifAddr.getBroadcast().getAddress()); + dump.accept(String.format("IP: %s/%s [%s] BR: %s", addrStr, prefix, hostname, broadcast)); + } + } + + } catch (Exception e) { + logger.error("Could not retrieve system information!", e); + } + } } diff --git a/src/main/java/de/fearnixx/jeak/antlr/CommandParserUtil.java b/src/main/java/de/fearnixx/jeak/antlr/CommandParserUtil.java new file mode 100644 index 00000000..b950fd7a --- /dev/null +++ b/src/main/java/de/fearnixx/jeak/antlr/CommandParserUtil.java @@ -0,0 +1,60 @@ +package de.fearnixx.jeak.antlr; + +import de.fearnixx.jeak.service.command.CommandCtxVisitor; +import de.fearnixx.jeak.service.command.CommandInfo; +import de.fearnixx.jeak.service.command.SyntaxErrorListener; +import de.mlessmann.confort.lang.RuntimeParseException; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CodePointCharStream; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.atn.PredictionMode; +import org.slf4j.Logger; + +public abstract class CommandParserUtil { + + private CommandParserUtil() { + } + + public static CommandInfo parseCommandLine(String arguments, Logger logger) { + CodePointCharStream charStream = CharStreams.fromString(arguments); + var lexer = new CommandExecutionCtxLexer(charStream); + var tokenStream = new CommonTokenStream(lexer); + var parser = new CommandExecutionCtxParser(tokenStream); + + + // Use 2-stage parsing for expression performance + // https://github.com/antlr/antlr4/blob/master/doc/faq/general.md#why-is-my-expression-parser-slow + try { + // STAGE 1 + var treeVisitor = new CommandCtxVisitor(); + var errorListener = new SyntaxErrorListener(treeVisitor.getInfo().getErrorMessages()::add); + + logger.debug("Trying to run STAGE 1 parsing. (SSL prediction)"); + parser.getInterpreter().setPredictionMode(PredictionMode.SLL); + parser.removeErrorListeners(); + parser.addErrorListener(errorListener); + var grammarContext = parser.commandExecution(); + treeVisitor.visitCommandExecution(grammarContext); + return treeVisitor.getInfo(); + } catch (Exception ex) { + // STAGE 2 + var treeVisitor = new CommandCtxVisitor(); + var errorListener = new SyntaxErrorListener(treeVisitor.getInfo().getErrorMessages()::add); + + logger.debug("Trying to run STAGE 2 parsing. (LL prediction)", ex); + tokenStream.seek(0); + parser.reset(); + parser.getInterpreter().setPredictionMode(PredictionMode.LL); + parser.removeErrorListeners(); + parser.addErrorListener(errorListener); + + try { + var grammarContext = parser.commandExecution(); + treeVisitor.visitCommandExecution(grammarContext); + } catch (RuntimeParseException e) { + treeVisitor.getInfo().getErrorMessages().add(e.getMessage()); + } + return treeVisitor.getInfo(); + } + } +} diff --git a/src/main/java/de/fearnixx/jeak/commandline/CLICommandContext.java b/src/main/java/de/fearnixx/jeak/commandline/CLICommandContext.java new file mode 100644 index 00000000..5a14d4a2 --- /dev/null +++ b/src/main/java/de/fearnixx/jeak/commandline/CLICommandContext.java @@ -0,0 +1,24 @@ +package de.fearnixx.jeak.commandline; + +import de.fearnixx.jeak.service.command.CommandInfo; + +import java.util.function.Consumer; + +public class CLICommandContext { + + private final CommandInfo commInfo; + private final Consumer messageConsumer; + + public CLICommandContext(CommandInfo commInfo, Consumer messageConsumer) { + this.commInfo = commInfo; + this.messageConsumer = messageConsumer; + } + + public CommandInfo getCommandInfo() { + return commInfo; + } + + public Consumer getMessageConsumer() { + return messageConsumer; + } +} diff --git a/src/main/java/de/fearnixx/jeak/commandline/CLIService.java b/src/main/java/de/fearnixx/jeak/commandline/CLIService.java new file mode 100644 index 00000000..c2efe000 --- /dev/null +++ b/src/main/java/de/fearnixx/jeak/commandline/CLIService.java @@ -0,0 +1,81 @@ +package de.fearnixx.jeak.commandline; + +import de.fearnixx.jeak.antlr.CommandParserUtil; +import de.fearnixx.jeak.service.command.CommandInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.OutputStream; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +import static de.fearnixx.jeak.antlr.CommandParserUtil.parseCommandLine; + +public class CLIService { + + private static final Object INSTANCE_LOCK = new Object(); + private static final Logger logger = LoggerFactory.getLogger(CLIService.getInstance().getClass()); + public static CLIService instance; + + public synchronized static CLIService getInstance() { + synchronized (INSTANCE_LOCK) { + if (instance == null) { + instance = new CLIService(); + } + return instance; + } + } + + /** + * In case the framework is not started via its shipped main class, calling applications may override + * the cli-command service instance so they can properly receive the command registrations. + * @implNote This MUST be called before any interaction with the actual framework as the instance probably will be initialized by then. + */ + protected static void setInstance(CLIService service) { + synchronized (INSTANCE_LOCK) { + if (instance != null) { + throw new IllegalStateException("Replacement CLI-Services MUST be set before the first #getInstance call!"); + } + instance = service; + } + } + + protected final Map> cliCommands = new ConcurrentHashMap<>(); + + protected CLIService() { + } + + protected Consumer getMessageConsumer() { + return System.out::println; + } + + public Optional> registerCommand(String command, Consumer commandConsumer) { + return Optional.ofNullable(cliCommands.put(command, commandConsumer)); + } + + public void receiveLine(String input) { + int spacePos = input.indexOf(' '); + String command; + String contextPart = null; + if (spacePos < 0) { + command = input; + } else { + command = input.substring(0, spacePos); + contextPart = input.substring(spacePos).trim(); + } + + Consumer cliConsumer = cliCommands.getOrDefault(command, null); + if (cliConsumer != null) { + CommandInfo commandInfo = parseCommandLine(contextPart, logger); + CLICommandContext cliContext = new CLICommandContext(commandInfo, getMessageConsumer()); + cliConsumer.accept(cliContext); + + } else { + final String unknownMsg = String.format("Unknown command: \"%s\"", command); + logger.info(unknownMsg); + getMessageConsumer().accept(unknownMsg); + } + } +} diff --git a/src/main/java/de/fearnixx/jeak/commandline/CommandLine.java b/src/main/java/de/fearnixx/jeak/commandline/CommandLine.java deleted file mode 100644 index 060bb387..00000000 --- a/src/main/java/de/fearnixx/jeak/commandline/CommandLine.java +++ /dev/null @@ -1,120 +0,0 @@ -package de.fearnixx.jeak.commandline; - -import de.fearnixx.jeak.Main; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.nio.charset.Charset; -import java.util.regex.Pattern; - -/** - * Created by MarkL4YG on 18.06.17. - */ -public class CommandLine implements Runnable { - - private static final Logger logger = LoggerFactory.getLogger(CommandLine.class); - - private final Object lock = new Object(); - private boolean terminated = false; - - private final InputStream in; - private final OutputStreamWriter out; - - public CommandLine(InputStream in, OutputStream out) { - this.in = in; - this.out = new OutputStreamWriter(out); - } - - public void run() { - StringBuilder b = new StringBuilder(128); - byte[] buffer = new byte[1024]; - byte[] cc = new byte[1]; - int buffPos = 0; - boolean lf, cr; - - terminated = false; - outer: while (true) { - try { - do { - synchronized (lock) { - if (terminated) { - logger.info("Commandline closed"); - break outer; - } - } - Thread.sleep(100); - } while (in.available() <= 0 && !terminated); - if (in.read(cc) == -1) { - logger.error("Commandline reached EOS"); - synchronized (lock) { - kill(); - break outer; - } - } - - - lf = cc[0] == '\n'; - cr = cc[0] == '\r'; - if (cr) continue; // Ignore carriage-return - - buffer[buffPos++] = cc[0]; - if (lf || buffPos == buffer.length) { - b.append(new String(buffer, 0, buffPos-1, Charset.defaultCharset())); - buffPos = 0; - if (lf) { - processCommand(b.toString()); - b = new StringBuilder(128); - } - } - Thread.sleep(20); - } catch (InterruptedException e) { - // We want to be explicit about this. - //noinspection UnnecessaryContinue - continue; - } catch (IOException e) { - if (!"Stream closed".equals(e.getMessage())) { - logger.error("Commandline crashed", e); - } - kill(); - } - } - } - - private static final Pattern commp = Pattern.compile("[\\w\\d]+"); - private void processCommand(String command) throws IOException { - if (!commp.matcher(command).matches()) { - logger.warn("Command not matching: {}", command); - return; - } - switch (command) { - case "stop": - case "quit": - case "exit": - case "shutdown": - Main.getInstance().shutdown(); - break; - case "help": - logger.info("\nCommands: \n\thelp - Display this page" + - "\n\tstop - Shutdown all bots"); - break; - default: - logger.warn("Unknown command: {}\n", command); - break; - } - } - - public void kill() { - synchronized (lock) { - terminated = true; - try { - in.close(); - } catch (IOException e) { - // Ignore - } - } - } -} diff --git a/src/main/java/de/fearnixx/jeak/service/command/TypedCommandService.java b/src/main/java/de/fearnixx/jeak/service/command/TypedCommandService.java index 47f58c88..6c4e01bc 100644 --- a/src/main/java/de/fearnixx/jeak/service/command/TypedCommandService.java +++ b/src/main/java/de/fearnixx/jeak/service/command/TypedCommandService.java @@ -40,6 +40,8 @@ import java.util.function.Consumer; import java.util.stream.Collectors; +import static de.fearnixx.jeak.antlr.CommandParserUtil.parseCommandLine; + @FrameworkService(serviceInterface = ICommandService.class) public class TypedCommandService extends CommandService { @@ -155,7 +157,7 @@ private synchronized void triggerCommand(IQueryEvent.INotification.IClientTextMe private void dispatchTyped(IQueryEvent.INotification.IClientTextMessage txtEvent, String arguments, CommandRegistration registration) { ILocaleContext langCtx = locales.getContext(txtEvent.getSender().getCountryCode()); - CommandInfo info = parseCommandLine(arguments); + CommandInfo info = parseCommandLine(arguments, logger); if (!info.getErrorMessages().isEmpty()) { logger.info("Aborting command due to parsing errors."); info.getErrorMessages().add(0, langCtx.getMessage(MSG_HAS_ERRORS)); @@ -245,49 +247,6 @@ private void sendErrorMessages(IQueryEvent.INotification.IClientTextMessage txtE .forEach(msg -> txtEvent.getConnection().sendRequest(txtEvent.getSender().sendMessage(msg))); } - protected CommandInfo parseCommandLine(String arguments) { - CodePointCharStream charStream = CharStreams.fromString(arguments); - var lexer = new CommandExecutionCtxLexer(charStream); - var tokenStream = new CommonTokenStream(lexer); - var parser = new CommandExecutionCtxParser(tokenStream); - - - // Use 2-stage parsing for expression performance - // https://github.com/antlr/antlr4/blob/master/doc/faq/general.md#why-is-my-expression-parser-slow - try { - // STAGE 1 - var treeVisitor = new CommandCtxVisitor(); - var errorListener = new SyntaxErrorListener(treeVisitor.getInfo().getErrorMessages()::add); - - logger.debug("Trying to run STAGE 1 parsing. (SSL prediction)"); - parser.getInterpreter().setPredictionMode(PredictionMode.SLL); - parser.removeErrorListeners(); - parser.addErrorListener(errorListener); - var grammarContext = parser.commandExecution(); - treeVisitor.visitCommandExecution(grammarContext); - return treeVisitor.getInfo(); - } catch (Exception ex) { - // STAGE 2 - var treeVisitor = new CommandCtxVisitor(); - var errorListener = new SyntaxErrorListener(treeVisitor.getInfo().getErrorMessages()::add); - - logger.debug("Trying to run STAGE 2 parsing. (LL prediction)", ex); - tokenStream.seek(0); - parser.reset(); - parser.getInterpreter().setPredictionMode(PredictionMode.LL); - parser.removeErrorListeners(); - parser.addErrorListener(errorListener); - - try { - var grammarContext = parser.commandExecution(); - treeVisitor.visitCommandExecution(grammarContext); - } catch (RuntimeParseException e) { - treeVisitor.getInfo().getErrorMessages().add(e.getMessage()); - } - return treeVisitor.getInfo(); - } - } - @SuppressWarnings("squid:S3864") @Override public void registerCommand(ICommandSpec spec) { diff --git a/src/test/java/de/fearnixx/jeak/test/junit/antlr/TestCommandParser.java b/src/test/java/de/fearnixx/jeak/test/junit/antlr/TestCommandParser.java index c892d6ec..abbf812f 100644 --- a/src/test/java/de/fearnixx/jeak/test/junit/antlr/TestCommandParser.java +++ b/src/test/java/de/fearnixx/jeak/test/junit/antlr/TestCommandParser.java @@ -1,11 +1,11 @@ package de.fearnixx.jeak.test.junit.antlr; import de.fearnixx.jeak.service.command.CommandInfo; -import de.fearnixx.jeak.service.command.TypedCommandService; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static de.fearnixx.jeak.antlr.CommandParserUtil.parseCommandLine; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; @@ -13,12 +13,10 @@ public class TestCommandParser { private static final Logger logger = LoggerFactory.getLogger(TestCommandParser.class); - private StubCommandSvc svcStub = new StubCommandSvc(); - @Test public void testNormalParameters() { String params = "these are four params"; - CommandInfo info = svcStub.parseLine(params); + CommandInfo info = parseCommandLine(params, logger); assertThat(info.isParameterized(), is(true)); assertThat(info.isArgumentized(), is(false)); assertThat(info.getParameters(), contains("these", "are", "four", "params")); @@ -27,7 +25,7 @@ public void testNormalParameters() { @Test public void testQuotedParameters() { String params = "there are \"quoted params\""; - CommandInfo info = svcStub.parseLine(params); + CommandInfo info = parseCommandLine(params, logger); assertThat(info.isParameterized(), is(true)); assertThat(info.isArgumentized(), is(false)); assertThat(info.getParameters(), contains("there", "are", "quoted params")); @@ -36,7 +34,7 @@ public void testQuotedParameters() { @Test public void testNonQuotedDashedParameters() { String params = "there-is a non-quoted param"; - CommandInfo commandInfo = svcStub.parseLine(params); + CommandInfo commandInfo = parseCommandLine(params, logger); assertThat(commandInfo.isParameterized(), is(true)); assertThat(commandInfo.isArgumentized(), is(false)); assertThat(commandInfo.getParameters(), contains("there-is", "a", "non-quoted", "param")); @@ -45,7 +43,7 @@ public void testNonQuotedDashedParameters() { @Test public void testQuotedEscapedParameters() { String params = "there are \"escapes of \\\" in params\""; - CommandInfo info = svcStub.parseLine(params); + CommandInfo info = parseCommandLine(params, logger); assertThat(info.isParameterized(), is(true)); assertThat(info.isArgumentized(), is(false)); assertThat(info.getParameters(), contains("there", "are", "escapes of \" in params")); @@ -54,7 +52,7 @@ public void testQuotedEscapedParameters() { @Test public void testOptionArguments() { String params = "--option --option2"; - CommandInfo info = svcStub.parseLine(params); + CommandInfo info = parseCommandLine(params, logger); assertThat(info.isParameterized(), is(false)); assertThat(info.isArgumentized(), is(true)); assertThat(info.getArguments().getOrDefault("option", null), is("true")); @@ -64,7 +62,7 @@ public void testOptionArguments() { @Test public void testValueArguments() { String params = "--option=value --option2=value2"; - CommandInfo info = svcStub.parseLine(params); + CommandInfo info = parseCommandLine(params, logger); assertThat(info.isParameterized(), is(false)); assertThat(info.isArgumentized(), is(true)); assertThat(info.getArguments().getOrDefault("option", null), is("value")); @@ -74,7 +72,7 @@ public void testValueArguments() { @Test public void testQuotedValueArguments() { String params = "--option=\"quoted value\" --option2=value"; - CommandInfo info = svcStub.parseLine(params); + CommandInfo info = parseCommandLine(params, logger); assertThat(info.isParameterized(), is(false)); assertThat(info.isArgumentized(), is(true)); assertThat(info.getArguments().getOrDefault("option", null), is("quoted value")); @@ -84,14 +82,14 @@ public void testQuotedValueArguments() { @Test public void testMixedArgumentParams() { String params = "this --shouldnt --work=now"; - CommandInfo info = svcStub.parseLine(params); + CommandInfo info = parseCommandLine(params, logger); assertThat(info.getErrorMessages().size(), greaterThan(0)); } @Test public void testNonQuotedDashedArguments() { String params = "--this=there-is --a=true --b=\"non-quoted\" --param"; - CommandInfo commandInfo = svcStub.parseLine(params); + CommandInfo commandInfo = parseCommandLine(params, logger); assertThat(commandInfo.isParameterized(), is(false)); assertThat(commandInfo.isArgumentized(), is(true)); assertThat(commandInfo.getArguments().get("this"), is("there-is")); @@ -99,11 +97,4 @@ public void testNonQuotedDashedArguments() { assertThat(commandInfo.getArguments().get("b"), is("non-quoted")); assertThat(commandInfo.getArguments().get("param"), is("true")); } - - private static class StubCommandSvc extends TypedCommandService { - - CommandInfo parseLine(String line) { - return parseCommandLine(line); - } - } }