/*
* Copyright 2013-2015 Cel Skeggs
*
* This file is part of the CCRE, the Common Chicken Runtime Engine.
*
* The CCRE is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* The CCRE is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the CCRE. If not, see <http://www.gnu.org/licenses/>.
*/
package ccre.cluck.tcp;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import ccre.cluck.CluckLink;
import ccre.cluck.CluckNode;
import ccre.concurrency.ReporterThread;
import ccre.log.Logger;
import ccre.net.ClientSocket;
import ccre.net.Network;
/**
* A self-maintaining handler for connecting to a specified remote address.
*
* @author skeggsc
*/
public class CluckTCPClient extends ReporterThread {
/**
* The default port to run on.
*/
public static final int DEFAULT_PORT = 80;
/**
* The CluckNode that this connection shares.
*/
protected final CluckNode node;
/**
* The link name for this connection.
*/
protected final String linkName;
/**
* The active remote socket.
*/
private ClientSocket sock;
/**
* The connection remote address.
*/
private String remote;
/**
* A short summary of the current error being experienced by this client.
*/
private String errorSummary;
/**
* The delay between each connection to the server.
*/
private int reconnectDelayMillis = 5000;
/**
* The hint for what the other end of the connection should call this link.
*/
protected final String remoteNameHint;
/**
* Should this client continue running?
*/
private volatile boolean isRunning = true;
/**
* Is this connection currently reconnecting?
*/
private volatile boolean isReconnecting = false;
/**
* Reconnect deadline: we should be reconnecting again by this time,
* hopefully.
*/
private volatile long reconnectDeadline = 0;
/**
* Is this connection currently established?
*/
private volatile boolean isEstablished = false;
/**
* Should this component log anything during normal operation?
*/
private boolean logDuringNormalOperation = true;
/**
* Create a new CluckTCPClient connecting to the specified remote on the
* default port, sharing the specified CluckNode, with the specified link
* name and hint for what the other end should call this link.
*
* @param remote The remote address.
* @param node The shared node.
* @param linkName The link name.
* @param remoteNameHint The hint for what the other end should call this
* link.
*/
public CluckTCPClient(String remote, CluckNode node, String linkName, String remoteNameHint) {
super("cluckcli-" + remote);
this.remote = remote;
this.node = node;
this.linkName = linkName;
this.remoteNameHint = remoteNameHint;
}
/**
* Set the remote address to this specified address.
*
* @param remote The remote address.
*/
public void setRemote(String remote) {
this.remote = remote;
closeActiveConnectionIfAny();
}
/**
* Get the remote address.
*
* @return the remote address.
*/
public String getRemote() {
return remote;
}
/**
* Set whether or not this component should log during normal operation.
*
* @param logDuringNormalOperation if logging should occur normally.
*/
public void setLogDuringNormalOperation(boolean logDuringNormalOperation) {
this.logDuringNormalOperation = logDuringNormalOperation;
}
/**
* End the active connection and don't reconnect.
*/
public void terminate() {
isRunning = false;
closeActiveConnectionIfAny();
}
/**
* Set the delay between times when this client reconnects to the server.
*
* @param millis The positive integer of milliseconds to wait.
* @throws IllegalArgumentException If millis <= 0.
*/
public void setReconnectDelay(int millis) throws IllegalArgumentException {
if (millis <= 0) {
throw new IllegalArgumentException("Reconnection delay must be >= 0.");
}
reconnectDelayMillis = millis;
}
private void closeActiveConnectionIfAny() {
if (sock != null) {
try {
sock.close();
} catch (IOException ex) {
Logger.warning("IO Error while closing connection", ex);
}
}
}
@Override
protected void threadBody() throws IOException, InterruptedException {
try {
while (isRunning) {
long start = System.currentTimeMillis();
if (logDuringNormalOperation) {
Logger.fine("Connecting to " + remote + " at " + start);
}
String postfix = "";
closeActiveConnectionIfAny();
try {
postfix = tryConnection();
} catch (Throwable ex) {
Logger.severe("Uncaught exception in network handler!", ex);
}
pauseBeforeSubsequentCycle(start, postfix);
}
} finally {
isReconnecting = false;
isEstablished = false;
if (sock != null) {
sock.close();
}
}
}
private void pauseBeforeSubsequentCycle(long start, String postfix) throws InterruptedException {
reconnectDeadline = reconnectDelayMillis + start;
long spent = System.currentTimeMillis() - start;
long remaining = reconnectDelayMillis - spent;
if (remaining > 0) {
if (remaining > 500 && logDuringNormalOperation) {
Logger.fine("Waiting " + remaining + " milliseconds before reconnecting." + postfix);
}
Thread.sleep(remaining);
}
}
private String tryConnection() {
String postfix = "";
this.errorSummary = null;
try {
try {
isReconnecting = true;
sock = Network.connectDynPort(remote, DEFAULT_PORT);
DataInputStream din = new DataInputStream(sock.openInputStream());
try {
DataOutputStream dout = new DataOutputStream(sock.openOutputStream());
isEstablished = true;
try {
CluckLink deny = doStart(din, dout, sock);
isReconnecting = false;
doMain(din, dout, sock, deny);
} finally {
dout.close();
}
} finally {
din.close();
}
} catch (IOException ex) {
boolean uhe = "java.net.UnknownHostException".equals(ex.getClass().getName());
this.errorSummary = uhe ? "Unknown Host: " + ex.getMessage() : ex.getMessage();
if (uhe || (ex.getMessage() != null && (ex.getMessage().startsWith("Remote server not available") || ex.getMessage().startsWith("Timed out while connecting")))) {
postfix = " (" + ex.getMessage() + ")";
} else {
Logger.warning("IO Error while handling connection", ex);
}
}
} finally {
isReconnecting = false;
isEstablished = false;
}
return postfix;
}
/**
* Starts a Cluck connection. Handshakes with the remote end, negotiates
* link names, and sets up socket timeouts, the sending thread, and the
* sending queue. Also adds the sending link to the node.
*
* @param din the input stream for the connection.
* @param dout the output stream for the connection.
* @param socket the socket for the connection, to be able to set timeouts.
* @return the established CluckLink.
* @throws IOException if something goes wrong while setting up the
* connection.
*/
protected CluckLink doStart(DataInputStream din, DataOutputStream dout, ClientSocket socket) throws IOException {
CluckProtocol.handleHeader(din, dout, remoteNameHint);
Logger.fine("Connected to " + remote + " at " + System.currentTimeMillis());
CluckProtocol.setTimeoutOnSocket(socket);
CluckLink establishedLink = CluckProtocol.handleSend(dout, linkName, node);
node.notifyNetworkModified(); // Only send here, not on server.
return establishedLink;
}
/**
* Run the "main loop" of receiving data over Cluck. This only takes care of
* receiving - use
* {@link #doStart(DataInputStream, DataOutputStream, ClientSocket)} first
* to set up sending. This needs the CluckLink returned by doStart to run
* properly and avoid network loops.
*
* @param din the input stream for the connection.
* @param dout the output stream for the connection.
* @param socket the socket for the connection, to be able to set timeouts.
* @param deny the established CluckLink returned by
* {@link #doStart(DataInputStream, DataOutputStream, ClientSocket)}.
* @throws IOException if the connection is malformed or fails.
*/
protected void doMain(DataInputStream din, DataOutputStream dout, ClientSocket socket, CluckLink deny) throws IOException {
CluckProtocol.handleRecv(din, linkName, node, deny);
}
/**
* @return if this connection is currently reconnecting
*/
public boolean isReconnecting() {
return isReconnecting;
}
/**
* Get the reconnect deadline - only really useful if isReconnecting() and
* isEstablished() both return false.
*
* @return the reconnect deadline: by when we should be reconnecting again.
*/
public long getReconnectDeadline() {
return reconnectDeadline;
}
/**
* @return if this connection is currently established
*/
public boolean isEstablished() {
return isEstablished;
}
/**
* @return a short summary of the current error being experienced by this
* client.
*/
public String getErrorSummary() {
return errorSummary;
}
}