/**
** Copyright (C) SAS Institute, All rights reserved.
** General Public License: http://www.opensource.org/licenses/gpl-license.php
**/
package org.safs.sockets;
import java.io.*;
import java.net.*;
import java.util.Enumeration;
import java.util.Vector;
/**
* This class implements a TCP protocol needed for Sockets control of remote processes
* through TCP String messages character encoded in UTF-8.
* <p>
* The class implements both the "local" controller side and the "remote" ServerSocket
* side in order to keep the protocol implementation for both sides correct.
* <p>
* This class is not normally used directly, but thru subclasses of {@link AbstractProtocolRunner}
* which provides the necessary separate Threading required for the SocketProtocol.
* <p>
* Currently, this protocol expects remote TCP services to be running and accepting connections
* on port {@link #DEFAULT_REMOTE_PORT} at the first, if port {@link #DEFAULT_REMOTE_PORT} is no available,
* remote TCP services will try to run on 2412, 2414 ... with augmentation of pace 2, the maximum possible
* port is {@link #MAX_SERVER_PORT}.
* By trying a broad range of ports, it can prevent conflicts with other system resources.
*
* Currently, this protocol lets local controller to contact and attempt a connection to
* those remote TCP services. As those TCP services can choose port dynamically, the local controller
* doesn't know which remote port to use for the connection. It will try to connect to port {@link #DEFAULT_REMOTE_PORT},
* if fail it will try to connect to port 2412, 2414 ... with augmentation of pace 2, the maximum possible
* port is {@link #MAX_SERVER_PORT}.
* <p>
*
* If there is no need for the local controller's port to be specific--such as using port forwarding
* to local emulators appearing as remote machines--then the local controller port really could be
* any port at all.
*
* <p>
* There is an initial handshake or verification that occurs between the local and remote SocketProtocol
* instances to confirm the device port owners are both SocketProtocol implementations.
* @see AbstractProtocolRunner
*
* (LeiWang) SEP 29, 2012 Fix a connection problem for mobile-device connected by USB (portForwarding is used)
*/
public class SocketProtocol {
public String TAG = getClass().getSimpleName();
private static int protocol = 1; // TCP Protocol version
public static final String DEFAULT_SERVER = "localhost";
public static final int DEFAULT_REMOTE_PORT = 2410;
public static final int DEFAULT_CONTROLLER_PORT = 2411;
public static final int MAX_SERVER_PORT = 2500;
public static final int NEXT_SERVER_PORT_PACE = 2;
private boolean local_mode = true; // determines local or remote operating mode
private boolean isRunning = false;
/** "PROTOCOLVERSION"
* Prompt and partial response for initial handshake between the local protocol runner and a remote
* protocol runner. The local runner will send this prompt terminated with the EOM marker and
* expects to receive a response in the format of "PROTOCOLVERSION=N"--an Integer representing the remote
* runner's protocol version. Currently, only "PROTOCOLVERSION=1" showing version 1
* is supported. The response String is expected to be terminated with the EOM marker. */
public static final String MSG_PROTOCOL_VERSION_QUERY = "PROTOCOLVERSION";
public static final String ENV_KEY_REMOTE_PORT = "ENV_KEY_REMOTE_PORT";
/** Indicates the Socket is being shutdown normally.
* @see #shutdownThread
* @see #shutdownCause */
public static final int STATUS_SHUTDOWN_NORMAL = 0;
/** Indicates the Socket is being shutdown abnormally no
* remote client connection will be possible. All attempts have been exhausted.
* @see #shutdownThread
* @see #shutdownCause */
public static final int STATUS_SHUTDOWN_REMOTE_CLIENT = 1;
/** Indicates the Socket is being shutdown abnormally because no
* remote service connection will be possible. All attempts have been exhausted.
* @see #shutdownThread
* @see #shutdownCause */
public static final int STATUS_SHUTDOWN_REMOTE_SERVICE = 2;
/** Indicates the Socket is being shutdown because from the controller side.
* @see #shutdownThread
* @see #shutdownCause */
public static final int STATUS_SHUTDOWN_CONTROLLER = 3;
/**
* String representations of the STATUS_SHUTDOWN causes.
*/
protected static final String[] STATUS_STRINGS = new String[]{"Normal Shutdown initiated",
"Remote Test initiating Shutdown",
"Remote Service initiating Shutdown",
"Controller initiating Shutdown"};
/** "[!_!]"
* Case-Insensitive End-Of-Message marker for all character messages exchanged between local and remote runners.<br>
* Normally, this is never changed. The local and remote runner implementations--coded
* together (or this same class)--share in an implied contract of what this marker shall be
* since both ends of the protocol must use it.*/
protected String EOM = "[!_!]";
private boolean keepAlive = true;
/** Hostname to contact for a remote client Runner. */
protected String remoteHostname = DEFAULT_SERVER;
/** port to contact for a remote client Runner. */
protected int remotePort = DEFAULT_REMOTE_PORT;
/** port to use for a local controller Runner. */
protected int controllerPort = DEFAULT_CONTROLLER_PORT;
/** default 60 seconds timeout to complete a connection with a remote ServerSocket accepting
* connections. */
private int clientConnectTimeout = 60; //seconds
/** local controller Socket connection. */
protected Socket controllerRunner = null;
/** remote client ServerSocket connection. */
private ServerSocket remoteRunner = null;
/** inputstream from the other Runner.*/
private InputStream inputstream = null;
/** inputstreamreader from the other Runner.*/
private InputStreamReader rawinputreader = null;
/** bufferedreader from the other Runner.*/
private BufferedReader bufferedreader = null;
/** outputstream to the other Runner.*/
private OutputStream outputstream = null;
/** outputstreamwriter to the other Runner.*/
private OutputStreamWriter rawoutputwriter = null;
/** bufferedwriter to the other Runner.*/
private BufferedWriter bufferedwriter = null;
/** Named DebugListeners and ConnectionListeners registered with this instance. */
private Vector combinedlisteners = new Vector();
/**
* int value providing an indication of why the thread might have been
* shutdown prematurely.
* @see #STATUS_SHUTDOWN_REMOTE_CLIENT
* @see #STATUS_SHUTDOWN_NORMAL */
private int shutdownCause = STATUS_SHUTDOWN_NORMAL;
/**
* For Local controller side, it is used to indicate whether it is connected to
* Remote side (ServerSocket)<br>
*
* For Remote side, it is used to indicate whether a controller runner has
* connected to it (ServerSocket).<br>
*/
private boolean connected = false;
/**
* Default no-op constructor using all defaults.
* The user should change any desired remote settings and add
* any DebugListeners and ConnectionListerners prior to starting the
* full use of the instance.
*/
public SocketProtocol(){;}
/**
* Constructor using all defaults while registering a NamedListener.
* The user should change any desired remote hostname/port settings prior to starting
* the full use of the instance.
* @param listener NamedListener (DebugListener, ConnectionListener, etc...) to register with
* the new instance. Ideally, the listener should implement both DebugListener and
* ConnectionListener interfaces.
*/
public SocketProtocol(NamedListener listener){
this();
addListener(listener);
}
/**
* Constructor using all defaults while registering a NamedListener and
* setting the local or remote mode of the instance.
* <p>
* By default, the class is setup to run in local controller mode.
* Users would normally only call this constructor to make local_mode = false and
* run in remote client mode.
* <p>
* The user should change any desired remote hostname/port settings prior to starting
* the full use of the instance.
* @param listener NamedListener to register with the new instance. Ideally, the
* listener should implement both DebugListener and ConnectionListener interfaces.
*/
public SocketProtocol(NamedListener listener, boolean local_mode){
this(listener);
setLocalMode(local_mode);
}
/**
* Set the mode of this instance. True for local controller mode and false for
* remote client mode. By default, the class is setup to run in local controller mode.
* @param local_mode true to run in local protocol controller mode (ex: port 2411). false
* to run in remote protocal client mode (ex: port 2410).
* @throws IllegalThreadStateException if a call attempts to set/change this while the
* instance is already connected or running.
*/
public void setLocalMode(boolean local_mode)throws IllegalThreadStateException{
if(isRunning){
throw new IllegalThreadStateException("Cannot change local/remote mode while thread is running.");
}else{this.local_mode = local_mode;}
}
/**
* @return true if the Runner is set for local controller mode.
* false for remote client mode.
*/
public boolean isLocalMode(){
return local_mode;
}
/**
* Register a DebugListener/ConnectionListener. If no DebugListener is registered then our
* debug output, if enabled, will go to System.out.println.
* @param listener to register to receive notifications.
* @return true if the listener was new and was successfully added
* @see #notifyConnection()
* @see #notifyLocalShutdown(int)
* @see #notifyRemoteShutdown(int)
*/
public boolean addListener(NamedListener listener){
String debugmsg = TAG+".addListener(): ";
boolean result = false;
if (listener == null) {
debug(debugmsg+"Invalid null NamedListener cannot be registered.");
return false;
}
if(! combinedlisteners.contains(listener)) {
result = combinedlisteners.add(listener);
if(result) {
debug(debugmsg+listener.getListenerName() +" was successfully registered.");
}else{
debug(debugmsg+listener.getListenerName() +" was NOT successfully registered.");
}
return result;
}
debug(debugmsg+"Runner is likely already registered with "+ listener.getListenerName());
return false;
}
/**
* @param listener
* @return true if the listener was found and was successfully removed
*/
public boolean removeListener(NamedListener listener){
String debugmsg = TAG+".removeListener(): ";
boolean result = false;
if (listener == null) {
debug(debugmsg+"Invalid null NamedListener cannot be unregistered.");
return false;
}
if(combinedlisteners.contains(listener)) {
result = combinedlisteners.remove(listener);
if(result) {
debug(debugmsg+listener.getListenerName() +" was successfully unregistered.");
}else{
debug(debugmsg+listener.getListenerName() +" was NOT successfully unregistered.");
}
return result;
}
debug(debugmsg+"DebugListener is likely NOT registered and was not unregistered.");
return false;
}
/** set to false to disable debug logging and improve performance. */
public boolean _debugEnabled = true;
/**
* Convenience routine for logging debug messages. Simply forwards the call to
* notifyDebug(String).
* @param text
* @see #notifyDebug(String)
*/
protected void debug(String text){
notifyDebug(text);
}
/**
* Notify registered DebugListeners to log a debug message.
* Notification will be sent to all registered DebugListeners, or to System.out.println
* if we have none or we did not successfully send to any listeners.
* This will only happen if debug logging is enabled--which it is by default.
* @param text
* @see DebugListener
* @see #_debugEnabled
*/
private void notifyDebug(String text){
if(_debugEnabled){
boolean success = false;
if(combinedlisteners.size() > 0){
for(int i=0;i<combinedlisteners.size();i++){
try{
((DebugListener)combinedlisteners.get(i)).onReceiveDebug(text);
success = true;
}
catch(Exception x){}
}
}
if(!success) System.out.println(text);
}
}
/**
* @param cause -- int STATUS_SHUTDOWN_xxx cause to describe.
* @return A String description of the cause of the shutdown. This is deduced from
* the shutdownCause int being used as a String[] lookup in STATUS_STRINGS.
* @see #STATUS_STRINGS
*/
public static String getShutdownCauseDescription(int cause){
String append = ".";
try{ append = ": "+ STATUS_STRINGS[cause];}catch(Exception x){}
return append;
}
/**
* Notify registered ConnectionListeners that a local to remote communication connection
* has been established..
* Notification will be sent to all registered ConnectionListeners, or to System.out.println
* if we have none or we did not successfully send to any listeners.
* @param text
* @see ConnectionListener#onReceiveConnection()
*/
private void notifyConnection(){
boolean success = false;
if(combinedlisteners.size() > 0){
for(int i=0;i<combinedlisteners.size();i++){
try{
((ConnectionListener)combinedlisteners.get(i)).onReceiveConnection();
success = true;
}
catch(Exception x){}
}
}
if(!success) System.out.println("Protocol Runners Connected");
}
/**
* Notify registered ConnectionListeners that a local shutdown has or should occur.
* Notification will be sent to all registered ConnectionListeners, or to System.out.println
* if we have none or we did not successfully send to any listeners.
* @param int shutdownCause
* @see ConnectionListener#onReceiveLocalShutdown(int)
*/
private void notifyLocalShutdown(int shutdownCause){
boolean success = false;
if(combinedlisteners.size() > 0){
for(int i=0;i<combinedlisteners.size();i++){
try{
((ConnectionListener)combinedlisteners.get(i)).onReceiveLocalShutdown(shutdownCause);
success = true;
}
catch(Exception x){}
}
}
if(!success) System.out.println("Protocol Runner performing local shutdown"+ getShutdownCauseDescription(shutdownCause));
}
/**
* Notify registered ConnectionListeners that a remote shutdown has or should occur.
* Notification will be sent to all registered ConnectionListeners, or to System.out.println
* if we have none or we did not successfully send to any listeners.
* @param int shutdownCause
* @see ConnectionListener#onReceiveRemoteShutdown(int)
*/
private void notifyRemoteShutdown(int shutdownCause){
boolean success = false;
if(combinedlisteners.size() > 0){
for(int i=0;i<combinedlisteners.size();i++){
try{
((ConnectionListener)combinedlisteners.get(i)).onReceiveRemoteShutdown(shutdownCause);
success = true;
}
catch(Exception x){}
}
}
if(!success) System.out.println("Protocol Runner received remote shutdown"+ getShutdownCauseDescription(shutdownCause));
}
/**
* @return the current Case-insensitive End-Of-Message marker String ending all character messages in the
* protocol.<br>
* Normally, this is never changed. The local and remote runner implementations--coded
* together (or this same class)--share in an implied contract of what this marker shall be
* since both ends of the protocol must use it.*/
public String getEOM() {
return EOM;
}
/**
* @param Case-insensitive End-Of-Message marker String to use for terminating all String messages in the protocol.
* Normally, this is never changed. The local and remote runner implementations--coded
* together (or this same class)--share in an implied contract of what this marker shall be
* since both ends of the protocol must use it.
* @throws IllegalArgumentException if the supplied marker argument is null, zero-length, or appears to be
* case-sensitive when comparing calls for toUpperCase() and toLowerCase(). */
public void setEOM(String marker) throws IllegalArgumentException{
if(marker == null ||
marker.length() == 0 ||
(! marker.toUpperCase().equals(marker.toLowerCase())))
throw new IllegalArgumentException("TCP protocol EOM marker must be a valid case-insensitive String of 1 or more characters.");
EOM = marker;
}
// store a runtime exception type so we register/log it only once.
private String _acceptException = null;
/**
* Used internally.
* This is for a remote client ServerSocket instance looking for connection requests from an
* external local controller instance.
* <p>
* If a connection request is received this routine will then attempt to validate
* the external local controller via verifyController. If the validation succeeds
* this routine will notify registered listeners of the connection.
* <p>
* If the connection request is received but the validation fails this routine will
* call notify registered listeners of a local shutdown and proceed to shutdown all
* sockets and streams.
* @param msTimeout milliseconds to look for a connection request.
* @return true if we accepted and verified a controller connection
* @throws IllegalThreadStateException if this method is called from an instance operating
* in local controller mode.
* @see #verifyControllerClient(int)
* @see #notifyConnection()
* @see #notifyLocalShutdown(int)
* @see #closeStreams()
*/
private boolean acceptControllerConnection(int msTimeout) throws IllegalThreadStateException{
String debugmsg = TAG+".acceptControllerConnection(): ";
if(isLocalMode()) throw new IllegalThreadStateException(
"Cannot acceptControllerConnections when running in local controller mode.");
try{
if(controllerRunner == null && remoteRunner != null) { // same as tcp not connected
remoteRunner.setSoTimeout(msTimeout);
controllerRunner = remoteRunner.accept();
controllerRunner.setKeepAlive(keepAlive);
connectStreams(controllerRunner);
debug(debugmsg+"Verifying controller Socket connection from: "+
controllerRunner.getInetAddress().getHostAddress()+
" port "+ controllerRunner.getLocalPort());
if(verifyControllerClient(10)) {
notifyConnection();
setConnected(true);
return true;
}
debug(debugmsg+"Remote client did NOT verify itself as a Protocol Runner running in local mode!");
// remote Socket is not our protocol client -- cannot proceed
shutdownCause = STATUS_SHUTDOWN_REMOTE_CLIENT;
notifyLocalShutdown(STATUS_SHUTDOWN_REMOTE_CLIENT);
setConnected(false);
}
}catch(Exception st){
if(!(st instanceof SocketTimeoutException)){
// only log this once per unique exception type
if(! st.getClass().getSimpleName().equals(_acceptException)){
_acceptException = st.getClass().getSimpleName();
debug(debugmsg+"acceptServerConnection "+
_acceptException +", "+ st.getMessage());
}
}
}
return false;
}
/**
* Used Internally.
* captures input and output streams from the Socket and instantiates necessary
* Buffered Readers and Writers with UTF-8 character encoding.
* @param socket valid connected Socket instance.
* @throws IOException if problems instantiating Readers and Writers.
*/
private void connectStreams(Socket socket)throws IOException{
inputstream = socket.getInputStream();
rawinputreader = new InputStreamReader(inputstream, "UTF-8");
bufferedreader = new BufferedReader(rawinputreader);
outputstream = socket.getOutputStream();
rawoutputwriter = new OutputStreamWriter(outputstream, "UTF-8");
bufferedwriter = new BufferedWriter(rawoutputwriter);
}
/**
* Used Internally.
* Close all instantiated Streams, Readers, and Writers and null
* all associated references.
*/
private void closeStreams(){
try{ if(bufferedreader!=null) bufferedreader.close();}catch(Exception x){}
try{ if(bufferedwriter!=null) bufferedwriter.close();}catch(Exception x){}
try{ if(rawinputreader!=null) rawinputreader.close();}catch(Exception x){}
try{ if(rawoutputwriter!=null) rawoutputwriter.close();}catch(Exception x){}
inputstream = null;
rawinputreader = null;
bufferedreader = null;
outputstream = null;
rawoutputwriter = null;
bufferedwriter = null;
}
/**
* Used Internally. Used by Local Controller side.
* Attempt to connect to a remote protocol client.
* This is only valid in local controller mode. For remote client mode the equivalent
* call would be {@link #acceptControllerConnection(int)}.
* <p>
* If a connection is accepted this routine will then attempt to validate
* the remote client via verifyRemoteClient. If the validation succeeds
* this routine will notify registered listeners of the connection.
* <p>
* If the connection is accepted but the validation fails this routine will
* call notify registered listeners of a local shutdown and proceed to shutdown all
* sockets and streams.
* @param sTimeout in seconds to keep trying to make the connection
* @return boolean -- connection established.
* false means "not successfully connected".
* @throws IllegalThreadStateException if the Thread is running in remote client
* mode instead of local controller mode.
* @see #notifyConnection()
* @see #notifyLocalShutdown(int)
* @see #closeStreams()
*/
private boolean createRemoteClientConnection(int sTimeout) throws IllegalThreadStateException{
String debugmsg = TAG+".createRemoteClientConnection(): ";
if(! isLocalMode()) throw new IllegalThreadStateException(
"Cannot createRemoteClientConnections when running in remote client mode.");
boolean keepTrying = true;
int ticks = 0;
int msWait = 100;
int maxticks = (sTimeout > 0) ? (sTimeout * (1000/msWait)) : 0;
int verifyRemoteTimeoutSecond = 10;
int verificationFail = 0;
int verificationFailMaxTry = sTimeout<verifyRemoteTimeoutSecond ? 1:(sTimeout/verifyRemoteTimeoutSecond);
debug(debugmsg+"Local Runner attempting to make remote Runner connection...");
try{
while(keepTrying){
//Try to bind to the remote ServerSocket at the same remotePort
bindToRemoteServer();
if(controllerRunner==null){
keepTrying = ticks++ < maxticks;
try{ Thread.sleep(msWait);}catch(Exception x){}
}else{
debug(debugmsg+"Remote Runner seems to be connected!");
controllerRunner.setKeepAlive(keepAlive);
connectStreams(controllerRunner);
if(verifyRemoteClient(verifyRemoteTimeoutSecond)) {
debug(debugmsg+"Remote Runner has been connected!");
notifyConnection();
setConnected(true);
return true;
}else{
//remote Socket is not SAFS client -- cannot proceed
debug(debugmsg+"Remote client did NOT verify itself as a remote SocketProtocolRunner!");
//Do we need continue to connect?
//Yes. If verification fail due to IOException, read input timeout, remote not ready etc.
//No. If verification fail due to failure of handshake: "wrong remote", how to detect?
verificationFail +=1;
keepTrying = (verificationFail<verificationFailMaxTry);
//Close streams and set controllerRunner to null if continue to reconnect
if(keepTrying) setConnected(false);
}
}
}
}catch(IOException io){
debug(debugmsg+"createRemoteClientConnection failure: "+ io.getClass().getSimpleName()+", "+ io.getMessage());
}
//If we come here, which means that we didn't connect to Remote Client (ServerSocket)
//at the default remotePort within the timeout.
//We should set the connected to false so that the runner will continue try to connect
//to the Remote side at the next possible TCP port remotePort.
setConnected(false);
remotePort = getNextPort(remotePort);
debug(debugmsg+"The remotePort is set to next possible port number '"+remotePort+"'");
//if the port number is exhausted, send a shutdown notification to runner so that runner will stop.
if(remotePort>MAX_SERVER_PORT){
shutdownCause = STATUS_SHUTDOWN_REMOTE_CLIENT;
notifyLocalShutdown(STATUS_SHUTDOWN_REMOTE_CLIENT);
}
return false;
}
/**
* Create the Socket object according to server name and port.
*/
protected void bindToRemoteServer() throws IOException{
controllerRunner = new Socket(remoteHostname, remotePort);
}
/**
* Used Internally. Used by Remote Client side.
* Attempt to createRemoteServerSocket.
* This is only valid in remote client mode. For local controller mode the equivalent
* call would be {@link #createRemoteClientConnection(int)}.
* @return boolean -- Remote ServerSocket successfully connected and ready to accept connections.
* @throws IllegalThreadStateException if the Thread is running in local controller
* mode instead of remote client mode.
*/
private boolean createRemoteServerSocket() throws IllegalThreadStateException{
String debugmsg = TAG+".createRemoteServerSocket(): ";
if(isLocalMode()) throw new IllegalThreadStateException(
"Cannot createRemoteServerSocket when running in local controller mode.");
try{
Enumeration e = NetworkInterface.getNetworkInterfaces();
NetworkInterface net;
String adds;
while(e.hasMoreElements()){
net = (NetworkInterface)e.nextElement();
adds = "NetworkInterface: " + net.getDisplayName();
Enumeration a = net.getInetAddresses();
while(a.hasMoreElements()){
adds+= ", "+ ((InetAddress)a.nextElement()).getHostAddress();
}
debug(debugmsg+adds);
}
boolean keeptrying = true;
while(keeptrying){
try {
debug(debugmsg+"Try to create socket server at port '"+remotePort+"'.");
remoteRunner = new ServerSocket(remotePort);
keeptrying = false;
} catch (IOException e1) {
debug(debugmsg+"Fail to create socket server at port '"+remotePort+"': Exception "+ e1.getMessage());
remotePort = getNextPort(remotePort);
}
}
System.setProperty(ENV_KEY_REMOTE_PORT, String.valueOf(remotePort));
debug(debugmsg+"Remote Runner available on port: "+ remoteRunner.getLocalPort());
return true;
}
catch(Exception x){
x.printStackTrace();
}
return false;
}
/**
* Try to augment the port number by {@value #NEXT_SERVER_PORT_PACE}<br>
*
* @param prevPort int, the previous port number
* @return int, the next port number
* @throws IllegalStateException, if the next port number exceeds the max port number.
*/
public int getNextPort(int prevPort){
if(prevPort+NEXT_SERVER_PORT_PACE > MAX_SERVER_PORT){
throw new IllegalStateException("No more port to use. Modify code to augment the constant MAX_SERVER_PORT.");
}
return prevPort+NEXT_SERVER_PORT_PACE;
}
/**
* For local controller:<br>
* Set the value of field {@link #connected} to indicate if the 'local controller'<br>
* is connected to 'remote client'<br>
*
* For remote client:<br>
* Set the value of field {@link #connected} to indicate if the 'remote client'<br>
* accepts a connection from 'local controller'<br>
*
* If the connected is false, the opened streams will be closed and the Socket<br>
* connection {@link #controllerRunner} will be set to null.<br>
*
* @param connected boolean
* @see #createRemoteClientConnection(int)
*/
private void setConnected(boolean connected){
this.connected = connected;
if(!connected){
//Close all opened streams
closeStreams();
//Set the controllerRunner to null
try{controllerRunner.close();}catch(Exception x){}
controllerRunner = null;
}
}
/**
* @return true if we have a validated 2-way connection.
*/
public boolean isConnected(){
return connected;
}
/**
* Used Internally.
* verify the connected remote socket is controlled by a SocketProtocol running in
* local controller mode.
* @param sTimeout in seconds to wait for proper client response.
* @return true only if this full handshake has completed
* @throws IllegalThreadStateException if this is called from an instance running in local
* controller mode.
* @throws InvalidObjectException if we don't have valid input/output streams.
* @throws IllegalThreadStateException if the Thread is running in local controller
* mode instead of remote client mode.
*/
private boolean verifyControllerClient(int sTimeout) throws IllegalThreadStateException, InvalidObjectException{
String debugmsg = TAG+".verifyControllerClient(): ";
if(isLocalMode()) throw new IllegalThreadStateException(
"Cannot verifyControllerClient when running in local controller mode.");
String result = waitForInput(sTimeout * 1000);
if(result != null){
if(result.startsWith(MSG_PROTOCOL_VERSION_QUERY)){
if(sendResponse(MSG_PROTOCOL_VERSION_QUERY+"="+ protocol)){
debug(debugmsg+"Controller verification has succeeded.");
return true;
}else{
debug(debugmsg+"Failed to send response to the Controller verification prompt!");
}
}else{
debug(debugmsg+"Invalid verification prompt from the Controller: "+ result);
}
}else{
debug(debugmsg+"Client did NOT receive verification prompt from Controller within timeout period.");
}
return false;
}
/** By default, this class only accepts connections from instances using the same
* protocol version. It is possible future subclasses will want to override
* this behavior to accept other--typically older known protocol versions.
* @param protocol
* @return true if our supported protocol version matches the requested version.
*/
public boolean acceptProtocolVersion(int protocol){
return this.protocol == protocol;
}
/**
* Used Internally.
* verify the connected remote socket is controlled by a client knowing the handshake.
* We send MSG_PROTOCOL_VERSION_QUERY, and the client should respond MSG_PROTOCOL_VERSION_QUERY=N.<br>
* Currently version N=1 is supported.
* @param sTimeout in seconds to wait for proper client response.
* @return true if the remote client has successfully validated.
* @see #MSG_PROTOCOL_VERSION_QUERY
* @throws InvalidObjectException if we don't have valid input/output streams.
* @throws IllegalThreadStateException if the Thread is running in remote client
* mode instead of local controller mode.
*/
private boolean verifyRemoteClient(int sTimeout)throws IllegalThreadStateException, InvalidObjectException{
String debugmsg = TAG+".verifyRemoteClient(): ";
if(!isLocalMode()) throw new IllegalThreadStateException(
"Cannot verifyRemoteClient when running in remote client mode.");
// exchange a handshake
boolean result = sendResponse(MSG_PROTOCOL_VERSION_QUERY);
if(result){
String response = waitForInput(sTimeout * 1000);
if(response == null) {
debug(debugmsg+"Remote client did not verify in timeout period.");
return false;
}
if (response.startsWith(MSG_PROTOCOL_VERSION_QUERY)){
String[] split = response.split("=");
try{
int check = Integer.parseInt(split[1]);
if(acceptProtocolVersion(check)){
debug(debugmsg+" client protocol "+ protocol +" connected.");
return true;
}else{
debug(debugmsg+"Remote client protocol "+ check +" is NOT supported.");
}
}catch(Exception x){
debug(debugmsg+"Remote client invalid protocol response format: "+ response);
}
}else{
debug(debugmsg+"Remote client invalid protocol response: "+ response);
}
}else{
debug(debugmsg+"Local Protocol Runner did NOT successfully send prompt to Remote client for verification.");
}
return false;
}
/**
* Simply callse closeStreams
* @see #closeStreams()
*/
public void closeProtocolRunners(){
closeStreams();
}
protected final static String debugprefix = "debug:";
/**
* Listen for UTF-8 encoded content from the connected instance and return it to the caller if
* it is deemed valid.
* <p>
* String content is not considered valid unless/until the End-Of-Message marker is received.
* Without receiving the EOM within the timeout period the routine will consider any received
* content invalid and will subsequently return a null value upon timeout.
* @param msTimeout timeout in milliseconds
* @return received input or null if no input stream available or no valid input
* received in timeout period. The EOM will have already been stripped from the message.
* @throws InvalidObjectException if we have no InputStream connected.
* @see #EOM */
public String waitForInput(long msTimeout)throws InvalidObjectException{
String debugmsg = TAG+".waitForInput(): ";
if(bufferedreader == null) throw new InvalidObjectException("No Remote Input Stream Connected.");
String request = null;
long maxTicks = System.currentTimeMillis();
if(msTimeout > 0 ) maxTicks += msTimeout;
boolean keepTrying = true;
StringBuffer buffer = null;
try{
//wait for some bytes to show up
while(keepTrying && !bufferedreader.ready()){
keepTrying = maxTicks > System.currentTimeMillis();
if(keepTrying) {
try{ Thread.sleep(100);}catch(Exception x){}
}else{
return null;
}
}
//let the buffer start loading
buffer = new StringBuffer();
int ichar;
long activityTimeout = 30000; // milliseconds
long origMax = maxTicks;
maxTicks = System.currentTimeMillis() + activityTimeout;
while((request==null) && (System.currentTimeMillis() < maxTicks)){
while(request == null && bufferedreader.ready()){
ichar = bufferedreader.read();
if(ichar != -1) buffer = buffer.append((char)ichar);
// start the timer over on new bytes
maxTicks = System.currentTimeMillis()+ activityTimeout;
if(buffer.length() >= EOM.length()){
if(EOM.equalsIgnoreCase(buffer.substring(buffer.length()- EOM.length()))){
if(buffer.length() > EOM.length()){
request = buffer.substring(0, buffer.length()-EOM.length());
}else{
// we have received an EOM only!
// clear it and start over.
buffer = new StringBuffer();
maxTicks = origMax;
}
}
}
}
if(request == null){ // reader was not ready
try{ Thread.sleep(100);}catch(Exception x){}
}
}
if(request == null){
debug(debugmsg+"waitForInput Timeout without End Of Message. Input: "+ buffer.toString());
return null;
}
}catch(IOException io){
io.printStackTrace();
}
// avoid debug logging debug messages twice!
if(!(request.indexOf(debugprefix)==0)) debug(debugmsg+"Received client input: "+ request);
return request;
}
/**
* Send UTF-8 encoded message to the connected instance, if any.
* This routine automatically adds the End-Of-Message marker to the message before sending it.
* Thus, callers should not put any End-of_message marker on the message to be sent.
* @param message
* @return true if we sent the message to the connected socket stream without error.
* This should NOT be considered any kind of confirmation that the remote TCP client received the message.
* @throws InvalidObjectException if we have no OutputStream connected.
* @see #EOM
*/
public boolean sendResponse(String message)throws InvalidObjectException{
if(bufferedwriter == null) throw new InvalidObjectException("No remote OutputStream connected.");
try{
bufferedwriter.write(message + EOM);
bufferedwriter.flush();
return true;
}catch(IOException io){
io.printStackTrace();
}
return false;
}
/**
* Attempt to create appropriate local or remote socket connections and attempt to
* connect with the other side.
* @return results of call to isConnected()
* @see #isConnected()
* @see #createRemoteServerSocket()
* @see #acceptControllerConnection(int)
* @see #createRemoteClientConnection(int)
*/
public boolean connectProtocolRunners(){
if(!isConnected()){
if(isLocalMode()){
// local controller mode
createRemoteClientConnection(clientConnectTimeout);
}
else{
// remote Runner mode
if (remoteRunner == null) {
createRemoteServerSocket();
}
if(remoteRunner != null){
acceptControllerConnection(100);
}
}
}
return isConnected();
}
/**
* By default keepAlive is TRUE unless changed.
* @return the keepAlive
*/
public boolean getKeepAlive() {
return keepAlive;
}
/**
* By default keepAlive is TRUE unless changed.
* @param keepAlive the keepAlive to set
*/
public void setKeepAlive(boolean keepAlive) {
this.keepAlive = keepAlive;
}
/**
* The default remote hostname is/was typically the "localhost". This is because the initial use
* and implementation for this protocol was for talking with simulators and emulators
* acting as remote devices on the local machine. Other uses may actually use a remote
* hostname that is NOT on the local machine.
* @return the server hostname on which we expect remote clients to accept connections.
*/
public String getRemoteHostname() {
return remoteHostname;
}
/**
* The default remote hostname is/was typically the "localhost". This is because the initial use
* and implementation for this protocol was for talking with simulators and emulators
* acting as remote devices on the local machine. Other uses may actually use a remote
* hostname that is NOT on the local machine.
* @param the server hostname on which we expect remote clients to accept connections.
*/
public void setRemoteHostname(String hostname) {
remoteHostname = hostname;
}
/**
* The default port currently used for remote client connections--typically port 2410.
* @return the server port on which we expect remote clients to accept connections.
*/
public int getRemotePort() {
return remotePort;
}
/** Provide an alternate port on which the remote client is accepting connections.
* The default port currently used for remote client connections is 2410. In the future
* a broader set of predefined port numbers needs to be established to avoid resource
* contention.
* @param remote server port on which the remote client is accepting connections. */
public void setRemotePort(int port) {
remotePort = port;
}
/**
* The default port currently used for outgoing controller Socket connections is 2411.
* <p>
* Typically Sockets can normally communicate on "any available port". However the initial use
* and implementation for this protocol was for talking with simulators and emulators
* acting as remote devices on the local machine. Thus, it was necessary to be able to
* do port forwarding to specifically forward a known local controller port to the known
* remote client port.
* @return the port on which we expect local controller Socket to communicate.
*/
public int getControllerPort() {
return controllerPort;
}
/** Provide an alternate port on which the local controller should open a Socket.
* The default port currently used for local controller communications is 2411.
* Typically Sockets can normally communicate on "any available port". The initial use
* and implementation for this protocol was for talking with simulators and emulators
* acting as remote devices on the local machine. Thus, it was necessary to be able to
* do port forwarding to specifically forward a known local controller port to the known
* remote client port.
* @param the port on which the local controller Socket will attempt to communicate. */
public void setControllerPort(int port) {
controllerPort = port;
}
/**
* @return the protocol version for the remote connection. Currently, only version 1 is
* known or supported.
* @throws InvalidObjectException if we have not successfully connected to a remote client. */
public int getConnectedProtocol()throws InvalidObjectException{
if(!isConnected()) throw new InvalidObjectException("No Remote Client Connection.");
return protocol;
}
/**
* Default is set at 60 seconds. This value is typically used internally for the call to
* {@link #createRemoteClientConnection(int)}
* @return the current clientConnectTimeout setting. */
public int getClientConnectTimeout() {
return clientConnectTimeout;
}
/**
* Default is set at 60 seconds. This value is typically used internally for the call to
* {@link #createRemoteClientConnection(int)}
* @param clientConnectTimeout in seconds */
public void setClientConnectTimeout(int clientConnectTimeout) {
this.clientConnectTimeout = clientConnectTimeout;
}
}