From b89dd9783a3a09d1d4c67fd8f0dec6984d4170b5 Mon Sep 17 00:00:00 2001 From: Markil3 <75867393+Markil3@users.noreply.github.com> Date: Mon, 26 Jul 2021 22:53:44 -0700 Subject: [PATCH] Set up a callback system for the browser link. This should somewhat simulate asynchronous method calling. --- add-on/background.js | 9 +- .../universeplayer/addon/BrowserLink.java | 475 ++++++++++++++++++ .../edu/regis/universeplayer/addon/Main.java | 177 +------ addonInter/src/main/resources/log4j2.xml | 2 +- 4 files changed, 491 insertions(+), 172 deletions(-) create mode 100644 addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserLink.java diff --git a/add-on/background.js b/add-on/background.js index f8ff779..bcbb69a 100644 --- a/add-on/background.js +++ b/add-on/background.js @@ -9,12 +9,5 @@ Listen for messages from the app. */ port.onMessage.addListener((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"); -}); \ No newline at end of file diff --git a/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserLink.java b/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserLink.java new file mode 100644 index 0000000..2c81a44 --- /dev/null +++ b/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserLink.java @@ -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 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 sendObject(Object message) + { + String messageJson; + byte[] messageData; + ByteBuffer header; + byte[][] packet; + Future 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 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 returnValue; + + public MessagePacket(byte[][] message) + { + this(message, null); + } + + public MessagePacket(byte[][] message, Future returnValue) + { + this.message = message; + this.returnValue = returnValue; + } + } +} 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 6f3e48f..ae0b9ec 100644 --- a/addonInter/src/main/java/edu/regis/universeplayer/addon/Main.java +++ b/addonInter/src/main/java/edu/regis/universeplayer/addon/Main.java @@ -4,181 +4,32 @@ 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.LoggerFactory; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.charset.StandardCharsets; +import java.util.LinkedList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; public class Main { private static Logger logger = LoggerFactory.getLogger(Main.class); - private static final Gson gson = new Gson(); - - private static BufferedInputStream browserIn; - private static BufferedOutputStream browserOut; - - public static void main(String[] args) + public static void main(String[] args) throws ExecutionException, InterruptedException { - boolean running = true; - int rawLength, messageLength; - byte[] header = new byte[4]; - byte[] message; - JsonElement messageJson; - JsonPrimitive jsonPrimative; - JsonObject jsonObject; - ByteBuffer headerReader; - - System.err.printf("Working from directory %s\n", System.getProperty("user.dir")); - if (ByteOrder.nativeOrder().equals(ByteOrder.BIG_ENDIAN)) + LinkedList> requests = new LinkedList<>(); + BrowserLink link = new BrowserLink(); + Thread linkThread = new Thread(link); + linkThread.start(); + logger.info("Sending message"); + for (int i = 0, l = 20; i < l; i++) { - logger.info("Incoming messages will be Big-endian"); + Future future = link.sendObject("ping"); + requests.add(future); } - else + while (requests.size() > 0) { - logger.info("Incoming messages will be Little-endian"); - } - 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(); + logger.info("Message received: {}", requests.poll().get()); } } } diff --git a/addonInter/src/main/resources/log4j2.xml b/addonInter/src/main/resources/log4j2.xml index 43c8d67..00e50f3 100644 --- a/addonInter/src/main/resources/log4j2.xml +++ b/addonInter/src/main/resources/log4j2.xml @@ -4,7 +4,7 @@ --> - +