package edu.colostate.vchill.socket; 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.gui.FileTreeNode; import edu.colostate.vchill.gui.ViewFileBrowser; import edu.colostate.vchill.socket.SocketArchCtl.Command; import edu.colostate.vchill.socket.SocketArchCtl.DirType; import edu.colostate.vchill.socket.SocketResponse.Status; import java.io.*; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketTimeoutException; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collection; /** * Main class of VCHILL's Socket Module (it's for controlling and interacting * with an archive server). Available functions include connecting, disconnecting, * getting lists of available files and bookmarks. * * @author Jochen Deyke * @author jpont * @version 2010-08-30 */ public final class SocketMain { private static final NumberFormat nf = new DecimalFormat("00"); private static final Config config = Config.getInstance(); private static final ScaleManager sm = ScaleManager.getInstance(); private String url; /** * control connection */ private Socket controlSocket; private DataInputStream controlIn; private DataOutputStream controlOut; /** * data connection */ private Socket dataSocket; private DataInputStream dataIn; private DataOutputStream dataOut; /** * session id from the server */ private int sessionID; /** * is the connection up? */ private boolean connected; /** * the connection this socket is associated with. */ private SocketConnection connection; /** * indicates whether or not to ask for login info. */ private boolean promptLogin; /** * the last data type(s) requested. Don't need to notify server if same type is wanted */ private long lastRequestedDataType = -1; //ensure first request will be different, even if 0 /** * timeout for headers on data channel */ private final static int TIMEOUT = 500; //in millis /** * Sole constructor * * @param promptLogin Indicates whether or not to prompt for login info. */ public SocketMain(final SocketConnection connection, final boolean promptLogin) { this.sessionID = -1; this.connected = false; this.connection = connection; this.promptLogin = promptLogin; } /** * 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 { if (this.connected) this.disconnect(); //can't connect if we're already connected... this.url = server + ":" + port; boolean loggedIn = true; if (this.promptLogin) loggedIn = this.getLogin(server); //do this first to ensure main win doesn't block login prompts if (loggedIn) { doConnect(server, port); /* new Thread(new Runnable() {public void run () { try { doConnect(server, port); } catch (IOException ioe) { DialogUtil.showErrorDialog(ioe.toString()); } }}, "ConnectionThread").start(); */ } } /** * Sets the global login strings (in SocketUtil) if they have not been set. * * @param server The name of the server to be used in prompts * @return true if login OK, false if cancelled */ private boolean getLogin(final String server) throws IOException { String signon = config.getSignon(); String password = config.getPassword(); String[] login = DialogUtil.showLoginDialog("Connecting to " + this.url + " - Login", "Please enter your signon:", signon, "Please enter your password:", password); if (login == null) { System.out.println("Login aborted"); return false; } config.setSignon(login[0]); config.setPassword(login[1]); return true; } private synchronized void doConnect(final String server, final int port) throws IOException { System.out.println("socket: connecting to " + this.url); this.controlSocket = new Socket(); this.controlSocket.setSoTimeout(5000); //timeout on control while connecting (ms) this.controlSocket.connect(new InetSocketAddress(server, port), 5000); this.controlIn = new DataInputStream(new BufferedInputStream(this.controlSocket.getInputStream())); this.controlOut = new DataOutputStream(new BufferedOutputStream(this.controlSocket.getOutputStream())); SocketResponse response = null; loop: do { if (!this.connected) { //first packet; preface with channel type this.controlOut.writeInt(ChillDefines.HELLO); this.controlOut.writeInt(Channel.ARCH_CTL.ordinal()); } this.sendPacket(new SocketArchCtl( Command.CONNECT_REQ, (short) 0, //start_sweep (ignored field) (short) 2, //ray_step (2 = java version (vs old C version)) (short) Version.majorRevision, //sweep_low (major version) (short) Version.minorRevision, //sweep_high (minor version) 0, //extra_delay (ignored field) config.getSignon() + ":" + config.getPassword())); response = this.readResponse(); if (response == null) return; //failure this.connected = true; //mark as connected so correct packet gets sent on retry switch (response.getStatus()) { case SERVER_READY: //no message of the day break loop; case SIGNON_UNKNOWN: //bad login config.setSignon(null); config.setPassword(null); DialogUtil.showErrorDialog("Unknown signon", "Specified email address is not registered.\n" + "please try again, or if you have not\n" + "registered, please do so at:\n" + "http://chill.colostate.edu/vchill/"); if (!this.getLogin(server)) { //retried login cancelled this.connected = false; return; } break; case PASSWD_FAIL: //bad password config.setPassword(null); DialogUtil.showErrorDialog("Bad password", "Password is incorrect, please check\n" + "your Caps Lock status and try again.\n" + "If you cannot, please call 970-491-6248\n" + "or email dave@chill.colostate.edu"); if (!this.getLogin(server)) { //retried login cancelled this.connected = false; return; } break; case SERVER_BUSY: case SERVER_FAILURE: //server problem DialogUtil.showErrorDialog("Problems with " + this.url, "Due to technical difficulties, the server is\n" + "currently not available. Please try connecting\n" + "again in a few minutes. If the problem persists,\n" + "please email dave@chill.colostate.edu and report\n" + "the problem."); this.connected = false; return; default: //unknown response throw new Error("Don't know how to handle:\n" + response); } } while (true); this.sessionID = response.getExtStatus(); System.out.println("session id = " + this.sessionID); this.controlSocket.setSoTimeout(0); //no timeout on control this.dataSocket = new Socket(server, port); this.dataSocket.setSoTimeout(0); //no default timeout on data this.dataIn = new DataInputStream(new BufferedInputStream(this.dataSocket.getInputStream())); this.dataOut = new DataOutputStream(new BufferedOutputStream(this.dataSocket.getOutputStream())); this.sendDataHandshake(); if (this.controlSocket == null || this.dataSocket == null) { System.err.println("Socket FAILED!! to connect!"); this.disconnect(); throw new IOException("Fatal error! Socket connection failure!"); } System.out.println("Connected."); } /** * Sets up the data connection. * Once the connection is open, The client writes two ints: a hello word (int 0xf0f00f0f), * and a channel request word (int 15 is used to request the GEN_MOM_DAT_CHANNEL type). * The server will respond with a ChillHSKHeader, followed by a ChillMomentFieldScale * for each available field. This provides the display with information on what's available * and how to scale and display the data. */ private void sendDataHandshake() throws IOException { this.dataOut.writeInt(ChillDefines.HELLO); this.dataOut.writeInt((this.sessionID << 16) | Channel.GEN_MOM_DAT.ordinal()); this.dataOut.flush(); try { Thread.sleep(250); } //wait to see if data is coming in catch (InterruptedException ie) { } while (this.dataIn.available() > 0) { //get (now optional) initial scaling info ChillHeaderHeader headerH = new ChillHeaderHeader(this.dataIn); assert headerH.recordType == ChillDefines.FIELD_SCALE_DATA; ChillMomentFieldScale scale = new ChillMomentFieldScale(this.dataIn, headerH); sm.putScale(scale); if (this.dataIn.available() > 0) continue; //skip delay try { Thread.sleep(250); } //wait to see if data is still coming in catch (InterruptedException ie) { } } } /** * Reads a response from the control channel. * If that response was a message, shows that message and reads another response. * * @return a SocketResponse packet from the control channel, * or null if an EOFException is encountered */ protected SocketResponse readResponse() throws IOException { SocketResponse response; while (true) { try { response = new SocketResponse(this.controlIn); } catch (EOFException eofe) { //got kicked out System.out.println("Kicked from server " + this.url + "!"); DialogUtil.showErrorDialog("Kicked from server " + this.url + "!"); this.cleanupDeadConnection(); return null; } switch (response.getStatus()) { case REQUEST_ERROR: case OPEN_ERROR: case FORMAT_ERROR: //handle server error codes //System.err.println(response.toString()); throw new IOException(response.getStatus().toString()); case MESSAGE_FOLLOWS: //check for message String message = SocketUtil.readString(this.controlIn, response.getExtStatus()); DialogUtil.showHelpDialog("Message from " + this.url, message); break; default: //unknown response return response; } } } /** * Sends a disconnect message, then closes the connection to the server */ public synchronized void disconnect() throws IOException { System.out.println("socket: disconnecting"); try { this.sendPacket(new SocketArchCtl(Command.DISCONNECT_REQ, (short) 0, (short) 0, (short) 0, (short) 0, 0, "")); //ignored fields //disconnect doesn't get a response, so we don't need to read it //} catch (Exception e) {} } finally { cleanupDeadConnection(); } } /** * Closes the (presumed dead) connection to the server */ private void cleanupDeadConnection() throws IOException { this.dataIn.close(); this.dataIn = null; this.dataOut.close(); this.dataOut = null; this.dataSocket.close(); this.dataSocket = null; this.controlIn.close(); this.controlIn = null; this.controlOut.close(); this.controlOut = null; this.controlSocket.close(); this.controlSocket = null; this.connected = false; this.lastRequestedDataType = 0; this.sessionID = -1; } /** * Retrieves the list of available (sub-)directories and files from the server * * @return a Collection of String objects containing the directory listing (including any extended data) * @throws IOException if an error occurs when communicating with the server */ public synchronized Collection<String> getDirectory(final String dir) throws IOException { SocketResponse response; try { Collection<String> list = new ArrayList<String>(); this.sendPacket(new SocketArchCtl( Command.DIRECTORY_REQ, (short) DirType.CONTENTS.ordinal(), //dirs + files (short) 0, (short) 0, (short) 0, 0, //ignored fields dir.split(" ")[0])); //strip extra info response = this.readResponse(); //dir follows if (response == null) return list; //failure - return empty list assert response.getStatus() == Status.DIRECTORY_FOLLOWS; int dirlen = response.getExtStatus(); byte[] dirlist = this.readControlBytes(dirlen); response = this.readResponse(); //dir sent if (response == null) return list; //failure - return empty list assert response.getStatus() == Status.DIRECTORY_SENT; int start = 0; for (int end = 0; end < dirlen; ++end) { //cut the byte[] into Strings switch (dirlist[end]) { case 0: case '\n': case '\r': //end of a string String x = new String(dirlist, start, end - start, "UTF-8"); start = end + 1; if (!x.equals("")) { String[] parts = x.split(" "); if (parts.length < 2) { if (parts[0].contains(".")) continue; //no scan type => not a data file } else { if (parts[parts.length - 1].equals("TS")) continue; //timeseries => no moment data } list.add(x); } } } return list; } catch (SocketTimeoutException stoe) { System.err.println("socket: timed out getting directory; retrying"); return this.getDirectory(dir); } } /** * Retrieves the list of available sweeps in a given file from the server * * @param dir the directory to list * @param file a file in <code>dir</code> to list * @return a Collection of String objects containing the sweeps * @throws IOException if an error occurs when communicating with the server */ public synchronized Collection<String> getSweepList(final String dir, final String file) throws IOException { try { SocketResponse response = this.getStatus(dir, file); if (response == null) return new ArrayList<String>(); //failure - return empty list int numSweeps = response.getMaxSweeps(); Collection<String> list = new ArrayList<String>(numSweeps); for (int i = 0; i < numSweeps; ++i) list.add("Sweep " + nf.format(i + 1)); if (response.isCalibrationPresent()) { FileTreeNode node = ViewFileBrowser.getInstance().getActions().getNode(url, dir, file); if (node != null) node.special = true; } return list; } catch (SocketTimeoutException stoe) { System.err.println("socket: timed out getting sweep list; retrying"); return this.getSweepList(dir, file); } } /** * Retrieves the status of a given file from the server. * this.response is overwritten with the result * * @param dir name of a directory (full path, no trailing slash) * @param file name of a file in <code>dir</code> to get the status of * @return a SocketResponse object (this.response) containing the server's response * @throws IOException if an error occurs when communicating with the server */ public synchronized SocketResponse getStatus(final String dir, final String file) throws IOException { this.sendPacket(new SocketArchCtl( Command.STATUS_REQ, (short) 0, (short) 0, (short) 0, (short) 0, 0, //ignored fields dir.split(" ")[0] + "/" + file.split(" ")[0])); //chop off extended info from names return this.readResponse(); } /** * Sends the requested fields list to the server. * * @param newDataType A bitmask of requested fields. * @throws java.io.IOException */ private void sendRequestedFields(final long newDataType) throws IOException { if (newDataType != this.lastRequestedDataType) { //only reset data channel if actually different System.out.println("requesting " + Long.toBinaryString(newDataType));//Long.toHexString(newDataType)); this.dataOut.writeLong(this.lastRequestedDataType = newDataType); this.dataOut.flush(); } } /** * Retrieves a sweep from the server. * The downloaded sweep is stored in the cache specified. * * @param commands a ControlSyncQueue containing a ControlMessage containing all necessary info for type of data desired * @param cache a shared cache object to load the data into * @throws IOException if an error occurs when communicating with the server */ public synchronized void getSweep(final ControlSyncQueue<TypedControlMessage> commands, final CacheMain cache) throws IOException { //gather list of types to get TypedControlMessage command = commands.get(); if (!command.message.isValid()) { this.connection.setIsSweepDone(true); return; } ArrayList<String> requestTypes = new ArrayList<String>(ChillDefines.MAX_PER_CHANNEL); //ControlMessages representing data requested ControlMessage rayCommand; //data currently being worked on ChillHeaderReader hr = new ChillHeaderReader(this.dataIn, cache) { ArrayList<String> types = null; //names of available data types @Override public boolean readData(final ChillDataHeader dataH, final ControlMessage metaCommand) throws IOException { if (types == null) { //only initialize once types = dataH.calculateTypes(); //determine which types are available } dataSocket.setSoTimeout(0); //no timeout on data byte[][] data = new byte[types.size()][dataH.numGates]; byte[] interleavedData = readDataBytes(types.size() * dataH.numGates); for (int t = 0; t < types.size(); ++t) { //split data types if (types.get(t) == null) continue; for (int g = 0; g < dataH.numGates; ++g) { data[t][g] = interleavedData[types.size() * g + t]; } cache.addRay(metaCommand, types.get(t), new ChillGenRay(hskH, dataH, types.get(t), data[t])); //add data to cache } return true; //currRayNum should increment //++currRayNumber; //increment ray number here so it doesn't go up on "continue" } }; System.out.println("socket: getting " + command); long newDataType = 0; for (String type : command.types) { ChillMomentFieldScale scale = sm.getScale(type); if (scale == null) continue; //unknown/not available requestTypes.add(type); //unknown fields will not be marked complete newDataType |= 1l << scale.fieldNumber; //add the new type to the bitmask if (requestTypes.size() == ChillDefines.MAX_PER_CHANNEL) break; //allow at most MAX_PER_CHANNEL data types (this is the server's limit) } sendRequestedFields(newDataType); //request data String filename = command.message.getDir().split(" ")[0] + "/" + command.message.getFile().split(" ")[0]; this.sendPacket(new SocketArchCtl( Command.SWEEP_MODE, Short.parseShort(command.message.getSweep().split(" ")[1]), (short) 0, (short) 0, (short) 0, 0, //ignored fields filename)); SocketResponse response = this.readResponse(); if (response == null) { //failure this.connection.setIsSweepDone(true); return; } if (response.getStatus() == Status.POSITION_ERROR) { System.err.println("Warning: Position error - skipping nonexistant sweep"); markComplete(requestTypes, command.message, cache); //error - don't bother to retry this.connection.setIsSweepDone(true); return; } else if (!response.isRunning()) { //something went wrong and the server isn't sending the sweep System.err.println("Failed to get sweep"); markComplete(requestTypes, command.message, cache); //error - don't bother to retry this.connection.setIsSweepDone(true); return; } //read radar data into cache sm.clear(); //clear the list of fields because the server should be sending us a new list int firstRayNumber = response.getRayNumber() - 1; //convert to 0-based int lastRayNumber = Integer.MAX_VALUE; //don't know when to stop yet for (int currRayNumber = firstRayNumber; currRayNumber < lastRayNumber; ) { //each ray: if (commands.stopping()) { //stop request received System.out.println("SocketMain: Attempting to stop..."); this.stop(filename); for (String type : requestTypes) cache.removeType(command.message, type); //remove incomplete sweep this.connection.setIsSweepDone(true); return; } if (this.controlIn.available() > 0) { //notification: sweep done response = this.readResponse(); if (response == null) { //failure for (String type : requestTypes) cache.removeType(command.message, type); //remove incomplete sweep this.connection.setIsSweepDone(true); return; } lastRayNumber = response.getRayNumber(); //we know when to stop now if (lastRayNumber == currRayNumber) break; } try { this.dataSocket.setSoTimeout(TIMEOUT); //default timeout on data //mark the stream so that if a timeout occurs then we can //put back all the data read so that VCHILL doesn't get //messed up this.dataIn.mark(Integer.MAX_VALUE); if (hr.readHeader(command.message)) ++currRayNumber; //successful data read? } catch (SocketTimeoutException stoe) { System.err.println("SocketMain: Read timed out getting sweep; retrying"); //put back the data that we tried reading this.dataIn.reset(); continue; } //try { Thread.sleep(5); } //let plotting code have a go //catch (InterruptedException ie) {} } markComplete(requestTypes, command.message, cache); this.connection.setIsSweepDone(true); } private void markComplete(final ArrayList<String> requestTypes, final ControlMessage command, final CacheMain cache) { //mark each type as complete - //use requested rather than available commands so unavailable data is not requested again for (String type : requestTypes) { System.out.println("marking " + type + " complete; cached " + cache.getNumberOfRays(command, type) + " rays"); cache.setCompleteFlag(command, type); } cache.setCompleteFlag(command, ChillDefines.META_TYPE); } /** * Stop server from sending further data and clear all input streams of remaining unprocessed data * * @param filename the name of the file currently downloading * @throws IOException if an error occurs while communicating with the server */ private void stop(final String filename) throws IOException { this.sendPacket(new SocketArchCtl( Command.HALT_COMMAND, (short) 0, (short) 0, (short) 0, (short) 0, 0, //ignored fields filename)); //The server sends a response to the halt command but //since we are going to skip everything then it's not //necessary to explicitly read the response. In fact, //not explicitly reading it prevents the situation where //vchill gets stuck because it's supposed to read the //data channel first. //SocketResponse response = this.readResponse(); //if (response == null) return; //failure //System.out.println(response); clearChannels(); } /** * Clears all channels of unprocessed incoming data * * @throws IOException if an error occurs */ private void clearChannels() throws IOException { int toSkip; int skipped; while (this.dataIn.available() + this.controlIn.available() > 0) { toSkip = this.controlIn.available(); System.out.println("skipping " + toSkip + " control bytes"); skipped = 0; while (skipped < toSkip) skipped += this.controlIn.skip(toSkip); toSkip = this.dataIn.available(); System.out.println("skipping " + toSkip + " data bytes"); skipped = 0; while (skipped < toSkip) skipped += this.dataIn.skip(toSkip); try { Thread.sleep(100); } //wait to see if data is still coming in catch (InterruptedException ie) { } } } /** * Send a command packet over the control channel * * @param command the command to send */ protected void sendPacket(final SocketArchCtl command) throws IOException { command.write(this.controlOut); this.controlOut.flush(); } /** * Check connection status * * @return true if the connection is active and ready, false if it is not */ public synchronized boolean connected() { if (this.controlSocket == null || this.dataSocket == null) return false; if (!(this.controlSocket.isConnected() && this.dataSocket.isConnected())) { try { System.out.println("Socket: socket timed out, disconnecting"); this.disconnect(); } catch (IOException ie) { throw new Error("Socket PANIC!! - IOException while trying to disconnect: ", ie); } } return this.connected; } /** * Read a specified number of bytes from the control channel * * @param numBytes the number of bytes to read from in (will block until numBytes bytes read) * @return byte[numBytes] containing the read bytes */ private byte[] readControlBytes(final int numBytes) throws IOException { return this.readBytes(numBytes, this.controlIn); } /** * Read a specified number of bytes from the data channel * * @param numBytes the number of bytes to read from in (will block until numBytes bytes read) * @return byte[numBytes] containing the read bytes */ private byte[] readDataBytes(final int numBytes) throws IOException { return this.readBytes(numBytes, this.dataIn); } /** * 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 byte[] readBytes(final int numBytes, final DataInputStream in) throws IOException { byte[] result = new byte[numBytes]; in.readFully(result); return result; } }