diff --git a/addonInter/src/main/java/edu/regis/universeplayer/addon/Main.java b/addonInter/src/main/java/edu/regis/universeplayer/addon/Main.java index 2e4c8b9..5bac7df 100644 --- a/addonInter/src/main/java/edu/regis/universeplayer/addon/Main.java +++ b/addonInter/src/main/java/edu/regis/universeplayer/addon/Main.java @@ -6,6 +6,7 @@ package edu.regis.universeplayer.addon; import edu.regis.universeplayer.browserCommands.BrowserConstants; import edu.regis.universeplayer.browserCommands.MessageHandler; +import org.apache.logging.log4j.core.LogEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,7 +19,7 @@ import java.util.concurrent.Future; public class Main { private static final Logger logger = LoggerFactory.getLogger(Main.class); - + public static void main(String[] args) throws IOException { MessageHandler interfaceLink; @@ -32,14 +33,22 @@ public class Main try { logger.debug("Setting up connection"); - socket = new Socket(BrowserConstants.IP,BrowserConstants.PORT); + socket = new Socket(BrowserConstants.IP, BrowserConstants.PORT); logger.debug("Connection established"); - + Socket finalSocket = socket; - interfaceLink = new MessageHandler("InterfaceHandler", finalSocket.getInputStream(), finalSocket.getOutputStream()) { + interfaceLink = new MessageHandler("InterfaceHandler", finalSocket.getInputStream(), finalSocket.getOutputStream()) + { + private long lastPing = 0; + @Override protected boolean onRun() { + /* + * How many milliseconds must pass between browser + * pings. + */ + final long PING_RATE = 5000; if (!finalSocket.isConnected() || finalSocket.isClosed() || finalSocket.isInputShutdown() || finalSocket.isOutputShutdown()) { logger.debug("Interface socket closed, shutting down"); @@ -48,10 +57,22 @@ public class Main /* * Make sure that it is active. */ -// this.sendUpdate("ping"); + if (SocketAppender.hasLogs()) + { + lastPing = System.currentTimeMillis(); + for (LogEvent event : SocketAppender.retrieveLogEvents()) + { + this.sendUpdate(event); + } + } + else if (System.currentTimeMillis() - lastPing >= PING_RATE) + { + lastPing = System.currentTimeMillis(); + this.sendUpdate("ping"); + } return false; } - + @Override protected void onClose() { @@ -72,20 +93,22 @@ public class Main * Pretty much just forwards any messages to the browser and * returns their value. */ - browserLink.addUpdateListener((update, link) -> { + browserLink.addUpdateListener((update, link) -> + { logger.debug("Sending update {}", update); interfaceLink.sendUpdate(update); }); - interfaceLink.addListener((providedValue, previousReturn) -> { + interfaceLink.addListener((providedValue, previousReturn) -> + { logger.debug("Forwarding message to browser: {}", providedValue); Object returnValue = browserLink.sendObject(providedValue).get(); logger.debug("Forwarding return to interface: {}", returnValue); return returnValue; }); - + browserThread = new Thread(browserLink); interfaceThread = new Thread(interfaceLink); - + logger.debug("Starting threads."); browserThread.start(); interfaceThread.start(); @@ -136,7 +159,7 @@ public class Main } } } - + private static void testMain() throws ExecutionException, InterruptedException { LinkedList> requests = new LinkedList<>(); diff --git a/addonInter/src/main/java/edu/regis/universeplayer/addon/SocketAppender.java b/addonInter/src/main/java/edu/regis/universeplayer/addon/SocketAppender.java new file mode 100644 index 0000000..2fd4442 --- /dev/null +++ b/addonInter/src/main/java/edu/regis/universeplayer/addon/SocketAppender.java @@ -0,0 +1,93 @@ +package edu.regis.universeplayer.addon; + +import org.apache.logging.log4j.core.*; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; + +import java.io.Serializable; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * This appender will store all messages sent to it, for the interface link to + * later collect and forward to the interface. + * + * @author William Hubbard + */ +@Plugin( + name = "Socket", + category = Core.CATEGORY_NAME, + elementType = Appender.ELEMENT_TYPE) +public class SocketAppender extends AbstractAppender +{ + private static final LinkedList events = new LinkedList<>(); + + /** + * Builds FileAppender instances. + * + * @param The type to build + */ + public static class Builder> extends AbstractAppender.Builder + implements org.apache.logging.log4j.core.util.Builder + { + @Override + public SocketAppender build() + { + return new SocketAppender(getName(), getFilter(), getOrCreateLayout(), isIgnoreExceptions(), getPropertyArray()); + } + } + + @PluginBuilderFactory + public static > B newBuilder() + { + return new Builder().asBuilder(); + } + + public SocketAppender(final String name, final Filter filter, final Layout layout, + final boolean ignoreExceptions, final Property[] properties) + { + super(name, filter, layout, ignoreExceptions, properties); + } + + /** + * Logs a LogEvent using whatever logic this Appender wishes to use. It is + * typically recommended to use a bridge pattern not only for the benefits + * from decoupling an Appender from its implementation, but it is also handy + * for sharing resources which may require some form of locking. + * + * @param event The LogEvent. + */ + @Override + public void append(LogEvent event) + { + synchronized (events) + { + events.add(event); + } + } + + /** + * Checks to see if any logs are available. + * @return + */ + public static boolean hasLogs() + { + synchronized (events) + { + return !events.isEmpty(); + } + } + + public static List retrieveLogEvents() + { + synchronized (events) + { + List logs = events.stream().toList(); + events.clear(); + return logs; + } + } +} diff --git a/browser/src/test/resources/log4j2.xml b/browser/src/test/resources/log4j2.xml new file mode 100644 index 0000000..f6802ec --- /dev/null +++ b/browser/src/test/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/AutoGson.java b/browserCommands/src/main/java/edu/regis/universeplayer/AutoGson.java new file mode 100644 index 0000000..62c32cf --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/AutoGson.java @@ -0,0 +1,5 @@ +package edu.regis.universeplayer; + +public class AutoGson +{ +} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageRunner.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageRunner.java index f2f6bb3..5fba973 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageRunner.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageRunner.java @@ -79,7 +79,8 @@ public abstract class MessageRunner implements Runnable, MessageSerializer BufferedInputStream browserIn = null; BufferedOutputStream browserOut = null; MessagePacket packet; - + + boolean running = true; int messageNum = -1; byte[][] returnMessage; ByteBuffer numBuffer = ByteBuffer.allocate(4); @@ -89,7 +90,7 @@ public abstract class MessageRunner implements Runnable, MessageSerializer browserIn = new BufferedInputStream(this.input); browserOut = new BufferedOutputStream(this.output); - while (!this.onRun()) + while (running && !this.onRun()) { /* * Sends a messages @@ -104,17 +105,19 @@ public abstract class MessageRunner implements Runnable, MessageSerializer { messageNum = this.messagesSent; packet.returnValue.index = messageNum; - writeMessage(browserOut, messageNum, packet.message); synchronized (this.sentQueue) { this.sentQueue.put(messageNum, packet); this.messagesSent++; } + + writeMessage(browserOut, messageNum, packet.message); } catch (IOException e) { - logger.error("Could not send message " + messageNum + " " + new String(packet.message, StandardCharsets.UTF_8), e); + logger.error("Could not send message {} {}", messageNum, new String(packet.message, StandardCharsets.UTF_8), e); + running = false; } } @@ -183,6 +186,7 @@ public abstract class MessageRunner implements Runnable, MessageSerializer catch (IOException e) { logger.error("Could not retrieve message", e); + running = false; } } } @@ -195,14 +199,11 @@ public abstract class MessageRunner implements Runnable, MessageSerializer /* * Release locks for any messages still waiting. */ - synchronized (this.sendQueue) + synchronized (this.sentQueue) { - logger.debug("Clearing up {} messages", this.sendQueue.size()); - while (this.sendQueue.size() > 0) - { - packet = this.sendQueue.poll(); - packet.returnMessage = null; - } + logger.debug("Clearing up {} messages", this.sentQueue.size()); + this.sentQueue.values().forEach(foundPacket -> foundPacket.returnMessage = new byte[0]); + this.sentQueue.clear(); } synchronized (this.readLock) { diff --git a/browserCommands/src/test/java/edu/regis/universeplayer/browserCommands/MessageRunnerTest.java b/browserCommands/src/test/java/edu/regis/universeplayer/browserCommands/MessageRunnerTest.java new file mode 100644 index 0000000..4f4c560 --- /dev/null +++ b/browserCommands/src/test/java/edu/regis/universeplayer/browserCommands/MessageRunnerTest.java @@ -0,0 +1,234 @@ +package edu.regis.universeplayer.browserCommands; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.*; +import java.net.SocketException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.Assert.*; + +public class MessageRunnerTest +{ + /** + * The input to the runner. This corresponds to the output from the + * handler. + * + * @see #handlerOut + */ + private static PipedInputStream runnerIn; + /** + * The output from the handler and to the runner. This corresponds to the + * input to the runner. + * + * @see #runnerIn + */ + private static PipedOutputStream handlerOut; + /** + * The input to the handler from the runner. This corresponds to the output + * from the runner. + * + * @see #runnerOut + */ + private static PipedInputStream handlerIn; + /** + * The output from the runner and to the handler. This corresponds to the + * input to the handler. + * + * @see #handlerIn + */ + private static PipedOutputStream runnerOut; + private static TestRunner runner; + private static TestHandler handler; + private static Thread runnerThread; + private static Thread handlerThread; + + private static class TestRunner extends MessageRunner + { + /** + * Creates a message runner. + * + * @param name - The name of the runner. This is used in logging. + * @param input - The input from our external source. + * @param output - The output to the external source. + */ + public TestRunner(String name, InputStream input, OutputStream output) + { + super(name, input, output); + } + } + + private static class TestHandler extends MessageHandler + { + /** + * Creates a message handler. + * + * @param name - The name of the handler. This is used in logging. + * @param input - The input from our external source. + * @param output - The output to the external source. + */ + public TestHandler(String name, InputStream input, OutputStream output) + { + super(name, input, output); + } + } + + @BeforeClass + public static void before() + { + try + { + runnerIn = new PipedInputStream(); + handlerOut = new PipedOutputStream(runnerIn); + handlerIn = new PipedInputStream(); + runnerOut = new PipedOutputStream(handlerIn); + + runner = new TestRunner("TestRunner", runnerIn, runnerOut); + handler = new TestHandler("TestHandler", handlerIn, handlerOut); + + runnerThread = new Thread(runner); + handlerThread = new Thread(handler); + runnerThread.start(); + handlerThread.start(); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + + @Test + public void testMessage() throws IOException, ExecutionException, InterruptedException + { + String mess = "Hello, world!"; + String returnMess = "Goodnight, moon!"; + MessageHandler.MessageListener listener = new MessageHandler.MessageListener() + { + @Override + public Object onMessage(Object providedValue, Object previousReturn) throws IOException, ExecutionException, InterruptedException + { + assertEquals(mess, providedValue); + return returnMess; + } + }; + handler.addListener(listener); + Future messFuture = runner.sendObject(mess); + assertEquals(returnMess, messFuture.get()); + handler.removeListener(listener); + } + + /** + * Tests messages that arrive in a different order than they were sent. + * + * @throws IOException + * @throws ExecutionException + * @throws InterruptedException + */ + @Test + public void testAsyncMessage() throws IOException, ExecutionException, InterruptedException + { + final String mess1 = "Hello, world!"; + final String mess2 = "Wake up, sun!"; + final String mess3 = "Good morning, house!"; + final String returnMess1 = "Goodnight, moon!"; + final String returnMess2 = "Good evening, star!"; + final String returnMess3 = "Goodbye, friend!"; + MessageHandler.MessageListener listener = new MessageHandler.MessageListener() + { + AtomicBoolean received2 = new AtomicBoolean(); + + @Override + public Object onMessage(Object providedValue, Object previousReturn) throws IOException, ExecutionException, InterruptedException + { + assertTrue(providedValue instanceof String); + switch ((String) providedValue) + { + case mess1: + synchronized (received2) + { + while (!received2.get()) + { + received2.wait(); + } + } + return returnMess1; + case mess3: + synchronized (received2) + { + received2.set(true); + received2.notifyAll(); + } + return returnMess3; + case mess2: + return returnMess2; + default: + Assert.fail("Expected \"" + mess1 + "\" or \"" + mess1 + "\", actual \"" + providedValue + "\""); + return null; + } + } + }; + handler.addListener(listener); + Future messFuture = runner.sendObject(mess1); + Thread.sleep(250); + assertFalse(messFuture.isDone()); + Future messFuture2 = runner.sendObject(mess2); + Thread.sleep(250); + assertFalse(messFuture.isDone()); + assertTrue(messFuture2.isDone()); + Future messFuture3 = runner.sendObject(mess3); + Thread.sleep(250); + assertTrue(messFuture.isDone()); + assertTrue(messFuture2.isDone()); + assertTrue(messFuture3.isDone()); + assertEquals(returnMess1, messFuture.get()); + assertEquals(returnMess2, messFuture2.get()); + assertEquals(returnMess3, messFuture3.get()); + handler.removeListener(listener); + } + + /** + * Tests the handler's update feature. + * + * @throws IOException + * @throws ExecutionException + * @throws InterruptedException + */ + @Test + public void testUpdate() throws IOException, ExecutionException, InterruptedException + { + final String mess1 = "Hello, world!"; + final AtomicBoolean received = new AtomicBoolean(); + UpdateListener listener = (object, runner) -> + { + assertEquals(mess1, object); + synchronized (received) + { + received.set(true); + received.notifyAll(); + } + }; + runner.addUpdateListener(listener); + handler.sendUpdate(mess1); + synchronized (received) + { + while (!received.get()) + { + received.wait(); + } + } + runner.removeUpdateListener(listener); + } + + @AfterClass + public static void closeRunner() throws IOException + { + runnerOut.close(); + runnerIn.close(); + handler.sendUpdate("ping"); + } +} \ No newline at end of file diff --git a/browserCommands/src/test/java/edu/regis/universeplayer/browserCommands/RunnerCloseTest.java b/browserCommands/src/test/java/edu/regis/universeplayer/browserCommands/RunnerCloseTest.java new file mode 100644 index 0000000..55a9fd5 --- /dev/null +++ b/browserCommands/src/test/java/edu/regis/universeplayer/browserCommands/RunnerCloseTest.java @@ -0,0 +1,177 @@ +package edu.regis.universeplayer.browserCommands; + +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.util.concurrent.ExecutionException; + +import static org.junit.Assert.assertNull; + +public class RunnerCloseTest +{ + private static Logger logger = LoggerFactory.getLogger(RunnerCloseTest.class); + /** + * The input to the runner. This corresponds to the output from the + * handler. + * + * @see #handlerOut + */ + private PipedInputStream runnerIn; + /** + * The output from the handler and to the runner. This corresponds to the + * input to the runner. + * + * @see #runnerIn + */ + private PipedOutputStream handlerOut; + /** + * The input to the handler from the runner. This corresponds to the output + * from the runner. + * + * @see #runnerOut + */ + private PipedInputStream handlerIn; + /** + * The output from the runner and to the handler. This corresponds to the + * input to the handler. + * + * @see #handlerIn + */ + private PipedOutputStream runnerOut; + private TestRunner runner; + private TestHandler handler; + private Thread runnerThread; + private Thread handlerThread; + + private class TestRunner extends MessageRunner + { + /** + * Creates a message runner. + * + * @param name - The name of the runner. This is used in logging. + * @param input - The input from our external source. + * @param output - The output to the external source. + */ + public TestRunner(String name, InputStream input, OutputStream output) + { + super(name, input, output); + } + } + + private class TestHandler extends MessageHandler + { + /** + * Creates a message handler. + * + * @param name - The name of the handler. This is used in logging. + * @param input - The input from our external source. + * @param output - The output to the external source. + */ + public TestHandler(String name, InputStream input, OutputStream output) + { + super(name, input, output); + } + } + + @Before + public void before() + { + try + { + runnerIn = new PipedInputStream(); + handlerOut = new PipedOutputStream(runnerIn); + handlerIn = new PipedInputStream(); + runnerOut = new PipedOutputStream(handlerIn); + + runner = new TestRunner("TestRunner", runnerIn, runnerOut); + handler = new TestHandler("TestHandler", handlerIn, handlerOut); + + handler.addListener((providedValue, previousReturn) -> "ping"); + + runnerThread = new Thread(runner); + handlerThread = new Thread(handler); + runnerThread.start(); + handlerThread.start(); + } + catch (IOException e) + { + e.printStackTrace(); + } + } + + /** + * Sees what happens when the runner is closed but sends a message anyway. + * + * @throws IOException + * @throws ExecutionException + * @throws InterruptedException + */ + @Test + public void testRunnerClose() throws IOException, ExecutionException, InterruptedException + { + logger.info("Testing runner close with message"); + runnerIn.close(); + runnerOut.close(); + assertNull(runner.sendObject("pong").get()); + logger.info("Runner test complete"); + } + + /** + * Sees what happens when the runner is closed and an update is sent from + * the handler. + * + * @throws IOException + * @throws ExecutionException + * @throws InterruptedException + */ + @Test + public void testRunnerCloseUpdate() throws IOException, ExecutionException, InterruptedException + { + logger.info("Testing runner close with update"); + runnerIn.close(); + runnerOut.close(); + handler.sendUpdate("ping"); + Thread.sleep(1000); + logger.info("Runner test complete"); + } + + /** + * Sees what happens when the handler is closed and the runner attempts to + * send a message. + * + * @throws IOException + * @throws ExecutionException + * @throws InterruptedException + */ + @Test + public void testHandlerClose() throws IOException, ExecutionException, InterruptedException + { + logger.info("Testing handler close with message"); + handlerIn.close(); + handlerOut.close(); + assertNull(runner.sendObject("pong").get()); + logger.info("Handler test complete"); + } + + /** + * Sees what happens when the handler is closed but attempts to send an + * update anyway. + * + * @throws IOException + * @throws ExecutionException + * @throws InterruptedException + */ + @Test + public void testHandlerCloseUpdate() throws IOException, ExecutionException, InterruptedException + { + logger.info("Testing handler close with update"); + handlerIn.close(); + handlerOut.close(); + handler.sendUpdate("ping"); + Thread.sleep(1000); + logger.info("Handler test complete"); + } +} \ No newline at end of file diff --git a/browserCommands/src/test/resources/log4j2.xml b/browserCommands/src/test/resources/log4j2.xml new file mode 100644 index 0000000..3d7f9f1 --- /dev/null +++ b/browserCommands/src/test/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + \ No newline at end of file