Established a connection between the Interface and the browser.

All we're doing right now is pinging, but it should be fairly simple to get an API working now that the browser and interface can talk.
This commit is contained in:
Markil3
2021-07-29 11:37:07 -07:00
parent 1321549ef3
commit 105c89b615
25 changed files with 1236 additions and 118 deletions

View File

@@ -27,15 +27,15 @@ import java.util.HashMap;
*/ */
public class BrowserLink extends MessageRunner 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(); public static final Gson gson = new Gson();
/** /**
* Creates a message runner. * Creates a message runner.
*/ */
public BrowserLink() public BrowserLink(String name)
{ {
super(System.in, System.out); super(name, System.in, System.out);
} }
@Override @Override
@@ -64,7 +64,7 @@ public class BrowserLink extends MessageRunner
ByteBuffer lengthBuffer = ByteBuffer.allocate(4); ByteBuffer lengthBuffer = ByteBuffer.allocate(4);
lengthBuffer.order(ByteOrder.nativeOrder()); lengthBuffer.order(ByteOrder.nativeOrder());
logger.debug("Writing message {} {}", messageNum, messageString); getLogger().debug("Writing message {} {}", messageNum, messageString);
/* /*
* Writes the message length * Writes the message length
*/ */
@@ -92,7 +92,7 @@ public class BrowserLink extends MessageRunner
readLength = in.read(lengthBuffer.array()); readLength = in.read(lengthBuffer.array());
if (readLength == 0) if (readLength == 0)
{ {
logger.debug("Input stream closed, no more messages."); getLogger().debug("Input stream closed, no more messages.");
return null; return null;
} }
messageLength = lengthBuffer.getInt(); messageLength = lengthBuffer.getInt();
@@ -101,7 +101,7 @@ public class BrowserLink extends MessageRunner
readLength = in.read(message); readLength = in.read(message);
if (readLength < message.length) 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); messageJson = gson.fromJson(new String(message, StandardCharsets.UTF_8), JsonObject.class);
messageNum = messageJson.get("messageNum").getAsInt(); messageNum = messageJson.get("messageNum").getAsInt();
@@ -115,7 +115,7 @@ public class BrowserLink extends MessageRunner
lengthBuffer.put(new byte[4]); lengthBuffer.put(new byte[4]);
lengthBuffer.clear(); lengthBuffer.clear();
lengthBuffer.putInt(messageNum); lengthBuffer.putInt(messageNum);
logger.debug("Reading message {}", messageNum); getLogger().debug("Reading message {}", messageNum);
return new byte[][] {lengthBuffer.array(), message}; return new byte[][] {lengthBuffer.array(), message};
} }
@@ -200,7 +200,7 @@ public class BrowserLink extends MessageRunner
} }
catch (ClassNotFoundException | ClassCastException e) 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) 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; return type;

View File

@@ -4,10 +4,14 @@
package edu.regis.universeplayer.addon; package edu.regis.universeplayer.addon;
import edu.regis.universeplayer.browserCommands.BrowserConstants;
import edu.regis.universeplayer.browserCommands.MessageHandler;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future; import java.util.concurrent.Future;
@@ -16,10 +20,91 @@ 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 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<Future<Object>> requests = new LinkedList<>(); LinkedList<Future<Object>> requests = new LinkedList<>();
BrowserLink link = new BrowserLink(); BrowserLink link = new BrowserLink("BrowserLink");
Thread linkThread = new Thread(link); Thread linkThread = new Thread(link);
linkThread.start(); linkThread.start();
logger.info("Sending message"); logger.info("Sending message");

View File

@@ -9,7 +9,7 @@
</Console> </Console>
<File name="File" fileName="addonInter.log" append="false"> <File name="File" fileName="addonInter.log" append="false">
<PatternLayout> <PatternLayout>
<Pattern>%d{HH:mm:ss.SSS} - %45c:%-4L - %-5level - %msg%n</Pattern> <Pattern>%d{HH:mm:ss.SSS} - %c.%M(%F:%L) - %-5level - %msg%n</Pattern>
</PatternLayout> </PatternLayout>
</File> </File>
</Appenders> </Appenders>
@@ -17,5 +17,6 @@
<Root level="debug"> <Root level="debug">
<AppenderRef ref="File"/> <AppenderRef ref="File"/>
</Root> </Root>
<Logger name="edu.regis.universeplayer.browserCommands.MessageRunner" level="trace"/>
</Loggers> </Loggers>
</Configuration> </Configuration>

View File

@@ -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<Void>
{
@Override
default Class<Void> getReturnType()
{
return Void.class;
}
}

View File

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

View File

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

View File

@@ -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<T> 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<T> getReturnType();
}

View File

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

View File

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

View File

@@ -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 <T> The type of return value.
* @author William Hubbard
* @version 0.1
*/
public class CommandReturn<T> 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;
}
}

View File

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

View File

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

View File

@@ -13,6 +13,7 @@ import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
@@ -22,7 +23,9 @@ import java.util.concurrent.Future;
*/ */
public class MessageHandler implements Runnable, MessageSerializer 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 InputStream input;
private final OutputStream output; private final OutputStream output;
@@ -34,16 +37,25 @@ public class MessageHandler implements Runnable, MessageSerializer
/** /**
* Creates a message handler. * 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 input - The input from our external source.
* @param output - The output to the 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.input = input;
this.output = output; this.output = output;
this.executor = Executors.newCachedThreadPool(); this.executor = Executors.newCachedThreadPool();
} }
@Override
public Logger getLogger()
{
return logger;
}
@Override @Override
public void run() public void run()
{ {
@@ -84,6 +96,7 @@ public class MessageHandler implements Runnable, MessageSerializer
messageResponse = this.triggerListeners(messageOb); messageResponse = this.triggerListeners(messageOb);
synchronized (this.messageResponses) synchronized (this.messageResponses)
{ {
numBuffer.clear();
this.messageResponses.put(numBuffer.getInt(), messageResponse); this.messageResponses.put(numBuffer.getInt(), messageResponse);
} }
} }
@@ -108,7 +121,7 @@ public class MessageHandler implements Runnable, MessageSerializer
if (responses.getValue().isDone()) if (responses.getValue().isDone())
{ {
toRemove.add(responses.getKey()); toRemove.add(responses.getKey());
messageByte = serializeObject(responses.getValue()); messageByte = serializeObject(responses.getValue().get());
writeMessage(browserOut, responses.getKey(), messageByte); 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 finally
{ {
/* /*
@@ -255,6 +272,6 @@ public class MessageHandler implements Runnable, MessageSerializer
* have generated return values. * have generated return values.
* @return The value to be returned to the remote. * @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;
} }
} }

View File

@@ -22,8 +22,9 @@ import java.util.concurrent.TimeoutException;
*/ */
public abstract class MessageRunner implements Runnable, MessageSerializer 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 InputStream input;
private final OutputStream output; private final OutputStream output;
private final Object readLock = new Object(); private final Object readLock = new Object();
@@ -41,15 +42,24 @@ public abstract class MessageRunner implements Runnable, MessageSerializer
/** /**
* Creates a message runner. * 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 input - The input from our external source.
* @param output - The output to the 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.input = input;
this.output = output; this.output = output;
} }
@Override
public Logger getLogger()
{
return logger;
}
@Override @Override
public void run() public void run()
{ {
@@ -91,7 +101,7 @@ public abstract class MessageRunner implements Runnable, MessageSerializer
} }
catch (IOException e) 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 finally
{ {
/* /*

View File

@@ -5,14 +5,13 @@
package edu.regis.universeplayer.browserCommands; package edu.regis.universeplayer.browserCommands;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*; import java.io.*;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
public interface MessageSerializer public interface MessageSerializer
{ {
Logger logger = LoggerFactory.getLogger(MessageSerializer.class); Logger getLogger();
/** /**
* Converts an object into a form that can be sent. * 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 default void writeMessage(OutputStream out, int messageNum, byte[] message) throws IOException
{ {
ByteBuffer lengthBuffer = ByteBuffer.allocate(4); ByteBuffer lengthBuffer = ByteBuffer.allocate(4);
logger.debug("Writing message {} {}", messageNum, message); getLogger().trace("Writing message {} {}", messageNum, message);
/* /*
* Writes the message number. * Writes the message number.
*/ */
@@ -110,7 +109,7 @@ public interface MessageSerializer
readLength = in.read(messageNum); readLength = in.read(messageNum);
if (readLength == 0) if (readLength == 0)
{ {
logger.debug("Input stream closed, no more messages."); getLogger().debug("Input stream closed, no more messages.");
return null; return null;
} }
/* /*
@@ -120,7 +119,7 @@ public interface MessageSerializer
readLength = in.read(lengthBuffer.array()); readLength = in.read(lengthBuffer.array());
if (readLength == 0) if (readLength == 0)
{ {
logger.error("Malformed message, could not get message length."); getLogger().error("Malformed message, could not get message length.");
return null; return null;
} }
message = new byte[lengthBuffer.getInt()]; message = new byte[lengthBuffer.getInt()];
@@ -130,9 +129,9 @@ public interface MessageSerializer
readLength = in.read(message); readLength = in.read(message);
if (readLength < message.length) 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}; return new byte[][] {messageNum, message};
} }
} }

View File

@@ -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 <V> The return type.
*/
public interface QueryFuture<V> extends Future<V>
{
/**
* 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;
}

View File

@@ -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<Float>
{
@Override
public String getCommandName()
{
return "length";
}
@Override
public Class<Float> getReturnType()
{
return Float.class;
}
}

View File

@@ -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<Boolean>
{
@Override
public String getCommandName()
{
return "isPaused";
}
@Override
public Class<Boolean> getReturnType()
{
return Boolean.class;
}
}

View File

@@ -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<Boolean>
{
@Override
public String getCommandName()
{
return "isPlaying";
}
@Override
public Class<Boolean> getReturnType()
{
return Boolean.class;
}
}

View File

@@ -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<URL>
{
@Override
public String getCommandName()
{
return "getSong";
}
@Override
public Class<URL> getReturnType()
{
return URL.class;
}
}

View File

@@ -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<Float>
{
@Override
public String getCommandName()
{
return "currentTime";
}
@Override
public Class<Float> getReturnType()
{
return Float.class;
}
}

View File

@@ -4,8 +4,11 @@
package edu.regis.universeplayer; package edu.regis.universeplayer;
import edu.regis.universeplayer.browserCommands.QueryFuture;
import edu.regis.universeplayer.data.Song; 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 * This interface serves as the connection to a music player of some sort, whether it be
* browser-based or from a file. * browser-based or from a file.
@@ -27,49 +30,60 @@ public interface Player<T extends Song>
* Loads up a song * Loads up a song
* *
* @param song - The song to load. * @param song - The song to load.
* @return A confirmation of whether the command was successful or not.
*/ */
void play(T song); QueryFuture<Void> loadSong(T song);
/** /**
* Enables playback of the current song, if one is active. * Enables playback of the current song, if one is active.
* @return A confirmation of whether the command was successful or not.
*/ */
void play(); QueryFuture<Void> play();
/** /**
* Pauses playback of the current song. * Pauses playback of the current song.
* @return A confirmation of whether the command was successful or not.
*/ */
void pause(); QueryFuture<Void> pause();
/** /**
* Toggles between playing and pausing the current song. * Toggles between playing and pausing the current song.
* @return A confirmation of whether the command was successful or not.
*/ */
void togglePlayback(); QueryFuture<Void> togglePlayback();
/** /**
* Sets the current song time to the specified position. * Sets the current song time to the specified position.
* *
* @param time - The specified time in the song, in seconds. * @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<Void> seek(float time);
/** /**
* Checks to see if the song is paused. * Checks to see if the song is paused.
* *
* @return Whether or not the song is paused. * @return Whether or not the song is paused.
*/ */
boolean isPaused(); QueryFuture<Boolean> isPaused();
/** /**
* Obtains the time we are currently at in the current song. * 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. * @return - The current song position in seconds, or -1 if no song is playing.
*/ */
float getCurrentTime(); QueryFuture<Float> getCurrentTime();
/** /**
* Gets the length of the current song. * Gets the length of the current song.
* *
* @return The song length, in seconds. * @return The song length, in seconds.
*/ */
float getLength(); QueryFuture<Float> getLength();
/**
* Closes the player.
* @return A confirmation of whether the player was successfully closed.
*/
QueryFuture<Void> close();
} }

View File

@@ -4,11 +4,20 @@
package edu.regis.universeplayer.browser; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.File; import java.io.*;
import java.io.IOException; 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. * This serves as a central point for controlling the browser process.
@@ -16,23 +25,88 @@ import java.io.IOException;
* @author William Hubbard * @author William Hubbard
* @since 0.1 * @since 0.1
*/ */
public class Browser public class Browser extends MessageRunner implements Player<InternetSong>
{ {
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 * Utility method for launching a browser instance
* *
* @throws IOException - Thrown if there is a problem launching the browser. * @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 os = System.getProperty("os.name").toLowerCase();
String arch = System.getProperty("os.arch").toLowerCase(); String arch = System.getProperty("os.arch").toLowerCase();
String args; String args;
int startExit;
File browserDir = new File(System.getProperty("user.dir"), "browser"); File browserDir = new File(System.getProperty("user.dir"), "browser");
if (!browserDir.exists()) if (!browserDir.exists())
@@ -46,12 +120,12 @@ public class Browser
{ {
if (arch.contains("64")) 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); process = Runtime.getRuntime().exec(new File(browserDir, "windows64/firefox.exe").getAbsolutePath() + args);
} }
else 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); process = Runtime.getRuntime().exec(new File(browserDir, "windows32/firefox.exe").getAbsolutePath() + args);
} }
} }
@@ -59,12 +133,12 @@ public class Browser
{ {
if (arch.contains("64")) 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); process = Runtime.getRuntime().exec(new File(browserDir, "linux64/firefox").getAbsolutePath() + args);
} }
else else
{ {
logger.info("Starting Linux 86 browser"); logger.debug("Starting Linux 86 browser");
process = Runtime.getRuntime().exec(new File(browserDir, "linux32/firefox").getAbsolutePath() + args); 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); throw new IOException("Could not find Firefox installation for OS " + os + " " + arch);
} }
return process;
}
@Override
public Song getCurrentSong()
{
return null;
}
@Override
public QueryFuture<Void> loadSong(InternetSong song)
{
try
{
return new ForwardedFuture<Void>(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. * Tells the browser process to shut down.
*/ */
public static void closeBrowser() @Override
public QueryFuture<Void> close()
{ {
if (process != null) if (process != null)
{ {
@@ -89,5 +186,118 @@ public class Browser
{ {
logger.info("Process already destroyed."); 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<Void> play()
{
return null;
}
@Override
public QueryFuture<Void> pause()
{
return null;
}
@Override
public QueryFuture<Void> togglePlayback()
{
return null;
}
@Override
public QueryFuture<Void> seek(float time)
{
return null;
}
@Override
public QueryFuture<Boolean> isPaused()
{
return null;
}
@Override
public QueryFuture<Float> getCurrentTime()
{
return null;
}
@Override
public QueryFuture<Float> getLength()
{
return null;
}
private class ForwardedFuture<T> implements QueryFuture<T>
{
private final Future future;
ForwardedFuture(Future future)
{
this.future = future;
}
private CommandReturn<T> getVal() throws ExecutionException, InterruptedException
{
return ((CommandReturn<T>) this.future.get());
}
private CommandReturn<T> getVal(long timeout, TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException
{
return ((CommandReturn<T>) 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();
}
} }
} }

View File

@@ -11,12 +11,16 @@ import java.awt.event.ComponentListener;
import java.awt.event.WindowEvent; import java.awt.event.WindowEvent;
import java.awt.event.WindowListener; import java.awt.event.WindowListener;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedList;
import java.util.concurrent.Future;
import javax.swing.JFrame; import javax.swing.JFrame;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
import javax.swing.JScrollPane; import javax.swing.JScrollPane;
import edu.regis.universeplayer.Player;
import edu.regis.universeplayer.browser.Browser; import edu.regis.universeplayer.browser.Browser;
import edu.regis.universeplayer.browser.MessageManager; import edu.regis.universeplayer.browser.MessageManager;
import edu.regis.universeplayer.data.CollectionType; import edu.regis.universeplayer.data.CollectionType;
@@ -54,43 +58,65 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
/** /**
* A link to the browser. * A link to the browser.
*/ */
private MessageManager browser; private ArrayList<Player> players = new ArrayList<>();
private int currentPlayer = -1;
public static void main(String[] args) public static void main(String[] args)
{ {
/* Thread browserThread;
* Add this just in case of a crash or something. It won't work if the Interface inter = null;
* program is forcible terminated by the OS, but it could be helpful Browser browser;
* otherwise.
*/
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
Browser.closeBrowser();
}));
logger.info("Starting application");
Interface inter = new Interface();
try try
{ {
inter.browser = new MessageManager(); /*
} * Add this just in case of a crash or something. It won't work if the
catch (IOException e) * program is forcible terminated by the OS, but it could be helpful
{ * otherwise.
logger.error("Could not open browser communication", e); */
JOptionPane.showMessageDialog(null, e, "Could not open browser communication", JOptionPane.ERROR_MESSAGE); logger.info("Starting application");
} inter = new Interface();
inter.pack(); inter.pack();
inter.setVisible(true); inter.setVisible(true);
/* try
* Launch the browser in the background. {
*/ inter.players.add(browser = Browser.createBrowser());
try browserThread = new Thread(browser);
{ browserThread.start();
Browser.launchBrowser(); Runtime.getRuntime().addShutdownHook(new Thread(browser::close));
LinkedList<Future<Object>> pingRequests = new LinkedList<>();
for (int i = 0; i < 20; i++)
{
logger.info("Sending ping");
pingRequests.add(browser.sendObject("ping"));
}
LinkedList<Future<Object>> toRemove = new LinkedList<>();
while (pingRequests.size() > 0)
{
for (Future<Object> 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) catch (Throwable e)
{ {
logger.error("Could not open browser background", 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 @Override
public void windowClosing(WindowEvent windowEvent) public void windowClosing(WindowEvent windowEvent)
{ {
Browser.closeBrowser(); this.players.forEach(Player::close);
if (this.browser != null)
{
try
{
this.browser.close();
}
catch (IOException e)
{
logger.error("Could not close browser", e);
}
}
} }
@Override @Override
@@ -243,29 +258,31 @@ public class Interface extends JFrame implements SongDisplayListener, ComponentL
@Override @Override
public void onCommand(PlaybackCommand command, Object data) public void onCommand(PlaybackCommand command, Object data)
{ {
Object message = null; Player player;
if (this.browser != null) 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 player.loadSong((Song) data);
{
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);
}
} }
} }
if (message != null) case PAUSE -> {
{ }
if ("ping".equals(message)) case NEXT -> {
{ }
JOptionPane.showMessageDialog(this, "Ping received!"); case PREVIOUS -> {
} }
case SEEK -> {
}
} }
} }
} }

View File

@@ -9,7 +9,7 @@
</Console> </Console>
<File name="File" fileName="interface.log" append="false"> <File name="File" fileName="interface.log" append="false">
<PatternLayout> <PatternLayout>
<Pattern>%d{HH:mm:ss.SSS} - %45c:%-4L - %-5level - %msg%n</Pattern> <Pattern>%d{HH:mm:ss.SSS} - %c.%M(%F:%L) - %-5level - %msg%n</Pattern>
</PatternLayout> </PatternLayout>
</File> </File>
</Appenders> </Appenders>
@@ -17,8 +17,5 @@
<Root level="debug"> <Root level="debug">
<AppenderRef ref="File"/> <AppenderRef ref="File"/>
</Root> </Root>
<Logger name="edu.regis.universeplayer.browser.Browser" level="info">
<AppenderRef ref="Console"/>
</Logger>
</Loggers> </Loggers>
</Configuration> </Configuration>