Set up a callback system for the browser link.

This should somewhat simulate asynchronous method calling.
This commit is contained in:
Markil3
2021-07-26 22:53:44 -07:00
parent 66ce116da4
commit b89dd9783a
4 changed files with 491 additions and 172 deletions

View File

@@ -9,12 +9,5 @@ Listen for messages from the app.
*/ */
port.onMessage.addListener((response) => { port.onMessage.addListener((response) => {
console.log("Received: " + response); console.log("Received: " + response);
port.postMessage("pong");
}); });
/*
On a click on the browser action, send the app a message.
*/
browser.browserAction.onClicked.addListener(() => {
console.log("Sending: ping");
port.postMessage("ping");
});

View File

@@ -0,0 +1,475 @@
/*
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/
package edu.regis.universeplayer.addon;
import com.google.gson.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* The browser link serves as a communication between this process and the
* browser. It automatically converts information as needed as it passes it to
* and from the browser process.
*
* @author William Hubbard
* @version 0.1
*/
public class BrowserLink implements Runnable
{
private static Logger logger = LoggerFactory.getLogger(BrowserLink.class);
public static final Gson gson = new Gson();
private BufferedInputStream browserIn;
private BufferedOutputStream browserOut;
private final Object readLock = new Object();
/**
* This queue serves as a cache for objects we need to send.
*/
private final LinkedList<MessagePacket> sendQueue = new LinkedList<>();
private boolean running;
@Override
public void run()
{
MessagePacket packet;
this.running = true;
try
{
browserIn = new BufferedInputStream(System.in);
browserOut = new BufferedOutputStream(System.out);
while (this.running)
{
/*
* Sends all messages
*/
synchronized (this.sendQueue)
{
packet = this.sendQueue.poll();
}
if (packet != null)
{
try
{
this.browserOut.write(packet.message[0]);
this.browserOut.write(packet.message[1]);
this.browserOut.flush();
/*
* Wait for a response from the browser.
*/
byte[][] returnMessage = this.readMessage();
packet.returnMessage = returnMessage;
if (returnMessage == null)
{
logger.info("Browser connection closed.");
this.running = false;
}
synchronized (this.readLock)
{
logger.trace("Received message, notifying futures.");
this.readLock.notifyAll();
}
}
catch (IOException e)
{
logger.error("Could not send message " + new String(packet.message[1], StandardCharsets.UTF_8), e);
}
}
}
}
finally
{
/*
* Release locks
*/
synchronized (this.sendQueue)
{
while (this.sendQueue.size() > 0)
{
packet = this.sendQueue.poll();
packet.returnMessage = null;
}
}
synchronized (this.readLock)
{
this.readLock.notifyAll();
}
try
{
browserIn.close();
}
catch (IOException e1)
{
logger.error("Could not close browser input", e1);
}
finally
{
try
{
browserOut.close();
}
catch (IOException e1)
{
logger.error("Could not close browser output", e1);
}
}
}
}
/**
* Reads a message from the browser input.
*
* @return - A new message packet from the browser, or null if it has been closed.
* @throws IOException - Should a read error occur.
*/
private byte[][] readMessage() throws IOException
{
ByteBuffer headerReader;
int rawLength, messageLength;
byte[] header = new byte[4];
byte[] message;
rawLength = browserIn.read(header);
/*
* A value of 0 or -1 indicates that there are no more messages, and
* that the channel has been closed.
*/
if (rawLength <= 0)
{
logger.info("The browser channel has been closed.");
return null;
}
headerReader = ByteBuffer.wrap(header, 0, rawLength);
/*
* The browser outputs in whatever the native endian order,
* which may not be what Java does.
*/
headerReader.order(ByteOrder.nativeOrder());
messageLength = headerReader.getInt();
logger.debug("Receiving message of {} length", messageLength);
message = new byte[messageLength];
browserIn.read(message);
logger.debug("Unpacking message {} (length {})", new String(message, StandardCharsets.UTF_8), messageLength);
return new byte[][] {header, message};
}
/**
* Sends an object to the browser.
*
* @param message - The object to send.
* @return A future for the response from the browser.
*/
public Future<Object> sendObject(Object message)
{
String messageJson;
byte[] messageData;
ByteBuffer header;
byte[][] packet;
Future<Object> future;
MessagePacket packetEntry;
logger.debug("Sending message {}", message);
messageJson = gson.toJson(message);
messageData = messageJson.getBytes(StandardCharsets.UTF_8);
header = ByteBuffer.allocate(4);
/*
* The browser outputs in whatever the native endian order is,
* which may not be what Java does.
*/
header.order(ByteOrder.nativeOrder());
header.putInt(messageData.length);
logger.debug("Writing message {} {}", header.array(), messageJson);
packetEntry = new MessagePacket(new byte[][] {header.array(), messageData});
future = new Future<>()
{
private boolean canceled = false;
@Override
public boolean cancel(boolean mayInterruptIfRunning)
{
synchronized (sendQueue)
{
if (sendQueue.remove(packetEntry))
{
canceled = true;
if (mayInterruptIfRunning)
{
logger.trace("Canceling future, notifying self futures.");
synchronized (readLock)
{
readLock.notifyAll();
}
}
else
{
logger.trace("Canceling future.");
}
return true;
}
else
{
return false;
}
}
}
@Override
public boolean isCancelled()
{
return this.canceled;
}
@Override
public boolean isDone()
{
return packetEntry.returnMessage != null;
}
@Override
public Object get() throws InterruptedException, ExecutionException
{
logger.trace("Future waiting for message");
synchronized (readLock)
{
while (!this.canceled && packetEntry.returnMessage == null)
{
readLock.wait();
}
}
if (packetEntry.returnMessage == null)
{
throw new InterruptedException();
}
logger.trace("Future has received message");
try
{
return getMessage(packetEntry.returnMessage[1]);
}
catch (IOException e)
{
throw new ExecutionException(e);
}
}
@Override
public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
{
logger.trace("Future waiting for message");
synchronized (readLock)
{
while (!this.canceled && packetEntry.returnMessage == null)
{
readLock.wait(unit.toMillis(timeout));
}
}
if (packetEntry.returnMessage == null)
{
throw new InterruptedException();
}
logger.trace("Future has received message");
try
{
return getMessage(packetEntry.returnMessage[1]);
}
catch (IOException e)
{
throw new ExecutionException(e);
}
}
};
packetEntry.returnValue = future;
synchronized (this.sendQueue)
{
sendQueue.add(packetEntry);
}
return future;
}
/**
* Queries for the next message from the browser.
*
* @return The received message
* @throws IOException - If there is an error when reading the next object,
* or if the browser connection is closed.
*/
private Object getMessage(byte[] messageData) throws IOException
{
JsonElement message = gson.fromJson(new String(messageData, StandardCharsets.UTF_8), JsonElement.class);
return getMessage(message);
}
/**
* Translates a JSON element into a Java object
*
* @param message - The message to translate.
* @return A java object, whether it be a primitive wrapper or an actual
* object.
* @throws IOException - Thrown if we cannot determine what type of JSON
* element was provided.
*/
private Object getMessage(JsonElement message) throws IOException
{
if (message.isJsonPrimitive())
{
return getPrimitiveMessage((JsonPrimitive) message);
}
else if (message.isJsonObject())
{
return getObjectMessage((JsonObject) message);
}
else if (message.isJsonArray())
{
return getArrayMessage((JsonArray) message);
}
else if (message.isJsonNull())
{
return null;
}
else
{
throw new IOException("Unknown JSON element " + message.getClass());
}
}
/**
* Translates a JSON primitive element into a Java object.
*
* @param message - The message to translate.
* @return A java object, whether it be a {@link Number} object, a
* {@link Boolean} object, or a {@link String} object.
* @throws IOException - Thrown if we cannot determine what type of JSON
* element was provided.
*/
private Object getPrimitiveMessage(JsonPrimitive message) throws IOException
{
if (message.isBoolean())
{
return message.getAsBoolean();
}
else if (message.isNumber())
{
return message.getAsNumber();
}
else if (message.isString())
{
return message.getAsString();
}
else
{
throw new IOException("Unknown JSON element " + message.getClass());
}
}
/**
* Translates a JSON object element into a Java object.
*
* @param message - The message to translate.
* @return A java object, or a {@link HashMap} containing objects if we
* cannot parse it.
*/
private Object getObjectMessage(JsonObject message)
{
HashMap<String, Object> type;
Class<?> clazz = null;
if (message.has("type"))
{
try
{
clazz = Class.forName(message.getAsJsonPrimitive("type").getAsString());
}
catch (ClassNotFoundException | ClassCastException e)
{
logger.error("Illegal class type " + message.get("type"), e);
}
}
if (clazz != null)
{
return gson.fromJson(message, clazz);
}
else
{
/*
* If all else fails, just return a standard hashmap. That should work.
*/
type = new HashMap<>();
message.entrySet().forEach(entry -> {
try
{
type.put(entry.getKey(), getMessage(entry.getValue()));
}
catch (IOException e)
{
logger.error("Could not parse message entry " + entry.getKey() + " (" + entry.getValue() + ")", e);
}
});
return type;
}
}
/**
* Translates a JSON array element into a Java array.
*
* @param message - The message to translate.
* @return A java array. It may contain a variety of element types.
* @throws IOException - Thrown if we cannot determine what type of JSON
* element was provided.
*/
private Object getArrayMessage(JsonArray message) throws IOException
{
Object[] returnValue = new Object[message.size()];
for (int i = 0, l = returnValue.length; i < l; i++)
{
returnValue[i] = getMessage(message.get(i));
}
return returnValue;
}
/**
* This represents a sent message in storage.
*/
private class MessagePacket
{
public final byte[][] message;
public byte[][] returnMessage;
public Future<Object> returnValue;
public MessagePacket(byte[][] message)
{
this(message, null);
}
public MessagePacket(byte[][] message, Future<Object> returnValue)
{
this.message = message;
this.returnValue = returnValue;
}
}
}

View File

@@ -4,181 +4,32 @@
package edu.regis.universeplayer.addon; package edu.regis.universeplayer.addon;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream; import java.util.LinkedList;
import java.io.BufferedOutputStream; import java.util.concurrent.ExecutionException;
import java.io.DataOutputStream; import java.util.concurrent.Future;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
public class Main public class Main
{ {
private static Logger logger = LoggerFactory.getLogger(Main.class); private static Logger logger = LoggerFactory.getLogger(Main.class);
private static final Gson gson = new Gson(); public static void main(String[] args) throws ExecutionException, InterruptedException
private static BufferedInputStream browserIn;
private static BufferedOutputStream browserOut;
public static void main(String[] args)
{ {
boolean running = true; LinkedList<Future<Object>> requests = new LinkedList<>();
int rawLength, messageLength; BrowserLink link = new BrowserLink();
byte[] header = new byte[4]; Thread linkThread = new Thread(link);
byte[] message; linkThread.start();
JsonElement messageJson; logger.info("Sending message");
JsonPrimitive jsonPrimative; for (int i = 0, l = 20; i < l; i++)
JsonObject jsonObject;
ByteBuffer headerReader;
System.err.printf("Working from directory %s\n", System.getProperty("user.dir"));
if (ByteOrder.nativeOrder().equals(ByteOrder.BIG_ENDIAN))
{ {
logger.info("Incoming messages will be Big-endian"); Future<Object> future = link.sendObject("ping");
requests.add(future);
} }
else while (requests.size() > 0)
{ {
logger.info("Incoming messages will be Little-endian"); logger.info("Message received: {}", requests.poll().get());
}
try
{
browserIn = new BufferedInputStream(System.in);
browserOut = new BufferedOutputStream(System.out);
while (running)
{
try
{
rawLength = browserIn.read(header);
if (rawLength <= 0)
{
running = false;
break;
}
headerReader = ByteBuffer.wrap(header, 0, rawLength);
/*
* The browser outputs in whatever the native endian order,
* which may not be what Java does.
*/
headerReader.order(ByteOrder.nativeOrder());
messageLength = headerReader.getInt();
logger.debug("Receiving message of {} length", messageLength);
message = new byte[messageLength];
browserIn.read(message);
logger.debug("Unpacking message {} (length {})", new String(message, "UTF-8"), messageLength);
messageJson = gson.fromJson(new String(message, StandardCharsets.UTF_8), JsonElement.class);
logger.debug("Reading JSON element {} (type: {})", messageJson, messageJson.getClass());
if (messageJson.isJsonPrimitive())
{
jsonPrimative = ((JsonPrimitive) messageJson);
if (jsonPrimative.isBoolean())
{
handleMessage(jsonPrimative.getAsBoolean());
}
else if (jsonPrimative.isNumber())
{
handleMessage(jsonPrimative.getAsNumber());
}
else if (jsonPrimative.isString())
{
handleMessage(jsonPrimative.getAsString());
}
}
else if (messageJson.isJsonObject())
{
jsonObject = ((JsonObject) messageJson);
// TODO - Parse the object into Java objects
}
}
catch (IOException e)
{
logger.error("Could not create input stream", e);
// Forward it to the browser via STDERR
e.printStackTrace();
}
}
}
finally
{
try
{
browserIn.close();
}
catch (IOException e1)
{
logger.error("Could not close browser input", e1);
}
try
{
browserOut.close();
}
catch (IOException e1)
{
logger.error("Could not close browser output", e1);
}
}
}
/**
* A callback for when a message is sent from the browser
*
* @param message - The message sent.
*/
private static void handleMessage(Object message)
{
if (message instanceof String)
{
if (message.equals("ping"))
{
sendMessage("pong");
}
}
}
/**
* A callback for when a message is sent from the browser
*
* @param message - The message sent.
*/
public static void sendMessage(Object message)
{
String messageJson;
byte[] messageData;
ByteBuffer headerWriter;
logger.debug("Sending message {}", message);
messageJson = gson.toJson(message);
messageData = messageJson.getBytes(StandardCharsets.UTF_8);
headerWriter = ByteBuffer.allocate(4);
/*
* The browser outputs in whatever the native endian order,
* which may not be what Java does.
*/
headerWriter.order(ByteOrder.nativeOrder());
headerWriter.putInt(messageData.length);
logger.debug("Writing message {} {}", headerWriter.array(), messageJson);
try
{
browserOut.write(headerWriter.array());
browserOut.write(messageData);
browserOut.flush();
}
catch (IOException e)
{
logger.error("Could not write message", e);
e.printStackTrace();
} }
} }
} }

View File

@@ -4,7 +4,7 @@
--> -->
<Configuration status="WARN"> <Configuration status="WARN">
<Appenders> <Appenders>
<Console name="Console" target="SYSTEM_OUT"> <Console name="Console" target="SYSTEM_ERR">
<PatternLayout pattern="%d%c%L %-5level - %msg%n"/> <PatternLayout pattern="%d%c%L %-5level - %msg%n"/>
</Console> </Console>
<File name="File" fileName="addonInter.log" append="false"> <File name="File" fileName="addonInter.log" append="false">