/*
* Copyright 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 java.net.SocketException;
import java.net.SocketTimeoutException;
import ccre.cluck.CluckConstants;
import ccre.cluck.CluckLink;
import ccre.cluck.CluckNode;
import ccre.drivers.ByteFiddling;
import ccre.log.Logger;
import ccre.net.ClientSocket;
/**
* A CluckTCPClient that traces all traffic that goes through it, for debugging
* purposes.
*
* @author skeggsc
*/
public class TracingCluckTCPClient extends CluckTCPClient {
/**
* A CluckLink that wraps another CluckLink and traces everything that gets
* sent through it.
*
* @author skeggsc
*/
public static class TracingLink implements CluckLink {
private final CluckLink link;
/**
* Create a new TracingLink to wrap an existing CluckLink.
*
* @param link the link to wrap.
*/
public TracingLink(CluckLink link) {
this.link = link;
}
@Override
public boolean send(String dest, String source, byte[] data) {
if (data.length == 0) {
Logger.finest("[LOCAL] SEND " + source + " -> " + dest + ": EMPTY");
} else if (data.length == 1) {
Logger.finest("[LOCAL] SEND " + source + " -> " + dest + ": " + CluckConstants.rmtToString(data[0]));
} else {
Logger.finest("[LOCAL] SEND " + source + " -> " + dest + ": " + CluckConstants.rmtToString(data[0]) + " <" + ByteFiddling.toHex(data, 1, data.length) + ">");
}
return link.send(dest, source, data);
}
}
/**
* Create a new TracingCluckTCPClient 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 TracingCluckTCPClient(String remote, CluckNode node, String linkName, String remoteNameHint) {
super(remote, node, linkName, remoteNameHint);
}
@Override
protected CluckLink doStart(DataInputStream din, DataOutputStream dout, ClientSocket sock) throws IOException {
CluckProtocol.handleHeader(din, dout, remoteNameHint);
Logger.fine("Connected to " + getRemote() + " at " + System.currentTimeMillis());
CluckProtocol.setTimeoutOnSocket(sock);
CluckLink link = CluckProtocol.handleSend(dout, linkName, node);
link = new TracingLink(link);
node.addOrReplaceLink(link, linkName);
node.notifyNetworkModified();// Only send here, not on server.
return link;
}
@Override
protected void doMain(DataInputStream din, DataOutputStream dout, ClientSocket sock, CluckLink denyLink) throws IOException {
try {
boolean expectKeepAlives = false;
long lastReceive = System.currentTimeMillis();
while (true) {
try {
String dest = CluckProtocol.readNullableString(din);
String source = CluckProtocol.readNullableString(din);
byte[] data = new byte[din.readInt()];
long checksumBase = din.readLong();
din.readFully(data);
if (din.readLong() != CluckProtocol.checksum(data, checksumBase)) {
throw new IOException("Checksums did not match!");
}
if (!expectKeepAlives && "KEEPALIVE".equals(dest) && source == null && data.length >= 2 && data[0] == CluckConstants.RMT_NEGATIVE_ACK && data[1] == 0x6D) {
expectKeepAlives = true;
Logger.info("Detected KEEPALIVE message. Expecting future keepalives on " + linkName + ".");
}
source = CluckProtocol.prependLink(linkName, source);
long start = System.currentTimeMillis();
logLocal(dest, source, data);
node.transmit(dest, source, data, denyLink);
long endAt = System.currentTimeMillis();
if (endAt - start > 1000) {
Logger.warning("[LOCAL] Took a long time to process: " + dest + " <- " + source + " of " + (endAt - start) + " ms");
}
lastReceive = System.currentTimeMillis();
} catch (SocketTimeoutException ex) {
if (expectKeepAlives && System.currentTimeMillis() - lastReceive > CluckProtocol.TIMEOUT_PERIOD_MILLIS) {
throw ex;
} else {
// otherwise, don't do anything - we don't know if this
// is a timeout.
}
}
}
} catch (SocketTimeoutException ex) {
Logger.fine("Link timed out: " + linkName);
} catch (SocketException ex) {
if ("Connection reset".equals(ex.getMessage())) {
Logger.fine("Link receiving disconnected: " + linkName);
} else {
throw ex;
}
}
}
private void logLocal(String dest, String source, byte[] data) {
if (data.length == 0) {
Logger.finest("[LOCAL] RECV " + source + " -> " + dest + ": EMPTY");
} else if (data.length == 1) {
Logger.finest("[LOCAL] RECV " + source + " -> " + dest + ": " + CluckConstants.rmtToString(data[0]));
} else if (!dest.equals("KEEPALIVE")) {
Logger.finest("[LOCAL] RECV: " + data.length);
Logger.finest("[LOCAL] RECV " + source + " -> " + dest + ": " + CluckConstants.rmtToString(data[0]) + " <" + ByteFiddling.toHex(data, 1, data.length) + ">");
}
}
}