package edu.colostate.vchill.proxy;
import edu.colostate.vchill.*;
import edu.colostate.vchill.ChillDefines.Channel;
import edu.colostate.vchill.cache.CacheMain;
import edu.colostate.vchill.chill.*;
import edu.colostate.vchill.socket.SocketArchCtl;
import edu.colostate.vchill.socket.SocketResponse;
import java.io.*;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
/**
* Thread that deals with the control socket.
*
* @author Alexander Deyke
* @author Jochen Deyke
* @author jpont
* @version 2009-06-01
*/
class ProxyControlThread implements Runnable, Control {
/**
* initial type definition scale information is stored here
*/
private static final ScaleManager sm = ScaleManager.getInstance();
/**
* name:port this object is connected to
*/
private String url;
/**
* control connection to client
*/
private Socket clientControlSocket;
private DataInputStream clientControlIn;
private DataOutputStream clientControlOut;
/**
* control connection to server
*/
private Socket serverControlSocket;
private DataInputStream serverControlIn;
private DataOutputStream serverControlOut;
/**
* session ID; used to match data channels with control channel
*/
private int sessionID;
/**
* reuseable object to represent the server's response to commands
*/
private SocketResponse response;
/**
* reuseable object to represent the client's command
*/
private SocketArchCtl command;
/**
* is the connection up?
*/
private boolean connected;
/**
* timeout for headers on data channel
*/
private final static int timeout = 5000; // in millis
/**
* list of ProxyDataThread associated with this ProxyControlThread
*/
private ArrayList<ProxyDataThread> clientDataThreads;
/**
* number of data sockets to server necessary to transfer all data
*/
private int serverDataSockets;
/**
* arrays of data sockets to server
*/
private Socket[] serverDataSocket;
private DataInputStream[] serverDataIn;
private DataOutputStream[] serverDataOut;
private long[] dataType;
/**
* time last packet was sent or received on this channel
*/
private long lastPacketTime;
/**
* shared cache
*/
private CacheMain cache;
/**
* parent Proxy object, get configuration info from here
*/
private Proxy parent;
/**
* thread object; needed for isAlive
*/
private Thread thread;
public ProxyControlThread(final Socket clientControlSocket, final Proxy parent) throws IOException {
this.parent = parent;
this.serverDataSockets = (ChillFieldInfo.types.length - (this.parent.getCalcFlag() ? 4 : 0) - 1) / ChillDefines.MAX_PER_CHANNEL + 1;
this.clientControlSocket = clientControlSocket;
this.clientDataThreads = new ArrayList<ProxyDataThread>();
this.serverDataSocket = new Socket[serverDataSockets];
this.serverDataIn = new DataInputStream[serverDataSockets];
this.serverDataOut = new DataOutputStream[serverDataSockets];
this.dataType = new long[serverDataSockets];
// Assign data types to the three sockets
for (int i = 0; i < ChillFieldInfo.types.length; ++i) {
// If we're calculating hybrid types, don't retrieve them
ChillFieldInfo type = ChillFieldInfo.types[i];
if (this.parent.getCalcFlag() && type.fieldNumber > ChillFieldInfo.CALC_CUTOFF) continue;
this.dataType[i / ChillDefines.MAX_PER_CHANNEL] |= 1l << type.fieldNumber;
}
this.clientControlIn = new DataInputStream(new BufferedInputStream(clientControlSocket.getInputStream()));
this.clientControlOut = new DataOutputStream(clientControlSocket.getOutputStream());
this.cache = this.parent.getCache();
this.thread = new Thread(this, "ProxyControlThread");
this.sessionID = -1;
}
/**
* Causes this thread to begin execution; the Java Virtual Machine calls the <code>run</code> method of this thread.
*/
public void start() {
this.thread.start();
}
/**
* Tests if this thread is alive. A thread is alive if it has been started and has not yet died.
*
* @return <code>true</code> if this thread is alive; <code>false</code> otherwise.
*/
public boolean isAlive() {
return this.thread.isAlive();
}
/**
* Connects to given server and port number
*
* @param server the name of the server to connect to
* @param port the port number to connect to
* @throws IOException if the connection fails
*/
public synchronized void connect(final String server, final int port) throws IOException {
this.url = server + ":" + port;
System.out.println("ProxyControlThread: Connecting to " + this.url);
this.serverControlSocket = new Socket(server, port);
this.serverControlSocket.setSoTimeout(25000); // timeout on control while connecting (ms)
this.serverControlIn = new DataInputStream(new BufferedInputStream(this.serverControlSocket.getInputStream()));
this.serverControlOut = new DataOutputStream(new BufferedOutputStream(this.serverControlSocket.getOutputStream()));
boolean accepted = false;
do {
System.out.println("ProxyControlThread: Getting and sending login and password.");
if (!connected) {
serverControlOut.writeInt(ChillDefines.HELLO);
serverControlOut.writeInt(ChillDefines.Channel.ARCH_CTL.ordinal());
serverControlOut.flush();
}
this.readCommand(); // Getting login and password from client
this.passCommand();
System.out.println("ProxyControlThread: Getting response to login and password.");
this.readAndPassResponse(); // Getting response
this.connected = true; // mark as connected so correct packet gets sent on retry
switch (this.response.getStatus()) {
case SERVER_READY:
System.out.println("ProxyControlThread: Server ready.");
accepted = true;
break;
case SIGNON_UNKNOWN:
System.err.println("ProxyControlThread: Bad login.");
continue;
case PASSWD_FAIL:
System.err.println("ProxyControlThread: Bad password.");
continue;
default: // unknown response
throw new Error("ProxyControlThread: Don't know how to handle:\n" + this.response);
}
} while (!accepted);
this.sessionID = this.response.getExtStatus();
this.serverControlSocket.setSoTimeout(0); // no timeout on control
for (int i = 0; i < serverDataSockets; ++i) {
this.serverDataSocket[i] = new Socket(server, port);
this.serverDataSocket[i].setSoTimeout(0); // no default timeout on data
this.serverDataIn[i] = new DataInputStream(new BufferedInputStream(this.serverDataSocket[i].getInputStream()));
this.serverDataOut[i] = new DataOutputStream(new BufferedOutputStream(this.serverDataSocket[i].getOutputStream()));
this.sendDataHandshake(i);
}
System.out.println("ProxyControlThread: Listening to commands.");
this.listenCommands();
}
/**
* Check connection status
*
* @return true if the connection is active and ready, false if it is not
*/
public synchronized boolean connected() {
if (this.connected == false) return false;
if (!(this.clientControlSocket.isConnected() && this.serverControlSocket.isConnected())) {
try {
System.out.println("ProxyControlThread: Socket: socket timed out, disconnecting");
this.disconnect();
} catch (IOException ioe) {
throw new Error("Socket PANIC!! - IOException while trying to disconnect: " + ioe);
}
}
return this.connected;
}
/**
* Listen to commands from the client.
*/
public synchronized void listenCommands() throws IOException {
while (true) {
while (this.connected() && clientControlIn.available() < SocketArchCtl.BYTE_SIZE) {
if (this.parent.getTimeout() != 0 && System.currentTimeMillis() - lastPacketTime >= this.parent.getTimeout()) {
System.out.println("ProxyControlThread: Idle too long. Disconnecting.");
disconnect();
} else {
Proxy.sleep(100);
}
}
if (!this.connected()) break;
this.readCommand();
switch (this.command.getArchMode()) {
case DIRECTORY_REQ:
getDirectory();
break; //dirs, files, bookmarks, contents
case STATUS_REQ:
getStatus();
break;
case DISCONNECT_REQ:
disconnect();
break;
case HALT_COMMAND:
halt();
break;
case SWEEP_MODE:
getSweep();
break;
default:
System.err.println("ProxyControlThread: Invalid command packet:\n" + this.command);
}
}
}
/**
* Retrieves a sweep from the server. All data types are requested
* or calculated and put into the cache. Then the requested ones can
* be passed on by the ProxyDataThread.
*
* @throws IOException if an error occurs when communicating with the server
*/
@SuppressWarnings("unchecked")
public synchronized void getSweep() throws IOException {
System.out.println("ProxyControlThread: Got Sweep Mode request");
ChillHSKHeader[] hskHs = new ChillHSKHeader[serverDataSockets];
ChillDataHeader[] dataHs = new ChillDataHeader[serverDataSockets];
ArrayList<String>[] types = new ArrayList[serverDataSockets];
String filename = command.getInFile();
String directory = filename.substring(0, filename.lastIndexOf("/"));
String file = filename.substring(filename.lastIndexOf("/") + 1, filename.length());
// Set rayCommand to metadata since that's where we're storing headers
//ControlMessage rayCommand = new ControlMessage(url, directory, file, "Sweep " + command.getStartSweep(), ChillDefines.META_TYPE);
ControlMessage rayCommand = new ControlMessage(url, directory, file, "Sweep " + command.getStartSweep());
//ControlMessage controlCommand = rayCommand.setType(ChillDefines.CONTROL_TYPE); //used for storing responses
//ControlMessage metaCommand = rayCommand; //rayCommand gets reused in for loops
boolean sentResponses = false;
if (cache.getCompleteFlag(rayCommand, ChillDefines.CONTROL_TYPE)) {
System.out.println("meta info complete");
this.response = (SocketResponse) cache.getData(rayCommand, ChillDefines.CONTROL_TYPE, 0);
System.out.println("ProxyControlThread: Sending start header.");
this.passResponse();
this.response = (SocketResponse) cache.getData(rayCommand, ChillDefines.CONTROL_TYPE, 1);
System.out.println("ProxyControlThread: Sending end header.");
this.passResponse();
sentResponses = true;
}
System.out.println("starting data threads");
for (ProxyDataThread thread : clientDataThreads) thread.send(rayCommand);
if (!sentResponses) { // Responses were only sent if everything was cached.
System.out.println("responses not yet sent");
ArrayList<String> allTypes = new ArrayList<String>();
for (ChillFieldInfo type : ChillFieldInfo.types) {
// If we're calculating hybrid data types, don't retrieve them
if (this.parent.getCalcFlag() && type.fieldNumber > ChillFieldInfo.CALC_CUTOFF) continue;
allTypes.add(type.fieldName);
}
ArrayList<String>[] availableTypes = new ArrayList[serverDataSockets]; // subset of this socket's subset of allTypes representing available data
System.out.println("ProxyControlThread: Not everything is cached.");
this.clearChannels();
this.passCommand();
this.readAndPassResponse();
// store initial response
System.out.println("adding initial response");
cache.addRay(rayCommand, ChillDefines.CONTROL_TYPE, this.response);
// do HDR, NCP+, etc. calculations
if (this.parent.getCalcFlag()) new ProxyCalculationThread(rayCommand, cache).start();
// read radar data into cache
int firstRayNumber = this.response.getRayNumber() - 1; // convert to 0-based
System.out.println("1st ray# = " + firstRayNumber);
int lastRayNumber = Integer.MAX_VALUE; // don't know when to stop yet
for (int currRayNumber = firstRayNumber; currRayNumber < lastRayNumber; ) { // each ray:
//System.out.println("loop executing");
if (this.clientControlIn.available() > 0) { // stop request received
System.out.println("in clientIn");
//Make it so we can revert the stream if the command isn't what we are looking for.
this.clientControlIn.mark(SocketArchCtl.BYTE_SIZE);
this.readCommand();
if (this.command.getArchMode() == SocketArchCtl.Command.HALT_COMMAND) {
System.out.println("ProxyControlThread: Attempting to stop...");
this.halt();
for (int i = 0; i < allTypes.size(); ++i) {
cache.removeType(rayCommand, allTypes.get(i)); // remove incomplete sweep
}
cache.removeType(rayCommand, ChillDefines.META_TYPE);
return;
} else {
//Reset the stream so that the command received will be
//handled properly later on.
this.clientControlIn.reset();
}
}
if (this.serverControlIn.available() > 0) { // notification: sweep done
System.out.println("in serverIn");
//System.out.println(this.response);
this.readAndPassResponse();
//System.out.println(this.response);
lastRayNumber = this.response.getRayNumber(); // we know when to stop now
System.out.println("last ray# = " + lastRayNumber);
}
try {
for (int i = 0; i < serverDataSockets; ++i) {
this.serverDataSocket[i].setSoTimeout(ProxyControlThread.timeout); // default timeout on data
ChillHeaderHeader headerH = new ChillHeaderHeader(serverDataIn[i]);
//System.out.println("header type = " + Integer.toHexString(headerH.recordType));
switch (headerH.recordType) {
case ChillDefines.GEN_MOM_DATA:
dataHs[i] = new ChillDataHeader(serverDataIn[i], headerH); // read header
if (i == 0)
cache.addRay(rayCommand, ChillDefines.META_TYPE, dataHs[i]); //only save one copy
if (availableTypes[i] == null) { // only initialize once
types[i] = dataHs[i].calculateTypes(); // determine which types are available
availableTypes[i] = new ArrayList<String>(types[i].size());
for (String type : types[i]) { // create a ControlMessage for each available type
availableTypes[i].add(type);
}
}
this.serverDataSocket[i].setSoTimeout(0); // no timeout on data
byte[][] data = new byte[availableTypes[i].size()][dataHs[i].numGates];
byte[] interleavedData = this.readBytes(availableTypes[i].size() * dataHs[i].numGates, serverDataIn[i]); // calculate size of and read data
for (int t = 0; t < availableTypes[i].size(); ++t) { // split apart different data types
if (availableTypes[i].get(t) == null) continue;
for (int g = 0; g < dataHs[i].numGates; ++g) {
data[t][g] = interleavedData[availableTypes[i].size() * g + t];
}
cache.addRay(rayCommand, availableTypes[i].get(t), new ChillGenRay(hskHs[i], dataHs[i], types[i].get(t), data[t])); //add data to cache
}
++currRayNumber; //increment ray number here so it doesn't go up on "continue"
//System.out.println("currRayNumber = " + currRayNumber);
break;
case ChillDefines.BRIEF_HSK_DATA:
hskHs[i] = new ChillHSKHeader(serverDataIn[i], headerH);
if (i == 0)
cache.addRay(rayCommand, ChillDefines.META_TYPE, hskHs[i]); //only save one copy
break;
case ChillDefines.FIELD_SCALE_DATA:
ChillMomentFieldScale scale = new ChillMomentFieldScale(serverDataIn[i], headerH);
if (i == 0)
cache.addRay(rayCommand, ChillDefines.META_TYPE, scale); //only save one copy
sm.putScale(scale);
break;
case ChillDefines.TRACK_DATA:
ChillTrackInfo track = new ChillTrackInfo(serverDataIn[i], headerH);
if (i == 0)
cache.addRay(rayCommand, ChillDefines.META_TYPE, track); //only save one copy
break;
default:
System.out.println("Don't know how to handle header of type " + headerH.recordType);
ChillHeader generic = new ChillHeader(headerH);
if (i == 0)
cache.addRay(rayCommand, ChillDefines.META_TYPE, generic); //only save one copy
break;
}
}
} catch (SocketTimeoutException stoe) {
System.err.println("ProxyControlThread: Read timed out getting sweep; retrying");
continue;
}
Proxy.sleep(5);
}
// Add in the calculated scales
if (this.parent.getCalcFlag()) {
cache.addRay(rayCommand, ChillDefines.META_TYPE, KdpUtil.scale);
cache.addRay(rayCommand, ChillDefines.META_TYPE, NcpPlusUtil.scale);
cache.addRay(rayCommand, ChillDefines.META_TYPE, HdrUtil.scale);
cache.addRay(rayCommand, ChillDefines.META_TYPE, RainUtil.scale);
}
// mark each type as complete -
// use requested rather than available commands so unavailable data is not requested again
for (int t = 0; t < allTypes.size(); ++t) {
String type = allTypes.get(t);
System.out.println("ProxyControlThread: marking " + type + " complete; cached " + cache.getNumberOfRays(rayCommand, type) + " rays");
cache.setCompleteFlag(rayCommand, type);
}
cache.addRay(rayCommand, ChillDefines.CONTROL_TYPE, this.response); // add final response
cache.setCompleteFlag(rayCommand, ChillDefines.CONTROL_TYPE);
System.out.println("ProxyControlThread: marking " + ChillDefines.META_TYPE + " complete; cached " + cache.getNumberOfRays(rayCommand, ChillDefines.META_TYPE) + " rays");
cache.setCompleteFlag(rayCommand, ChillDefines.META_TYPE);
}
System.out.println("ProxyControlThread: (Now) everything is cached.");
}
/**
* Passes the disconnect request to the server and closes the connection
*/
private synchronized void disconnect() throws IOException {
System.out.println("ProxyControlThread: Got Disconnect request");
this.clearChannels();
this.passCommand();
this.killConnection(null);
}
/**
* Closes the connection to the server
*/
public synchronized void killConnection(final ProxyDataThread pdt) throws IOException {
System.out.println("ProxyControlThread: Disconnecting.");
this.clientControlIn.close();
this.clientControlIn = null;
this.clientControlOut.close();
this.clientControlOut = null;
this.clientControlSocket.close();
this.clientControlSocket = null;
this.serverControlIn.close();
this.serverControlIn = null;
this.serverControlOut.close();
this.serverControlOut = null;
this.serverControlSocket.close();
this.serverControlSocket = null;
// Condemn all associated ProxyDataThreads so they'll disconnect, too.
for (ProxyDataThread thread : clientDataThreads) thread.condemn();
this.connected = false;
this.sessionID = -1;
}
/**
* Stop server from sending further data and clear all input streams of remaining unprocessed data.
*
* @throws IOException if an error occurs while communicating with the server
*/
private synchronized void halt() throws IOException {
System.out.println("ProxyControlThread: Got Halt request");
this.passCommand();
this.readAndPassResponse();
this.clearChannels();
}
/**
* Retrieves the status of a given file from the server.
* this.response is overwritten with the result
*
* @throws IOException if an error occurs when communicating with the server
*/
public synchronized void getStatus() throws IOException {
System.out.println("ProxyControlThread: Got Status request");
this.clearChannels();
this.passCommand();
this.readAndPassResponse();
}
/**
* Retrieves the list of available directories/files or bookmarks from the server
*
* @throws IOException if an error occurs when communicating with the server
*/
public synchronized void getDirectory() throws IOException {
System.out.println("ProxyControlThread: Got Directory request");
this.clearChannels();
this.passCommand();
this.readAndPassResponse(); // dir follows
int dirlen = this.response.getExtStatus();
byte[] dirlist = this.readBytes(dirlen, this.serverControlIn);
this.sendBytes(dirlist, dirlen, this.clientControlOut);
this.readAndPassResponse(); // dir sent
}
/**
* Clears all channels of unprocessed incoming data
*
* @throws IOException if an error occurs
*/
private synchronized void clearChannels() throws IOException {
int toSkip;
int skipped;
while (this.serverControlIn.available() > 0) {
toSkip = this.serverControlIn.available();
System.out.println("ProxyControlThread: skipping " + toSkip + " control bytes");
skipped = 0;
while (skipped < toSkip) skipped += this.serverControlIn.skip(toSkip);
for (int i = 0; i < serverDataSockets; ++i) {
toSkip = this.serverDataIn[i].available();
System.out.println("ProxyControlThread: skipping " + toSkip + " data bytes");
skipped = 0;
while (skipped < toSkip) skipped += this.serverDataIn[i].skip(toSkip);
}
Proxy.sleep(100); // wait to see if data is still coming in
}
}
/**
* Reads a SocketArchCtl packet from the client control channel into this.command
*/
protected synchronized void readCommand() throws IOException {
this.command = new SocketArchCtl(readBytes(SocketArchCtl.BYTE_SIZE, clientControlIn));
}
/**
* Passes the command packet to the server
*/
protected synchronized void passCommand() throws IOException {
this.serverControlOut.write(this.command.getBytes(), 0, SocketArchCtl.BYTE_SIZE);
this.serverControlOut.flush();
}
/**
* Reads a SocketResponse packet from the server control channel into this.response
* and passes the response packet to the client
*/
protected synchronized void readAndPassResponse() throws IOException {
while (true) {
System.out.println("reading response");
this.response = new SocketResponse(readBytes(SocketResponse.BYTE_SIZE, serverControlIn));
//System.out.println(this.response.toString());
System.out.println("passing response");
this.passResponse();
switch (response.getStatus()) {
case POSITION_ERROR:
case OPEN_ERROR:
case FORMAT_ERROR: // handle server error codes
System.err.println(this.response.toString());
throw new IOException(this.response.getStatus().toString());
case MESSAGE_FOLLOWS: //check for message
this.sendBytes(this.readBytes(this.response.getExtStatus(), serverControlIn), this.response.getExtStatus(), clientControlOut);
break;
default: //unknown/other response
return;
}
}
}
/**
* Passes the response this.response packet to the client
*/
protected synchronized void passResponse() throws IOException {
this.lastPacketTime = System.currentTimeMillis();
this.clientControlOut.write(this.response.getBytes(), 0, SocketResponse.BYTE_SIZE);
this.clientControlOut.flush();
}
/**
* Read a specified number of bytes from the given InputStream
*
* @param numBytes the number of bytes to read from in (will block until numBytes bytes read)
* @param in the stream to read from
* @return byte[numBytes] containing the read bytes
*/
private synchronized byte[] readBytes(final int numBytes, final DataInputStream in) throws IOException {
this.lastPacketTime = System.currentTimeMillis();
byte[] result = new byte[numBytes];
in.readFully(result);
return result;
}
/**
* Write a byte array of given length to the given OutputStream
*
* @param array byte[numBytes] containing the read bytes
* @param numBytes the number of bytes to read from in (will block until numBytes bytes read)
* @param out the stream to write to
*/
private synchronized void sendBytes(final byte[] array, final int numBytes, final DataOutputStream out) throws IOException {
this.lastPacketTime = System.currentTimeMillis();
out.write(array, 0, numBytes);
out.flush();
}
/**
* Sets up the data connection by sending the source and a default data type.
*
* @param socketNum index of socket on which to send the handshake
*/
private void sendDataHandshake(final int socketNum) throws IOException {
System.out.println("sending data handshake");
this.serverDataOut[socketNum].writeInt(ChillDefines.HELLO);
this.serverDataOut[socketNum].writeInt((this.sessionID << 16) | Channel.GEN_MOM_DAT.ordinal());
this.serverDataOut[socketNum].flush();
do { //get initial scaling info; needed to open first window(s)
ChillHeaderHeader headerH = new ChillHeaderHeader(this.serverDataIn[socketNum]);
assert headerH.recordType == ChillDefines.FIELD_SCALE_DATA;
ChillMomentFieldScale scale = new ChillMomentFieldScale(this.serverDataIn[socketNum], headerH);
System.out.println("got scaling block " + scale.fieldName);
sm.putScale(scale);
if (this.serverDataIn[socketNum].available() > 0) continue; //skip delay
try {
Thread.sleep(250);
} //wait to see if data is still coming in
catch (InterruptedException ie) {
}
} while (this.serverDataIn[socketNum].available() > 0);
this.serverDataOut[socketNum].writeLong(this.dataType[socketNum]);
this.serverDataOut[socketNum].flush();
}
/**
* Adds a ProxyDataThread to this thread's list.
*
* @param thread thread to add
*/
public void addDataThread(final ProxyDataThread thread) {
clientDataThreads.add(thread);
}
/**
* Connects to the archive server specified in Proxy.
*/
public void run() {
try {
connect(this.parent.getServerName(), this.parent.getServerPort());
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
/**
* Returns the session ID associated with this thread's sockets.
*/
public int getSessionID() {
return this.sessionID;
}
public boolean getCalcFlag() {
return this.parent.getCalcFlag();
}
}