package net.i2p.client.streaming.impl;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import net.i2p.I2PAppContext;
import net.i2p.client.I2PSession;
import net.i2p.data.Destination;
import net.i2p.data.SessionKey;
import net.i2p.data.SessionTag;
import net.i2p.data.SigningPrivateKey;
import net.i2p.util.Log;
import net.i2p.util.SimpleTimer2;
/**
* This is the class used for outbound packets.
*
* coordinate local attributes about a packet - send time, ack time, number of
* retries, etc.
*/
class PacketLocal extends Packet implements MessageOutputStream.WriteStatus {
private final I2PAppContext _context;
private final Log _log;
private final Connection _connection;
private final Destination _to;
private SessionKey _keyUsed;
private final long _createdOn;
private final AtomicInteger _numSends = new AtomicInteger();
private volatile long _lastSend;
private long _acceptedOn;
/** LOCKING: this */
private long _ackOn;
private long _cancelledOn;
private final AtomicInteger _nackCount = new AtomicInteger();
private volatile boolean _retransmitted;
private volatile SimpleTimer2.TimedEvent _resendEvent;
/** not bound to a connection */
public PacketLocal(I2PAppContext ctx, Destination to, I2PSession session) {
super(session);
_context = ctx;
_createdOn = ctx.clock().now();
_log = ctx.logManager().getLog(PacketLocal.class);
_to = to;
_connection = null;
_lastSend = -1;
_cancelledOn = -1;
}
/** bound to a connection */
public PacketLocal(I2PAppContext ctx, Destination to, Connection con) {
super(con.getSession());
_context = ctx;
_createdOn = ctx.clock().now();
_log = ctx.logManager().getLog(PacketLocal.class);
_to = to;
_connection = con;
_lastSend = -1;
_cancelledOn = -1;
}
public Destination getTo() { return _to; }
/**
* @deprecated should always return null
*/
@Deprecated
public SessionKey getKeyUsed() { return _keyUsed; }
/**
* @deprecated I2PSession throws out the tags
*/
@Deprecated
public void setKeyUsed(SessionKey key) {
if (key != null)
_log.error("Who is sending tags thru the streaming lib?");
_keyUsed = key;
}
/**
* @deprecated should always return null or an empty set
*/
@Deprecated
public Set<SessionTag> getTagsSent() { return Collections.emptySet(); }
/**
* @deprecated I2PSession throws out the tags
*/
@Deprecated
public void setTagsSent(Set<SessionTag> tags) {
if (tags != null && !tags.isEmpty())
_log.error("Who is sending tags thru the streaming lib? " + tags.size());
/****
if ( (_tagsSent != null) && (!_tagsSent.isEmpty()) && (!tags.isEmpty()) ) {
//int old = _tagsSent.size();
//_tagsSent.addAll(tags);
if (!_tagsSent.equals(tags))
System.out.println("ERROR: dup tags: old=" + _tagsSent.size() + " new=" + tags.size() + " packet: " + toString());
} else {
_tagsSent = tags;
}
****/
}
public boolean shouldSign() {
return isFlagSet(FLAG_SIGNATURE_INCLUDED |
FLAG_SYNCHRONIZE |
FLAG_CLOSE |
FLAG_ECHO);
}
public long getCreatedOn() { return _createdOn; }
public long getLifetime() { return _context.clock().now() - _createdOn; }
public void incrementSends() {
_numSends.incrementAndGet();
_lastSend = _context.clock().now();
}
private void cancelResend() {
SimpleTimer2.TimedEvent ev = _resendEvent;
if (ev != null)
ev.cancel();
}
public void ackReceived() {
final long now = _context.clock().now();
synchronized (this) {
if (_ackOn <= 0)
_ackOn = now;
releasePayload();
notifyAll();
}
cancelResend();
}
public void cancelled() {
synchronized (this) {
_cancelledOn = _context.clock().now();
releasePayload();
notifyAll();
}
cancelResend();
if (_log.shouldLog(Log.DEBUG))
_log.debug("Cancelled! " + toString(), new Exception("cancelled"));
}
public SimpleTimer2.TimedEvent getResendEvent() { return _resendEvent; }
/** how long after packet creation was it acked?
* @return how long after packet creation the packet was ACKed in ms
*/
public synchronized int getAckTime() {
if (_ackOn <= 0)
return -1;
else
return (int)(_ackOn - _createdOn);
}
public int getNumSends() { return _numSends.get(); }
public long getLastSend() { return _lastSend; }
/** @return null if not bound */
public Connection getConnection() { return _connection; }
/**
* Will force a fast restransmit on the 3rd call (FAST_RETRANSMIT_THRESHOLD)
* but only if it's the lowest unacked (see Connection.ResendPacketEvent)
*/
public void incrementNACKs() {
final int cnt = _nackCount.incrementAndGet();
SimpleTimer2.TimedEvent evt = _resendEvent;
if (cnt >= Connection.FAST_RETRANSMIT_THRESHOLD && evt != null && (!_retransmitted) &&
(_numSends.get() == 1 || _lastSend < _context.clock().now() - 4*1000)) { // Don't fast retx if we recently resent it
_retransmitted = true;
evt.reschedule(0);
// the predicate used to be '+', changing to '-' --zab
if (_log.shouldLog(Log.DEBUG)) {
final String log = String.format("%s nacks and retransmits. Criteria: nacks=%d, retransmitted=%b,"+
" numSends=%d, lastSend=%d, now=%d",
toString(), cnt, _retransmitted, _numSends.get(), _lastSend, _context.clock().now());
_log.debug(log);
}
} else if (_log.shouldLog(Log.DEBUG)) {
final String log = String.format("%s nack but no retransmit. Criteria: nacks=%d, retransmitted=%b,"+
" numSends=%d, lastSend=%d, now=%d",
toString(), cnt, _retransmitted, _numSends.get(), _lastSend, _context.clock().now());
_log.debug(log);
}
}
public int getNACKs() { return _nackCount.get(); }
public void setResendPacketEvent(SimpleTimer2.TimedEvent evt) { _resendEvent = evt; }
/**
* Sign and write the packet to the buffer (starting at the offset) and return
* the number of bytes written.
*
* @param buffer data to be written
* @param offset starting point in the buffer
* @return Count of bytes written
* @throws IllegalStateException if there is data missing or otherwise b0rked
* @since 0.9.20 moved from Packet
*/
public int writeSignedPacket(byte buffer[], int offset) throws IllegalStateException {
setFlag(FLAG_SIGNATURE_INCLUDED);
SigningPrivateKey key = _session.getPrivateKey();
int size = writePacket(buffer, offset, key.getType().getSigLen());
_optionSignature = _context.dsa().sign(buffer, offset, size, key);
if (_optionSignature == null)
throw new IllegalStateException("Signature failed");
//if (false) {
// Log l = ctx.logManager().getLog(Packet.class);
// l.error("Signing: " + toString());
// l.error(Base64.encode(buffer, 0, size));
// l.error("Signature: " + Base64.encode(_optionSignature.getData()));
//}
// jump into the signed data and inject the signature where we
// previously placed a bunch of zeroes
int signatureOffset = offset
//+ 4 // sendStreamId
//+ 4 // receiveStreamId
//+ 4 // sequenceNum
//+ 4 // ackThrough
//+ 1 // resendDelay
//+ 2 // flags
//+ 2 // optionSize
+ 21
+ (_nacks != null ? 4*_nacks.length + 1 : 1)
+ (isFlagSet(FLAG_DELAY_REQUESTED) ? 2 : 0)
+ (isFlagSet(FLAG_FROM_INCLUDED) ? _optionFrom.size() : 0)
+ (isFlagSet(FLAG_MAX_PACKET_SIZE_INCLUDED) ? 2 : 0);
System.arraycopy(_optionSignature.getData(), 0, buffer, signatureOffset, _optionSignature.length());
return size;
}
@Override
public StringBuilder formatAsString() {
StringBuilder buf = super.formatAsString();
//if ( (_tagsSent != null) && (!_tagsSent.isEmpty()) )
// buf.append(" with tags");
final int nackCount = _nackCount.get();
if (nackCount > 0)
buf.append(" nacked ").append(nackCount).append(" times");
synchronized(this) {
if (_ackOn > 0)
buf.append(" ack after ").append(getAckTime());
}
int numSends = _numSends.get();
if (numSends > 1)
buf.append(" sent ").append(numSends).append(" times");
if (isFlagSet(FLAG_SYNCHRONIZE |
FLAG_CLOSE |
FLAG_RESET)) {
Connection con = _connection;
if (con != null) {
buf.append(" from ");
Destination local = _session.getMyDestination();
if (local != null)
buf.append(local.calculateHash().toBase64().substring(0,4));
else
buf.append("unknown");
buf.append(" to ");
Destination remote = con.getRemotePeer();
if (remote != null)
buf.append(remote.calculateHash().toBase64().substring(0,4));
else
buf.append("unknown");
}
}
return buf;
}
////// begin WriteStatus methods
/**
* Blocks until outbound window is not full. See Connection.packetSendChoke().
* @param maxWaitMs MessageOutputStream is the only caller, generally with -1
*/
public void waitForAccept(int maxWaitMs) throws IOException, InterruptedException {
long before = _context.clock().now();
boolean accepted = false;
try {
// throws IOE or IE
accepted = _connection.packetSendChoke(maxWaitMs);
} finally {
if (accepted) {
_acceptedOn = _context.clock().now();
} else {
_acceptedOn = -1;
releasePayload();
}
if ( (_acceptedOn - before > 1000) && (_log.shouldLog(Log.DEBUG)) ) {
int queued = _connection.getUnackedPacketsSent();
int window = _connection.getOptions().getWindowSize();
int afterQueued = _connection.getUnackedPacketsSent();
_log.debug("Took " + (_acceptedOn - before) + "ms to get "
+ (accepted ? "accepted" : "rejected")
+ (_cancelledOn > 0 ? " and CANCELLED" : "")
+ ", queued behind " + queued +" with a window size of " + window
+ ", finally accepted with " + afterQueued + " queued: "
+ toString());
}
}
}
/** block until the packet is acked from the far end */
public void waitForCompletion(int maxWaitMs) throws IOException, InterruptedException {
long expiration = _context.clock().now()+maxWaitMs;
try {
while (true) {
long timeRemaining = expiration - _context.clock().now();
if ( (timeRemaining <= 0) && (maxWaitMs > 0) ) break;
synchronized (this) {
if (_ackOn > 0) break;
if (!_connection.getIsConnected())
throw new IOException("disconnected");
if (_cancelledOn > 0)
throw new IOException("cancelled");
if (timeRemaining > 60*1000)
timeRemaining = 60*1000;
else if (timeRemaining <= 0)
timeRemaining = 10*1000;
wait(timeRemaining);
}
}
} finally {
if (!writeSuccessful())
releasePayload();
}
}
public synchronized boolean writeAccepted() { return _acceptedOn > 0 && _cancelledOn <= 0; }
public synchronized boolean writeFailed() { return _cancelledOn > 0; }
public synchronized boolean writeSuccessful() { return _ackOn > 0 && _cancelledOn <= 0; }
////// end WriteStatus methods
/** Generate a pcap/tcpdump-compatible format,
* so we can use standard debugging tools.
*/
public void logTCPDump() {
try {
I2PSocketManagerFull.pcapWriter.write(this);
} catch (IOException ioe) {
_log.warn("pcap write ioe: " + ioe);
}
}
}