Stabilized the browser shutdown process.

This commit is contained in:
Markil3
2021-10-09 14:31:47 -06:00
parent 0ffa626cb7
commit 72ccfbc444
8 changed files with 589 additions and 22 deletions

View File

@@ -6,6 +6,7 @@ package edu.regis.universeplayer.addon;
import edu.regis.universeplayer.browserCommands.BrowserConstants; import edu.regis.universeplayer.browserCommands.BrowserConstants;
import edu.regis.universeplayer.browserCommands.MessageHandler; import edu.regis.universeplayer.browserCommands.MessageHandler;
import org.apache.logging.log4j.core.LogEvent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -18,7 +19,7 @@ import java.util.concurrent.Future;
public class Main public class Main
{ {
private static final Logger logger = LoggerFactory.getLogger(Main.class); private static final Logger logger = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) throws IOException public static void main(String[] args) throws IOException
{ {
MessageHandler interfaceLink; MessageHandler interfaceLink;
@@ -32,14 +33,22 @@ public class Main
try try
{ {
logger.debug("Setting up connection"); logger.debug("Setting up connection");
socket = new Socket(BrowserConstants.IP,BrowserConstants.PORT); socket = new Socket(BrowserConstants.IP, BrowserConstants.PORT);
logger.debug("Connection established"); logger.debug("Connection established");
Socket finalSocket = socket; Socket finalSocket = socket;
interfaceLink = new MessageHandler("InterfaceHandler", finalSocket.getInputStream(), finalSocket.getOutputStream()) { interfaceLink = new MessageHandler("InterfaceHandler", finalSocket.getInputStream(), finalSocket.getOutputStream())
{
private long lastPing = 0;
@Override @Override
protected boolean onRun() 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()) if (!finalSocket.isConnected() || finalSocket.isClosed() || finalSocket.isInputShutdown() || finalSocket.isOutputShutdown())
{ {
logger.debug("Interface socket closed, shutting down"); logger.debug("Interface socket closed, shutting down");
@@ -48,10 +57,22 @@ public class Main
/* /*
* Make sure that it is active. * 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; return false;
} }
@Override @Override
protected void onClose() protected void onClose()
{ {
@@ -72,20 +93,22 @@ public class Main
* Pretty much just forwards any messages to the browser and * Pretty much just forwards any messages to the browser and
* returns their value. * returns their value.
*/ */
browserLink.addUpdateListener((update, link) -> { browserLink.addUpdateListener((update, link) ->
{
logger.debug("Sending update {}", update); logger.debug("Sending update {}", update);
interfaceLink.sendUpdate(update); interfaceLink.sendUpdate(update);
}); });
interfaceLink.addListener((providedValue, previousReturn) -> { interfaceLink.addListener((providedValue, previousReturn) ->
{
logger.debug("Forwarding message to browser: {}", providedValue); logger.debug("Forwarding message to browser: {}", providedValue);
Object returnValue = browserLink.sendObject(providedValue).get(); Object returnValue = browserLink.sendObject(providedValue).get();
logger.debug("Forwarding return to interface: {}", returnValue); logger.debug("Forwarding return to interface: {}", returnValue);
return returnValue; return returnValue;
}); });
browserThread = new Thread(browserLink); browserThread = new Thread(browserLink);
interfaceThread = new Thread(interfaceLink); interfaceThread = new Thread(interfaceLink);
logger.debug("Starting threads."); logger.debug("Starting threads.");
browserThread.start(); browserThread.start();
interfaceThread.start(); interfaceThread.start();
@@ -136,7 +159,7 @@ public class Main
} }
} }
} }
private static void testMain() throws ExecutionException, InterruptedException private static void testMain() throws ExecutionException, InterruptedException
{ {
LinkedList<Future<Object>> requests = new LinkedList<>(); LinkedList<Future<Object>> requests = new LinkedList<>();

View File

@@ -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<LogEvent> events = new LinkedList<>();
/**
* Builds FileAppender instances.
*
* @param <B> The type to build
*/
public static class Builder<B extends Builder<B>> extends AbstractAppender.Builder<B>
implements org.apache.logging.log4j.core.util.Builder<SocketAppender>
{
@Override
public SocketAppender build()
{
return new SocketAppender(getName(), getFilter(), getOrCreateLayout(), isIgnoreExceptions(), getPropertyArray());
}
}
@PluginBuilderFactory
public static <B extends Builder<B>> B newBuilder()
{
return new Builder<B>().asBuilder();
}
public SocketAppender(final String name, final Filter filter, final Layout<? extends Serializable> 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<LogEvent> retrieveLogEvents()
{
synchronized (events)
{
List<LogEvent> logs = events.stream().toList();
events.clear();
return logs;
}
}
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
~ Copyright (c) 2021 William Hubbard. All Rights Reserved.
-->
<Configuration packages="edu.regis.universeplayer.browser" status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_ERR">
<PatternLayout pattern="%c:%L %-5level - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console"/>
</Root>
<Logger name="BrowserRunner" level="info"/>
</Loggers>
</Configuration>

View File

@@ -0,0 +1,5 @@
package edu.regis.universeplayer;
public class AutoGson
{
}

View File

@@ -79,7 +79,8 @@ public abstract class MessageRunner implements Runnable, MessageSerializer
BufferedInputStream browserIn = null; BufferedInputStream browserIn = null;
BufferedOutputStream browserOut = null; BufferedOutputStream browserOut = null;
MessagePacket packet; MessagePacket packet;
boolean running = true;
int messageNum = -1; int messageNum = -1;
byte[][] returnMessage; byte[][] returnMessage;
ByteBuffer numBuffer = ByteBuffer.allocate(4); ByteBuffer numBuffer = ByteBuffer.allocate(4);
@@ -89,7 +90,7 @@ public abstract class MessageRunner implements Runnable, MessageSerializer
browserIn = new BufferedInputStream(this.input); browserIn = new BufferedInputStream(this.input);
browserOut = new BufferedOutputStream(this.output); browserOut = new BufferedOutputStream(this.output);
while (!this.onRun()) while (running && !this.onRun())
{ {
/* /*
* Sends a messages * Sends a messages
@@ -104,17 +105,19 @@ public abstract class MessageRunner implements Runnable, MessageSerializer
{ {
messageNum = this.messagesSent; messageNum = this.messagesSent;
packet.returnValue.index = messageNum; packet.returnValue.index = messageNum;
writeMessage(browserOut, messageNum, packet.message);
synchronized (this.sentQueue) synchronized (this.sentQueue)
{ {
this.sentQueue.put(messageNum, packet); this.sentQueue.put(messageNum, packet);
this.messagesSent++; this.messagesSent++;
} }
writeMessage(browserOut, messageNum, packet.message);
} }
catch (IOException e) 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) catch (IOException e)
{ {
logger.error("Could not retrieve message", 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. * Release locks for any messages still waiting.
*/ */
synchronized (this.sendQueue) synchronized (this.sentQueue)
{ {
logger.debug("Clearing up {} messages", this.sendQueue.size()); logger.debug("Clearing up {} messages", this.sentQueue.size());
while (this.sendQueue.size() > 0) this.sentQueue.values().forEach(foundPacket -> foundPacket.returnMessage = new byte[0]);
{ this.sentQueue.clear();
packet = this.sendQueue.poll();
packet.returnMessage = null;
}
} }
synchronized (this.readLock) synchronized (this.readLock)
{ {

View File

@@ -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<Object> 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<Object> messFuture = runner.sendObject(mess1);
Thread.sleep(250);
assertFalse(messFuture.isDone());
Future<Object> messFuture2 = runner.sendObject(mess2);
Thread.sleep(250);
assertFalse(messFuture.isDone());
assertTrue(messFuture2.isDone());
Future<Object> 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");
}
}

View File

@@ -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");
}
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
~ Copyright (c) 2021 William Hubbard. All Rights Reserved.
-->
<Configuration packages="edu.regis.universeplayer.browserCommands" status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_ERR">
<PatternLayout pattern="%c:%L %-5level - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console"/>
</Root>
<Logger name="BrowserRunner" level="info"/>
</Loggers>
</Configuration>