diff --git a/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserLink.java b/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserLink.java index 360cbb3..c7b56a7 100644 --- a/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserLink.java +++ b/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserLink.java @@ -27,15 +27,15 @@ import java.util.HashMap; */ public class BrowserLink extends MessageRunner { - private static final Logger logger = LoggerFactory.getLogger(BrowserLink.class); +// private static final Logger logger = LoggerFactory.getLogger(BrowserLink.class); public static final Gson gson = new Gson(); /** * Creates a message runner. */ - public BrowserLink() + public BrowserLink(String name) { - super(System.in, System.out); + super(name, System.in, System.out); } @Override @@ -64,7 +64,7 @@ public class BrowserLink extends MessageRunner ByteBuffer lengthBuffer = ByteBuffer.allocate(4); lengthBuffer.order(ByteOrder.nativeOrder()); - logger.debug("Writing message {} {}", messageNum, messageString); + getLogger().debug("Writing message {} {}", messageNum, messageString); /* * Writes the message length */ @@ -92,7 +92,7 @@ public class BrowserLink extends MessageRunner readLength = in.read(lengthBuffer.array()); if (readLength == 0) { - logger.debug("Input stream closed, no more messages."); + getLogger().debug("Input stream closed, no more messages."); return null; } messageLength = lengthBuffer.getInt(); @@ -101,7 +101,7 @@ public class BrowserLink extends MessageRunner readLength = in.read(message); if (readLength < message.length) { - logger.warn("Message shorter than reported (expected {} bytes, got {} bytes)", message.length, readLength); + getLogger().warn("Message shorter than reported (expected {} bytes, got {} bytes)", message.length, readLength); } messageJson = gson.fromJson(new String(message, StandardCharsets.UTF_8), JsonObject.class); messageNum = messageJson.get("messageNum").getAsInt(); @@ -115,7 +115,7 @@ public class BrowserLink extends MessageRunner lengthBuffer.put(new byte[4]); lengthBuffer.clear(); lengthBuffer.putInt(messageNum); - logger.debug("Reading message {}", messageNum); + getLogger().debug("Reading message {}", messageNum); return new byte[][] {lengthBuffer.array(), message}; } @@ -200,7 +200,7 @@ public class BrowserLink extends MessageRunner } catch (ClassNotFoundException | ClassCastException e) { - logger.error("Illegal class type " + message.get("type"), e); + getLogger().error("Illegal class type " + message.get("type"), e); } } @@ -221,7 +221,7 @@ public class BrowserLink extends MessageRunner } catch (IOException e) { - logger.error("Could not parse message entry " + entry.getKey() + " (" + entry.getValue() + ")", e); + getLogger().error("Could not parse message entry " + entry.getKey() + " (" + entry.getValue() + ")", e); } }); return type; 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 972fb65..546c062 100644 --- a/addonInter/src/main/java/edu/regis/universeplayer/addon/Main.java +++ b/addonInter/src/main/java/edu/regis/universeplayer/addon/Main.java @@ -4,10 +4,14 @@ package edu.regis.universeplayer.addon; +import edu.regis.universeplayer.browserCommands.BrowserConstants; +import edu.regis.universeplayer.browserCommands.MessageHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; import java.util.LinkedList; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -16,10 +20,91 @@ public class Main { private static final Logger logger = LoggerFactory.getLogger(Main.class); - public static void main(String[] args) throws ExecutionException, InterruptedException + public static void main(String[] args) throws IOException, InterruptedException + { + ServerSocket socketSource = null; + Socket socket = null; + MessageHandler interfaceLink; + BrowserLink browserLink; + Thread interfaceThread, browserThread; + try + { + try + { + logger.debug("Connecting to browser"); + browserLink = new BrowserLink("BrowserLink"); + logger.debug("Setting up server"); + socketSource = new ServerSocket(BrowserConstants.PORT); + logger.debug("Server started"); + socket = socketSource.accept(); + logger.debug("Connection received"); + interfaceLink = new MessageHandler("InterfaceHandler", socket.getInputStream(), socket.getOutputStream()); + /* + * Pretty much just forwards any messages to the browser and + * returns their value. + */ + 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(); + logger.debug("Joining threads."); + browserThread.join(); + interfaceThread.join(); + logger.debug("Threads completed."); + } + catch (IOException e) + { + logger.error("Could not initialize socket", e); + throw e; + } + } + catch (Throwable e) + { + logger.error("ERROR", e); + throw e; + } + finally + { + logger.debug("Shutting down interface link"); + if (socket != null) + { + try + { + socket.close(); + } + catch (IOException e) + { + logger.error("Could not close socket", e); + } + finally + { + logger.debug("Shutting down server"); + try + { + socketSource.close(); + } + catch (IOException e) + { + logger.error("Could not close server", e); + } + } + } + } + } + + private static void testMain() throws ExecutionException, InterruptedException { LinkedList> requests = new LinkedList<>(); - BrowserLink link = new BrowserLink(); + BrowserLink link = new BrowserLink("BrowserLink"); Thread linkThread = new Thread(link); linkThread.start(); logger.info("Sending message"); diff --git a/addonInter/src/main/resources/log4j2.xml b/addonInter/src/main/resources/log4j2.xml index 00e50f3..3aa5a6f 100644 --- a/addonInter/src/main/resources/log4j2.xml +++ b/addonInter/src/main/resources/log4j2.xml @@ -9,7 +9,7 @@ - %d{HH:mm:ss.SSS} - %45c:%-4L - %-5level - %msg%n + %d{HH:mm:ss.SSS} - %c.%M(%F:%L) - %-5level - %msg%n @@ -17,5 +17,6 @@ + \ No newline at end of file diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/BrowserCommand.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/BrowserCommand.java new file mode 100644 index 0000000..c441f76 --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/BrowserCommand.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browserCommands; + +/** + * Browser commands are used to send orders to the browser, requesting + * confirmation as to whether those commands were completed or not. + */ +public interface BrowserCommand extends BrowserQuery +{ + @Override + default Class getReturnType() + { + return Void.class; + } +} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/BrowserConstants.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/BrowserConstants.java new file mode 100644 index 0000000..ec50ac8 --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/BrowserConstants.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browserCommands; + +public class BrowserConstants +{ + /** + * The IP that the connection will be hosted on. + */ + public static final String IP = "127.0.0.1"; + /** + * The port both processes use for connection. + */ + public static final int PORT = 3000; +} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/BrowserError.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/BrowserError.java new file mode 100644 index 0000000..29db868 --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/BrowserError.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browserCommands; + +import java.io.Serializable; + +/** + * A browser error is a special Throwable that contains information about an + * error that occurred within the browser. This is so that we can properly log + * browser errors under standard Java logging. + * + * @author William Hubbard + * @version 0.1 + */ +public class BrowserError extends Throwable +{ + private final String name; + private final String message; + + /** + * Creats a browser error. + * + * @param name - The type of error thrown. + * @param message - The message as reported by the browser. + * @param stackTrace - The stack trace string provided by the browser. + */ + public BrowserError(String name, String message, String stackTrace) + { + super(name + ": " + message, null, false, true); + this.name = name; + this.message = message; + this.setStackTrace(this.parseFirefoxTrace(stackTrace)); + } + + /** + * Gets the type of error returned by the browser. + * + * @return - The browser error type. + */ + public String getType() + { + return this.name; + } + + /** + * Gets the original message from the browser. + * + * @return - The browser error message. + */ + public String getBrowserMessage() + { + return this.message; + } + + /** + * Parses a stack trace string as per the specifications of Mozilla Firefox + * version 30+. + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Stack. + * + * @param stackTrace - The stack trace string provided by the browser. + * @return A stack trace that Java can parse. + */ + private StackTraceElement[] parseFirefoxTrace(String stackTrace) + { + StackTraceElement trace; + String methodName; + String file; + String fileLoc; + int line; +// int column; + + /* + * Parses the Firefox trace into + */ + String[] scriptTrace = stackTrace.split("\n"); + StackTraceElement[] javaTrace = new StackTraceElement[scriptTrace.length]; + for (int i = 0, l = scriptTrace.length; i < l; i++) + { + methodName = scriptTrace[i].substring(0, scriptTrace[i].indexOf('@')); + file = scriptTrace[i].substring(methodName.length() + 1, scriptTrace[i].indexOf(':')); +// column = Integer.parseInt(scriptTrace[i].substring(scriptTrace[i].lastIndexOf(':'))); + /* + * Windows paths often have colons in their file name, so we can't use the first index of one. + */ + fileLoc = scriptTrace[i].substring(0, scriptTrace[i].lastIndexOf(':')); + line = Integer.parseInt(fileLoc.substring(fileLoc.lastIndexOf(':'))); + + trace = new StackTraceElement(null, null, null, "", methodName, file, line); + javaTrace[i] = trace; + } + + return javaTrace; + } + + /** + * Parses a stack trace string as per the specifications of IE 10+ (and used + * by modern Chromium browsers). + * https://web.archive.org/web/20140210004225/https://msdn.microsoft.com/en-us/library/windows/apps/hh699850.aspx + * + * @param stackTrace - The stack trace string to parse. + * @return A stack trace that Java can parse. + */ + private StackTraceElement[] parseChromeTrace(String stackTrace) + { + StackTraceElement trace; + String methodName; + String fileLoc; + int line; +// int column; + + /* + * Parses the Chromium trace. + */ + String[] scriptTrace = stackTrace.split("\n"); + /* + * The first line of the trace contains the error message itself. We do + * not want this in the stack trace. + */ + StackTraceElement[] javaTrace = new StackTraceElement[scriptTrace.length - 1]; + + for (int i = 1, l = scriptTrace.length; i < l; i++) + { +// column = Integer.parseInt(scriptTrace[i].substring(scriptTrace[i].lastIndexOf(':'))); + fileLoc = scriptTrace[i].substring(0, scriptTrace[i].lastIndexOf(':')); + line = Integer.parseInt(fileLoc.substring(fileLoc.lastIndexOf(':'))); + methodName = fileLoc.substring(fileLoc.indexOf("at") + 3, fileLoc.lastIndexOf(':')); + + trace = new StackTraceElement(null, null, null, "", methodName, null, line); + javaTrace[i - 1] = trace; + } + + return javaTrace; + } +} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/BrowserQuery.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/BrowserQuery.java new file mode 100644 index 0000000..ca35c38 --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/BrowserQuery.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browserCommands; + +import java.io.Serializable; + +/** + * Objects of this type are used by the interface to request information from + * the browser. + * + * @author William Hubbard + * @version 0.1 + */ +public interface BrowserQuery extends Serializable +{ + /** + * Obtains the name of the command. + * @return The command name. + */ + String getCommandName(); + + /** + * Obtains the type of value this command returns. + * @return The return type. + */ + Class getReturnType(); +} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandConfirmation.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandConfirmation.java new file mode 100644 index 0000000..763a7eb --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandConfirmation.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browserCommands; + +import java.io.Serializable; + +/** + * This class contains information as to the success of a command. + * + * @author William Hubbard + * @version 0.1 + */ +public class CommandConfirmation implements Serializable +{ + /** + * The error returned by the browser, or null if execution was successful. + */ + private BrowserError errorCode; + private String message; + + /** + * Confirms a successful command. + */ + public CommandConfirmation() + { + this("Command successful"); + } + + /** + * Confirms a successful command. + * + * @param message - Details on the nature of the command execution. + */ + public CommandConfirmation(String message) + { + this((BrowserError) null); + this.message = message; + } + + /** + * Confirms an unsuccessful command. + * + * @param error - The error thrown, or null if the command was successful. + */ + public CommandConfirmation(BrowserError error) + { + this.errorCode = error; + } + + /** + * Checks to see if the command executed properly. + * + * @return True if the command was successful, false otherwise. + */ + public boolean wasSuccessful() + { + return this.errorCode == null; + } + + /** + * Obtains the error returned by the browser. + * + * @return The error, or null if the command was successful. + */ + public BrowserError getError() + { + return this.errorCode; + } + + /** + * Obtains details as to the nature of the command execution. + * + * @return The detailed message on the command. + */ + public String getMessage() + { + return this.errorCode != null ? this.errorCode.getMessage() : this.message; + } +} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandLoadSong.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandLoadSong.java new file mode 100644 index 0000000..7199a47 --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandLoadSong.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browserCommands; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * This command is used to tell the browser to load a song URL, such as a + * YouTube video. + * + * @author William Hubbard + * @version 0.1 + */ +public class CommandLoadSong implements BrowserCommand +{ + /** + * The song to load. + */ + private URL song; + + /** + * Used for serialization only. Do not use. + */ + public CommandLoadSong() + { + + } + + /** + * Tells the browser to load a song. + * + * @param song - The song to load. + */ + public CommandLoadSong(URL song) + { + this.song = song; + } + + /** + * Tells the browser to load a song. + * + * @param song - The song to load. + */ + public CommandLoadSong(String song) throws MalformedURLException + { + this.song = new URL(song); + } + + @Override + public String getCommandName() + { + return "loadSong"; + } + + /** + * Obtains the song that is to be loaded. + * + * @return - The loaded song. + */ + public URL getSong() + { + return this.song; + } +} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandReturn.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandReturn.java new file mode 100644 index 0000000..4f4b6e2 --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandReturn.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browserCommands; + +import java.io.Serializable; + +/** + * This wrapper class serves to return a browser return value over serialization protocols. + * + * @param The type of return value. + * @author William Hubbard + * @version 0.1 + */ +public class CommandReturn implements Serializable +{ + private T returnValue; + + private CommandConfirmation confirmation; + + /** + * For serialization only. Do not use. + */ + public CommandReturn() + { + + } + + /** + * Creates a new return object. + * + * @param value - The value to return. + * @param confirmation - Information on the execution of the command. + */ + public CommandReturn(T value, CommandConfirmation confirmation) + { + this.returnValue = value; + this.confirmation = confirmation; + } + + /** + * Obtains the value returned. + * + * @return The returned value. May be null. + */ + public T getReturnValue() + { + return this.returnValue; + } + + /** + * Obtains information about the execution of this command. + * + * @return The execution status. + */ + public CommandConfirmation getConfirmation() + { + return this.confirmation; + } +} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandSeek.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandSeek.java new file mode 100644 index 0000000..b145a58 --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandSeek.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browserCommands; + +/** + * The seek command tells the browser to move playback to a certain location. + * + * @author William Hubbard + * @version 0.1 + */ +public class CommandSeek implements BrowserCommand +{ + /** + * The time to seek to. + */ + private float time; + /** + * Whether the {@link #time} parameter should be interpreted as relative to + * the current position or absolute. + */ + private boolean relative; + + /** + * For serialization only. Do not use. + */ + public CommandSeek() + { + + } + + /** + * Tells the browser to seek to a certain absolute time in the song. + * + * @param time - The time to seek to. + */ + public CommandSeek(float time) + { + this.time = time; + this.relative = false; + } + + /** + * Tells the browser to seek to a certain time in the song. + * + * @param time - The time to seek to. + * @param relative - Whether or not the time should be treated as relative + * to the current play time or not. + */ + public CommandSeek(float time, boolean relative) + { + this.time = time; + this.relative = relative; + } + + @Override + public String getCommandName() + { + return "seek"; + } + + /** + * Obtains the time to seek to. + * + * @return The new playback time. + */ + public float getSeekTime() + { + return this.time; + } + + /** + * Checks to see whether the command should be interpreted as local to the + * current play time. + * + * @return Whether the seek time is local or not. + */ + public boolean isRelative() + { + return this.relative; + } +} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandSetPlayback.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandSetPlayback.java new file mode 100644 index 0000000..1b36558 --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/CommandSetPlayback.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browserCommands; + +/** + * This command sets the playback status of the browser. + * + * @author William Hubbard + * @version 0.1 + */ +public class CommandSetPlayback implements BrowserCommand +{ + /** + * An enum for playback status. + */ + public enum Playback + { + PLAY, PAUSE + } + + /** + * The playback status communicated by the interface. + */ + private Playback status; + + /** + * Creates a play command. + * @return A new play command. + */ + public static CommandSetPlayback playCommand() + { + return new CommandSetPlayback(Playback.PLAY); + } + + /** + * Creates a pause command. + * @return A new pause command. + */ + public static CommandSetPlayback pauseCommand() + { + return new CommandSetPlayback(Playback.PAUSE); + } + + /** + * Used for serialization only. Do not use. + */ + public CommandSetPlayback() + { + + } + + /** + * Tells the browser to chang the playback. + * + * @param status - Whether the browser should be playing or pausing. + */ + public CommandSetPlayback(Playback status) + { + this.status = status; + } + + @Override + public String getCommandName() + { + return "playback"; + } + + /** + * Obtains the playback command. + * + * @return Whether the browser should be playing or not. + */ + public Playback getPlayback() + { + return this.status; + } + + /** + * Checks whether this command sets the browser playing. + * + * @return Whether the browser should be playing or not. + */ + public boolean shouldPlay() + { + return this.status == Playback.PLAY; + } + + /** + * Checks whether this command pauses + * + * @return Whether the browser should be playing or not. + */ + public boolean shouldPause() + { + return this.status == Playback.PAUSE; + } +} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageHandler.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageHandler.java index 56ce186..0c7f0f5 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageHandler.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageHandler.java @@ -13,6 +13,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.Map; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -22,7 +23,9 @@ import java.util.concurrent.Future; */ public class MessageHandler implements Runnable, MessageSerializer { - private static final Logger logger = LoggerFactory.getLogger(MessageHandler.class); + private final Logger logger; + + private final String name; private final InputStream input; private final OutputStream output; @@ -34,16 +37,25 @@ public class MessageHandler implements Runnable, MessageSerializer /** * 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 MessageHandler(InputStream input, OutputStream output) + public MessageHandler(String name, InputStream input, OutputStream output) { + this.name = name; + this.logger = LoggerFactory.getLogger(name); this.input = input; this.output = output; this.executor = Executors.newCachedThreadPool(); } + @Override + public Logger getLogger() + { + return logger; + } + @Override public void run() { @@ -84,6 +96,7 @@ public class MessageHandler implements Runnable, MessageSerializer messageResponse = this.triggerListeners(messageOb); synchronized (this.messageResponses) { + numBuffer.clear(); this.messageResponses.put(numBuffer.getInt(), messageResponse); } } @@ -108,7 +121,7 @@ public class MessageHandler implements Runnable, MessageSerializer if (responses.getValue().isDone()) { toRemove.add(responses.getKey()); - messageByte = serializeObject(responses.getValue()); + messageByte = serializeObject(responses.getValue().get()); writeMessage(browserOut, responses.getKey(), messageByte); } } @@ -123,6 +136,10 @@ public class MessageHandler implements Runnable, MessageSerializer } } } + catch (Throwable e) + { + logger.error(this.getClass().getName() + " Error", e); + } finally { /* @@ -255,6 +272,6 @@ public class MessageHandler implements Runnable, MessageSerializer * have generated return values. * @return The value to be returned to the remote. */ - Object onMessage(Object providedValue, Object previousReturn); + Object onMessage(Object providedValue, Object previousReturn) throws IOException, ExecutionException, InterruptedException; } } 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 ea5b6a8..90a7ed2 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageRunner.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageRunner.java @@ -22,8 +22,9 @@ import java.util.concurrent.TimeoutException; */ public abstract class MessageRunner implements Runnable, MessageSerializer { - private static final Logger logger = LoggerFactory.getLogger(MessageRunner.class); + private final Logger logger; + private final String name; private final InputStream input; private final OutputStream output; private final Object readLock = new Object(); @@ -41,15 +42,24 @@ public abstract class MessageRunner implements Runnable, MessageSerializer /** * 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 MessageRunner(InputStream input, OutputStream output) + public MessageRunner(String name, InputStream input, OutputStream output) { + this.name = name; + this.logger = LoggerFactory.getLogger(name); this.input = input; this.output = output; } + @Override + public Logger getLogger() + { + return logger; + } + @Override public void run() { @@ -91,7 +101,7 @@ public abstract class MessageRunner implements Runnable, MessageSerializer } catch (IOException e) { - logger.error("Could not send message " + new String(packet.message, StandardCharsets.UTF_8), e); + logger.error("Could not send message " + this.messagesSent + " " + new String(packet.message, StandardCharsets.UTF_8), e); } } @@ -145,6 +155,10 @@ public abstract class MessageRunner implements Runnable, MessageSerializer } } } + catch (Throwable e) + { + logger.error(this.getClass().getName() + " Error", e); + } finally { /* diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageSerializer.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageSerializer.java index 0fd5730..f5fc216 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageSerializer.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/MessageSerializer.java @@ -5,14 +5,13 @@ package edu.regis.universeplayer.browserCommands; import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.*; import java.nio.ByteBuffer; public interface MessageSerializer { - Logger logger = LoggerFactory.getLogger(MessageSerializer.class); + Logger getLogger(); /** * Converts an object into a form that can be sent. @@ -63,7 +62,7 @@ public interface MessageSerializer default void writeMessage(OutputStream out, int messageNum, byte[] message) throws IOException { ByteBuffer lengthBuffer = ByteBuffer.allocate(4); - logger.debug("Writing message {} {}", messageNum, message); + getLogger().trace("Writing message {} {}", messageNum, message); /* * Writes the message number. */ @@ -110,7 +109,7 @@ public interface MessageSerializer readLength = in.read(messageNum); if (readLength == 0) { - logger.debug("Input stream closed, no more messages."); + getLogger().debug("Input stream closed, no more messages."); return null; } /* @@ -120,7 +119,7 @@ public interface MessageSerializer readLength = in.read(lengthBuffer.array()); if (readLength == 0) { - logger.error("Malformed message, could not get message length."); + getLogger().error("Malformed message, could not get message length."); return null; } message = new byte[lengthBuffer.getInt()]; @@ -130,9 +129,9 @@ public interface MessageSerializer readLength = in.read(message); if (readLength < message.length) { - logger.warn("Message shorter than reported (expected {} bytes, got {} bytes)", message.length, readLength); + getLogger().warn("Message shorter than reported (expected {} bytes, got {} bytes)", message.length, readLength); } - logger.debug("Reading message"); + getLogger().trace("Reading message"); return new byte[][] {messageNum, message}; } } diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryFuture.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryFuture.java new file mode 100644 index 0000000..13cf25f --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryFuture.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browserCommands; + +import java.util.concurrent.*; + +/** + * This future contains extra information regarding the success of a browser + * command on the browser side. + * + * @param The return type. + */ +public interface QueryFuture extends Future +{ + /** + * Waits if necessary for the computation to complete, and then checks to see whether execution was successful. + * + * @return A status code on whether the command was successful or not. + * @throws CancellationException – if the computation was cancelled + * @throws ExecutionException – if the computation threw an exception + * @throws InterruptedException – if the current thread was interrupted while waiting + */ + CommandConfirmation getConfirmation() throws CancellationException, ExecutionException, InterruptedException; + + /** + * Waits if necessary for the computation to complete, and then checks to see whether execution was successful. + * + * @param timeout - How long to hang the thread. + * @param unit - What units was specified in the timeout parameter. + * @return A status code on whether the command was successful or not. + * @throws TimeoutException – if the computation timed out + * @throws ExecutionException – if the computation threw an exception + * @throws InterruptedException – if the current thread was interrupted while waiting + */ + CommandConfirmation getConfirmation(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; +} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryLength.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryLength.java new file mode 100644 index 0000000..2773d7f --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryLength.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browserCommands; + +/** + * This query asks for the total length of the song. + * + * @author William Hubbard + * @version 0.1 + */ +public class QueryLength implements BrowserQuery +{ + @Override + public String getCommandName() + { + return "length"; + } + + @Override + public Class getReturnType() + { + return Float.class; + } +} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryPaused.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryPaused.java new file mode 100644 index 0000000..695ff0a --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryPaused.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browserCommands; + +/** + * This query asks for confirmation as to whether the browser playback is paused + * or not. + * + * @author William Hubbard + * @version 0.1 + */ +public class QueryPaused implements BrowserQuery +{ + @Override + public String getCommandName() + { + return "isPaused"; + } + + @Override + public Class getReturnType() + { + return Boolean.class; + } +} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryPlaying.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryPlaying.java new file mode 100644 index 0000000..3b99ad7 --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryPlaying.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browserCommands; + +/** + * This query asks for whether a song is currently playing or not. Note that + * this is not the same as whether it is paused or not, as a song could have + * run into a playback error. + * + * @author William Hubbard + * @version 0.1 + */ +public class QueryPlaying implements BrowserQuery +{ + @Override + public String getCommandName() + { + return "isPlaying"; + } + + @Override + public Class getReturnType() + { + return Boolean.class; + } +} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QuerySong.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QuerySong.java new file mode 100644 index 0000000..312edf1 --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QuerySong.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browserCommands; + +import java.net.URL; + +/** + * This query asks for the currently loaded song. + * + * @author William Hubbard + * @version 0.1 + */ +public class QuerySong implements BrowserQuery +{ + @Override + public String getCommandName() + { + return "getSong"; + } + + @Override + public Class getReturnType() + { + return URL.class; + } +} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryTime.java b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryTime.java new file mode 100644 index 0000000..9373424 --- /dev/null +++ b/browserCommands/src/main/java/edu/regis/universeplayer/browserCommands/QueryTime.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 William Hubbard. All Rights Reserved. + */ + +package edu.regis.universeplayer.browserCommands; + +/** + * This query asks for the current playback time. + * + * @author William Hubbard + * @version 0.1 + */ +public class QueryTime implements BrowserQuery +{ + @Override + public String getCommandName() + { + return "currentTime"; + } + + @Override + public Class getReturnType() + { + return Float.class; + } +} diff --git a/interface/src/main/java/edu/regis/universeplayer/Player.java b/interface/src/main/java/edu/regis/universeplayer/Player.java index 75438d6..66f0a7a 100644 --- a/interface/src/main/java/edu/regis/universeplayer/Player.java +++ b/interface/src/main/java/edu/regis/universeplayer/Player.java @@ -4,8 +4,11 @@ package edu.regis.universeplayer; +import edu.regis.universeplayer.browserCommands.QueryFuture; import edu.regis.universeplayer.data.Song; +import java.util.concurrent.Future; + /** * This interface serves as the connection to a music player of some sort, whether it be * browser-based or from a file. @@ -22,54 +25,65 @@ public interface Player * @return The current song, or null if none is playing. */ Song getCurrentSong(); - + /** * Loads up a song * * @param song - The song to load. + * @return A confirmation of whether the command was successful or not. */ - void play(T song); - + QueryFuture loadSong(T song); + /** * Enables playback of the current song, if one is active. + * @return A confirmation of whether the command was successful or not. */ - void play(); - + QueryFuture play(); + /** * Pauses playback of the current song. + * @return A confirmation of whether the command was successful or not. */ - void pause(); - + QueryFuture pause(); + /** * Toggles between playing and pausing the current song. + * @return A confirmation of whether the command was successful or not. */ - void togglePlayback(); - + QueryFuture togglePlayback(); + /** * Sets the current song time to the specified position. * * @param time - The specified time in the song, in seconds. + * @return A confirmation of whether the command was successful or not. */ - void seek(float time); - + QueryFuture seek(float time); + /** * Checks to see if the song is paused. * * @return Whether or not the song is paused. */ - boolean isPaused(); - + QueryFuture isPaused(); + /** * Obtains the time we are currently at in the current song. * * @return - The current song position in seconds, or -1 if no song is playing. */ - float getCurrentTime(); - + QueryFuture getCurrentTime(); + /** * Gets the length of the current song. * * @return The song length, in seconds. */ - float getLength(); + QueryFuture getLength(); + + /** + * Closes the player. + * @return A confirmation of whether the player was successfully closed. + */ + QueryFuture close(); } diff --git a/interface/src/main/java/edu/regis/universeplayer/browser/Browser.java b/interface/src/main/java/edu/regis/universeplayer/browser/Browser.java index 5e02fe4..eb9744a 100644 --- a/interface/src/main/java/edu/regis/universeplayer/browser/Browser.java +++ b/interface/src/main/java/edu/regis/universeplayer/browser/Browser.java @@ -4,11 +4,20 @@ package edu.regis.universeplayer.browser; +import edu.regis.universeplayer.Player; +import edu.regis.universeplayer.browserCommands.*; +import edu.regis.universeplayer.data.Song; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.io.IOException; +import java.io.*; +import java.net.ConnectException; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.LinkedList; +import java.util.Scanner; +import java.util.concurrent.*; /** * This serves as a central point for controlling the browser process. @@ -16,23 +25,88 @@ import java.io.IOException; * @author William Hubbard * @since 0.1 */ -public class Browser +public class Browser extends MessageRunner implements Player { - private static Logger logger = LoggerFactory.getLogger(Browser.class); + private static final Logger logger = LoggerFactory.getLogger(Browser.class); + private final Socket socket; + private final Process process; - private static Process process; + public static Browser createBrowser() throws IOException, InterruptedException + { + /* + * The maximum number of attempts that will be made to establish a + * connection. + */ + final int MAX_ATTEMPTS = 20; + /* + * How long the thread will sleep between connection attempts, in + * milliseconds. + */ + final long SLEEP_TIME = 500; + int startExit; + Socket socket = null; + Browser browser; + Process browserProcess = launchBrowser(); + /* + * Wait for the browser to fully start. + */ + startExit = browserProcess.waitFor(); + if (startExit != 0) + { + logger.error("Error in browser launch (exit code {})", startExit); + try (Scanner scanner = new Scanner(browserProcess.getErrorStream())) + { + while (scanner.hasNextLine()) + { + logger.error(scanner.nextLine()); + } + } + throw new IOException("Error in browser launch (exit code " + startExit + ")"); + } + logger.debug("Browser started."); + + ConnectException connErr = null; + logger.debug("Attempting connection"); + for (int attempts = 0; socket == null && attempts < MAX_ATTEMPTS; attempts++) + { + try + { + socket = new Socket(BrowserConstants.IP, BrowserConstants.PORT); + } + catch (ConnectException e) + { + connErr = e; + logger.debug("Connection attempt {} failed, trying again", attempts); + Thread.sleep(SLEEP_TIME); + } + } + if (socket == null) + { + throw connErr; + } + logger.debug("Browser connection established."); + browser = new Browser(socket, browserProcess); + return browser; + } + + private Browser(Socket socket, Process process) throws IOException + { + super("BrowserRunner", socket.getInputStream(), socket.getOutputStream()); + this.socket = socket; + this.process = process; + } /** * Utility method for launching a browser instance * * @throws IOException - Thrown if there is a problem launching the browser. */ - public static void launchBrowser() throws IOException + private static Process launchBrowser() throws IOException { + Process process = null; String os = System.getProperty("os.name").toLowerCase(); String arch = System.getProperty("os.arch").toLowerCase(); String args; - int startExit; File browserDir = new File(System.getProperty("user.dir"), "browser"); if (!browserDir.exists()) @@ -46,12 +120,12 @@ public class Browser { if (arch.contains("64")) { - logger.info("Starting Windows x86_64 browser"); + logger.debug("Starting Windows x86_64 browser"); process = Runtime.getRuntime().exec(new File(browserDir, "windows64/firefox.exe").getAbsolutePath() + args); } else { - logger.info("Starting Windows x86 browser"); + logger.debug("Starting Windows x86 browser"); process = Runtime.getRuntime().exec(new File(browserDir, "windows32/firefox.exe").getAbsolutePath() + args); } } @@ -59,12 +133,12 @@ public class Browser { if (arch.contains("64")) { - logger.info("Starting Linux x86_64 browser"); + logger.debug("Starting Linux x86_64 browser"); process = Runtime.getRuntime().exec(new File(browserDir, "linux64/firefox").getAbsolutePath() + args); } else { - logger.info("Starting Linux 86 browser"); + logger.debug("Starting Linux 86 browser"); process = Runtime.getRuntime().exec(new File(browserDir, "linux32/firefox").getAbsolutePath() + args); } } @@ -72,12 +146,35 @@ public class Browser { throw new IOException("Could not find Firefox installation for OS " + os + " " + arch); } + + return process; + } + + @Override + public Song getCurrentSong() + { + return null; + } + + @Override + public QueryFuture loadSong(InternetSong song) + { + try + { + return new ForwardedFuture(this.sendObject(new CommandLoadSong(song.location))); + } + catch (IOException e) + { + logger.error("Could not send message", e); + return null; + } } /** * Tells the browser process to shut down. */ - public static void closeBrowser() + @Override + public QueryFuture close() { if (process != null) { @@ -89,5 +186,118 @@ public class Browser { logger.info("Process already destroyed."); } + try + { + this.socket.close(); + } + catch (IOException e) + { + logger.error("Could not close browser socket", e); + } + return null; + } + + @Override + public QueryFuture play() + { + return null; + } + + @Override + public QueryFuture pause() + { + return null; + } + + @Override + public QueryFuture togglePlayback() + { + return null; + } + + @Override + public QueryFuture seek(float time) + { + return null; + } + + @Override + public QueryFuture isPaused() + { + return null; + } + + @Override + public QueryFuture getCurrentTime() + { + return null; + } + + @Override + public QueryFuture getLength() + { + return null; + } + + private class ForwardedFuture implements QueryFuture + { + private final Future future; + + ForwardedFuture(Future future) + { + this.future = future; + } + + private CommandReturn getVal() throws ExecutionException, InterruptedException + { + return ((CommandReturn) this.future.get()); + } + + private CommandReturn getVal(long timeout, TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException + { + return ((CommandReturn) this.future.get(timeout, unit)); + } + + @Override + public CommandConfirmation getConfirmation() throws CancellationException, ExecutionException, InterruptedException + { + return this.getVal().getConfirmation(); + } + + @Override + public CommandConfirmation getConfirmation(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException + { + return this.getVal(timeout, unit).getConfirmation(); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) + { + return this.future.cancel(mayInterruptIfRunning); + } + + @Override + public boolean isCancelled() + { + return this.future.isCancelled(); + } + + @Override + public boolean isDone() + { + return this.future.isDone(); + } + + @Override + public T get() throws InterruptedException, ExecutionException + { + return this.getVal().getReturnValue(); + } + + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException + { + return this.getVal(timeout, unit).getReturnValue(); + } } } diff --git a/interface/src/main/java/edu/regis/universeplayer/player/Interface.java b/interface/src/main/java/edu/regis/universeplayer/player/Interface.java index 866158b..5642a4a 100644 --- a/interface/src/main/java/edu/regis/universeplayer/player/Interface.java +++ b/interface/src/main/java/edu/regis/universeplayer/player/Interface.java @@ -11,12 +11,16 @@ import java.awt.event.ComponentListener; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedList; +import java.util.concurrent.Future; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JScrollPane; +import edu.regis.universeplayer.Player; import edu.regis.universeplayer.browser.Browser; import edu.regis.universeplayer.browser.MessageManager; import edu.regis.universeplayer.data.CollectionType; @@ -54,43 +58,65 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL /** * A link to the browser. */ - private MessageManager browser; + private ArrayList players = new ArrayList<>(); + private int currentPlayer = -1; public static void main(String[] args) { - /* - * Add this just in case of a crash or something. It won't work if the - * program is forcible terminated by the OS, but it could be helpful - * otherwise. - */ - Runtime.getRuntime().addShutdownHook(new Thread(() -> { - Browser.closeBrowser(); - })); - logger.info("Starting application"); - Interface inter = new Interface(); + Thread browserThread; + Interface inter = null; + Browser browser; try { - inter.browser = new MessageManager(); + /* + * Add this just in case of a crash or something. It won't work if the + * program is forcible terminated by the OS, but it could be helpful + * otherwise. + */ + logger.info("Starting application"); + inter = new Interface(); + inter.pack(); + inter.setVisible(true); + + try + { + inter.players.add(browser = Browser.createBrowser()); + browserThread = new Thread(browser); + browserThread.start(); + Runtime.getRuntime().addShutdownHook(new Thread(browser::close)); + + LinkedList> pingRequests = new LinkedList<>(); + for (int i = 0; i < 20; i++) + { + logger.info("Sending ping"); + pingRequests.add(browser.sendObject("ping")); + } + + LinkedList> toRemove = new LinkedList<>(); + while (pingRequests.size() > 0) + { + for (Future future: pingRequests) + { + if (future.isDone()) + { + logger.info("Receiving {}", future.get()); + toRemove.add(future); + } + } + pingRequests.removeAll(toRemove); + toRemove.clear(); + } + } + catch (IOException e) + { + logger.error("Could not open browser background", e); + JOptionPane.showMessageDialog(inter, e, "Could not open browser background", JOptionPane.ERROR_MESSAGE); + } } - catch (IOException e) - { - logger.error("Could not open browser communication", e); - JOptionPane.showMessageDialog(null, e, "Could not open browser communication", JOptionPane.ERROR_MESSAGE); - } - inter.pack(); - inter.setVisible(true); - - /* - * Launch the browser in the background. - */ - try - { - Browser.launchBrowser(); - } - catch (IOException e) + catch (Throwable e) { logger.error("Could not open browser background", e); - JOptionPane.showMessageDialog(inter, e, "Could not open browser background", JOptionPane.ERROR_MESSAGE); + JOptionPane.showMessageDialog(inter != null && inter.isVisible() ? inter : null, e, "Error!", JOptionPane.ERROR_MESSAGE); } } @@ -191,18 +217,7 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL @Override public void windowClosing(WindowEvent windowEvent) { - Browser.closeBrowser(); - if (this.browser != null) - { - try - { - this.browser.close(); - } - catch (IOException e) - { - logger.error("Could not close browser", e); - } - } + this.players.forEach(Player::close); } @Override @@ -243,29 +258,31 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL @Override public void onCommand(PlaybackCommand command, Object data) { - Object message = null; - if (this.browser != null) + Player player; + if (this.currentPlayer >= 0 && this.currentPlayer < this.players.size()) { - synchronized (this.browser) + player = this.players.get(this.currentPlayer); + } + else + { + throw new NullPointerException("No player available"); + } + switch (command) + { + case PLAY -> { + if (data instanceof Song) { - try - { - this.browser.ping(); - message = this.browser.getMessage(); - } - catch (IOException e) - { - logger.error("Could not send message to browser", e); - JOptionPane.showMessageDialog(this, e, "Could not send message to browser", JOptionPane.ERROR_MESSAGE); - } + player.loadSong((Song) data); } } - if (message != null) - { - if ("ping".equals(message)) - { - JOptionPane.showMessageDialog(this, "Ping received!"); - } + case PAUSE -> { + } + case NEXT -> { + } + case PREVIOUS -> { + } + case SEEK -> { + } } } } \ No newline at end of file diff --git a/interface/src/main/resources/log4j2.xml b/interface/src/main/resources/log4j2.xml index de7c6a5..f67fe99 100644 --- a/interface/src/main/resources/log4j2.xml +++ b/interface/src/main/resources/log4j2.xml @@ -9,7 +9,7 @@ - %d{HH:mm:ss.SSS} - %45c:%-4L - %-5level - %msg%n + %d{HH:mm:ss.SSS} - %c.%M(%F:%L) - %-5level - %msg%n @@ -17,8 +17,5 @@ - - - \ No newline at end of file