* i2psnark:

- Fixes when complete except for skipped files
     (ticket #447) status in UI, don't connect outbound,
     disconnect seeds when done
   - More classes pkg private
This commit is contained in:
zzz
2012-05-23 16:36:37 +00:00
parent e27df771aa
commit 580bb5a6fe
7 changed files with 118 additions and 29 deletions

View File

@ -24,7 +24,7 @@ package org.klomp.snark;
/** /**
* Callback used when some peer changes state. * Callback used when some peer changes state.
*/ */
public interface CoordinatorListener interface CoordinatorListener
{ {
/** /**
* Called when the PeerCoordinator notices a change in the state of a peer. * Called when the PeerCoordinator notices a change in the state of a peer.

View File

@ -48,7 +48,7 @@ import org.klomp.snark.dht.DHT;
/** /**
* Coordinates what peer does what. * Coordinates what peer does what.
*/ */
public class PeerCoordinator implements PeerListener class PeerCoordinator implements PeerListener
{ {
private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerCoordinator.class); private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerCoordinator.class);
@ -116,6 +116,12 @@ public class PeerCoordinator implements PeerListener
*/ */
private final List<Piece> wantedPieces; private final List<Piece> wantedPieces;
/** The total number of bytes in wantedPieces, or -1 if not yet known.
* Sync on wantedPieces.
* @since 0.9.1
*/
private long wantedBytes;
/** partial pieces - lock by synching on wantedPieces - TODO store Requests, not PartialPieces */ /** partial pieces - lock by synching on wantedPieces - TODO store Requests, not PartialPieces */
private final List<PartialPiece> partialPieces; private final List<PartialPiece> partialPieces;
@ -171,16 +177,22 @@ public class PeerCoordinator implements PeerListener
} }
} }
// only called externally from Storage after the double-check fails /**
* Only called externally from Storage after the double-check fails.
* Sets wantedBytes too.
*/
public void setWantedPieces() public void setWantedPieces()
{ {
if (metainfo == null || storage == null) if (metainfo == null || storage == null) {
wantedBytes = -1;
return; return;
}
// Make a list of pieces // Make a list of pieces
synchronized(wantedPieces) { synchronized(wantedPieces) {
wantedPieces.clear(); wantedPieces.clear();
BitField bitfield = storage.getBitField(); BitField bitfield = storage.getBitField();
int[] pri = storage.getPiecePriorities(); int[] pri = storage.getPiecePriorities();
long count = 0;
for (int i = 0; i < metainfo.getPieces(); i++) { for (int i = 0; i < metainfo.getPieces(); i++) {
// only add if we don't have and the priority is >= 0 // only add if we don't have and the priority is >= 0
if ((!bitfield.get(i)) && if ((!bitfield.get(i)) &&
@ -189,8 +201,10 @@ public class PeerCoordinator implements PeerListener
if (pri != null) if (pri != null)
p.setPriority(pri[i]); p.setPriority(pri[i]);
wantedPieces.add(p); wantedPieces.add(p);
count += metainfo.getPieceLength(i);
} }
} }
wantedBytes = count;
Collections.shuffle(wantedPieces, _random); Collections.shuffle(wantedPieces, _random);
} }
} }
@ -233,7 +247,9 @@ public class PeerCoordinator implements PeerListener
} }
/** /**
* Returns how many bytes are still needed to get the complete file. * Bytes not yet in storage. Does NOT account for skipped files.
* Not exact (does not adjust for last piece size).
* Returns how many bytes are still needed to get the complete torrent.
* @return -1 if in magnet mode * @return -1 if in magnet mode
*/ */
public long getLeft() public long getLeft()
@ -244,6 +260,15 @@ public class PeerCoordinator implements PeerListener
return ((long) storage.needed()) * metainfo.getPieceLength(0); return ((long) storage.needed()) * metainfo.getPieceLength(0);
} }
/**
* Bytes still wanted. DOES account for skipped files.
* @return exact value. or -1 if no storage yet.
* @since 0.9.1
*/
public long getNeededLength() {
return wantedBytes;
}
/** /**
* Returns the total number of uploaded bytes of all peers. * Returns the total number of uploaded bytes of all peers.
*/ */
@ -330,11 +355,25 @@ public class PeerCoordinator implements PeerListener
return infohash; return infohash;
} }
/**
* Inbound.
* Not halted, peers < max.
* @since 0.9.1
*/
public boolean needPeers() public boolean needPeers()
{ {
return !halted && peers.size() < getMaxConnections(); return !halted && peers.size() < getMaxConnections();
} }
/**
* Outbound.
* Not halted, peers < max, and need pieces.
* @since 0.9.1
*/
public boolean needOutboundPeers() {
return wantedBytes != 0 && needPeers();
}
/** /**
* Reduce max if huge pieces to keep from ooming when leeching * Reduce max if huge pieces to keep from ooming when leeching
* @return 512K: 16; 1M: 11; 2M: 6 * @return 512K: 16; 1M: 11; 2M: 6
@ -472,7 +511,10 @@ public class PeerCoordinator implements PeerListener
return null; return null;
} }
// returns true if actual attempt to add peer occurs /**
* Add peer (inbound or outbound)
* @return true if actual attempt to add peer occurs
*/
public boolean addPeer(final Peer peer) public boolean addPeer(final Peer peer)
{ {
if (halted) if (halted)
@ -755,6 +797,7 @@ public class PeerCoordinator implements PeerListener
if (!want.get(i)) { if (!want.get(i)) {
Piece piece = new Piece(i); Piece piece = new Piece(i);
wantedPieces.add(piece); wantedPieces.add(piece);
wantedBytes += metainfo.getPieceLength(i);
// As connections are already up, new Pieces will // As connections are already up, new Pieces will
// not have their PeerID list populated, so do that. // not have their PeerID list populated, so do that.
for (Peer p : peers) { for (Peer p : peers) {
@ -777,6 +820,7 @@ public class PeerCoordinator implements PeerListener
} else { } else {
iter.remove(); iter.remove();
toCancel.add(p); toCancel.add(p);
wantedBytes -= metainfo.getPieceLength(p.getId());
} }
} }
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
@ -910,11 +954,14 @@ public class PeerCoordinator implements PeerListener
throw new RuntimeException(msg, ioe); throw new RuntimeException(msg, ioe);
} }
wantedPieces.remove(p); wantedPieces.remove(p);
wantedBytes -= metainfo.getPieceLength(p.getId());
} }
// just in case // just in case
removePartialPiece(piece); removePartialPiece(piece);
boolean done = wantedBytes <= 0;
// Announce to the world we have it! // Announce to the world we have it!
// Disconnect from other seeders when we get the last piece // Disconnect from other seeders when we get the last piece
List<Peer> toDisconnect = new ArrayList(); List<Peer> toDisconnect = new ArrayList();
@ -924,7 +971,7 @@ public class PeerCoordinator implements PeerListener
Peer p = it.next(); Peer p = it.next();
if (p.isConnected()) if (p.isConnected())
{ {
if (completed() && p.isCompleted()) if (done && p.isCompleted())
toDisconnect.add(p); toDisconnect.add(p);
else else
p.have(piece); p.have(piece);
@ -937,7 +984,11 @@ public class PeerCoordinator implements PeerListener
p.disconnect(true); p.disconnect(true);
} }
if (completed()) { if (done) {
// put msg on the console if partial, since Storage won't do it
if (!completed())
snark.storageCompleted(storage);
synchronized (partialPieces) { synchronized (partialPieces) {
for (PartialPiece ppp : partialPieces) { for (PartialPiece ppp : partialPieces) {
ppp.release(); ppp.release();
@ -1262,11 +1313,12 @@ public class PeerCoordinator implements PeerListener
} }
/** /**
* Get peers from PEX -
* PeerListener callback * PeerListener callback
* @since 0.8.4 * @since 0.8.4
*/ */
public void gotPeers(Peer peer, List<PeerID> peers) { public void gotPeers(Peer peer, List<PeerID> peers) {
if (completed() || !needPeers()) if (!needOutboundPeers())
return; return;
Destination myDest = _util.getMyDestination(); Destination myDest = _util.getMyDestination();
if (myDest == null) if (myDest == null)

View File

@ -782,6 +782,7 @@ public class Snark
} }
/** /**
* Bytes not yet in storage. Does NOT account for skipped files.
* @return exact value. or -1 if no storage yet. * @return exact value. or -1 if no storage yet.
* getNeeded() * pieceLength(0) isn't accurate if last piece * getNeeded() * pieceLength(0) isn't accurate if last piece
* is still needed. * is still needed.
@ -802,6 +803,20 @@ public class Snark
} }
/** /**
* Bytes still wanted. DOES account for skipped files.
* FIXME -1 when not running.
* @return exact value. or -1 if no storage yet or when not running.
* @since 0.9.1
*/
public long getNeededLength() {
PeerCoordinator coord = coordinator;
if (coord != null)
return coord.getNeededLength();
return -1;
}
/**
* Does not account for skipped files.
* @return number of pieces still needed (magnet mode or not), or -1 if unknown * @return number of pieces still needed (magnet mode or not), or -1 if unknown
* @since 0.8.4 * @since 0.8.4
*/ */

View File

@ -338,7 +338,8 @@ public class Storage
} }
/** /**
* Must call setPiecePriorities() after calling this * Must call Snark.updatePiecePriorities()
* (which calls getPiecePriorities()) after calling this.
* @param file canonical path (non-directory) * @param file canonical path (non-directory)
* @param pri default 0; <0 to disable * @param pri default 0; <0 to disable
* @since 0.8.1 * @since 0.8.1

View File

@ -23,7 +23,7 @@ package org.klomp.snark;
/** /**
* Callback used when Storage changes. * Callback used when Storage changes.
*/ */
public interface StorageListener interface StorageListener
{ {
/** /**
* Called when the storage creates a new file of a given length. * Called when the storage creates a new file of a given length.

View File

@ -72,7 +72,7 @@ public class TrackerClient extends I2PAppThread
private boolean stop; private boolean stop;
private boolean started; private boolean started;
private List trackers; private List<Tracker> trackers;
/** /**
* @param meta null if in magnet mode * @param meta null if in magnet mode
@ -260,7 +260,7 @@ public class TrackerClient extends I2PAppThread
for (Iterator iter = trackers.iterator(); iter.hasNext(); ) { for (Iterator iter = trackers.iterator(); iter.hasNext(); ) {
Tracker tr = (Tracker)iter.next(); Tracker tr = (Tracker)iter.next();
if ((!stop) && (!tr.stop) && if ((!stop) && (!tr.stop) &&
(completed || coordinator.needPeers()) && (completed || coordinator.needOutboundPeers()) &&
(event.equals(COMPLETED_EVENT) || System.currentTimeMillis() > tr.lastRequestTime + tr.interval)) (event.equals(COMPLETED_EVENT) || System.currentTimeMillis() > tr.lastRequestTime + tr.interval))
{ {
try try
@ -292,7 +292,7 @@ public class TrackerClient extends I2PAppThread
} }
} }
if ( (left != 0) && (!completed) ) { if (coordinator.needOutboundPeers()) {
// we only want to talk to new people if we need things // we only want to talk to new people if we need things
// from them (duh) // from them (duh)
List<Peer> ordered = new ArrayList(peers); List<Peer> ordered = new ArrayList(peers);
@ -341,7 +341,7 @@ public class TrackerClient extends I2PAppThread
} // *** end of trackers loop here } // *** end of trackers loop here
// Get peers from PEX // Get peers from PEX
if (left > 0 && coordinator.needPeers() && (meta == null || !meta.isPrivate()) && !stop) { if (coordinator.needOutboundPeers() && (meta == null || !meta.isPrivate()) && !stop) {
Set<PeerID> pids = coordinator.getPEXPeers(); Set<PeerID> pids = coordinator.getPEXPeers();
if (!pids.isEmpty()) { if (!pids.isEmpty()) {
_util.debug("Got " + pids.size() + " from PEX", Snark.INFO); _util.debug("Got " + pids.size() + " from PEX", Snark.INFO);
@ -365,7 +365,7 @@ public class TrackerClient extends I2PAppThread
// FIXME this needs to be in its own thread // FIXME this needs to be in its own thread
if (_util.getDHT() != null && (meta == null || !meta.isPrivate()) && !stop) { if (_util.getDHT() != null && (meta == null || !meta.isPrivate()) && !stop) {
int numwant; int numwant;
if (left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needPeers()) if (event.equals(STOPPED_EVENT) || !coordinator.needOutboundPeers())
numwant = 1; numwant = 1;
else else
numwant = _util.getMaxConnections(); numwant = _util.getMaxConnections();
@ -459,7 +459,7 @@ public class TrackerClient extends I2PAppThread
if (! event.equals(NO_EVENT)) if (! event.equals(NO_EVENT))
buf.append("&event=").append(event); buf.append("&event=").append(event);
buf.append("&numwant="); buf.append("&numwant=");
if (left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needPeers()) if (left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needOutboundPeers())
buf.append('0'); buf.append('0');
else else
buf.append(_util.getMaxConnections()); buf.append(_util.getMaxConnections());

View File

@ -890,14 +890,19 @@ public class I2PSnarkServlet extends DefaultServlet {
} }
} }
long total = snark.getTotalLength(); long total = snark.getTotalLength();
// includes skipped files, -1 for magnet mode
long remaining = snark.getRemainingLength(); long remaining = snark.getRemainingLength();
if (remaining > total) if (remaining > total)
remaining = total; remaining = total;
// does not include skipped files, -1 for magnet mode or when not running.
long needed = snark.getNeededLength();
if (needed > total)
needed = total;
long downBps = snark.getDownloadRate(); long downBps = snark.getDownloadRate();
long upBps = snark.getUploadRate(); long upBps = snark.getUploadRate();
long remainingSeconds; long remainingSeconds;
if (downBps > 0) if (downBps > 0 && needed > 0)
remainingSeconds = remaining / downBps; remainingSeconds = needed / downBps;
else else
remainingSeconds = -1; remainingSeconds = -1;
boolean isRunning = !snark.isStopped(); boolean isRunning = !snark.isStopped();
@ -938,18 +943,31 @@ public class I2PSnarkServlet extends DefaultServlet {
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "trackererror.png\" title=\"" + err + "\"></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Tracker Error") + statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "trackererror.png\" title=\"" + err + "\"></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Tracker Error") +
"<br>" + err; "<br>" + err;
} }
} else if (remaining == 0) { // < 0 means no meta size yet } else if (remaining == 0 || needed == 0) { // < 0 means no meta size yet
if (isRunning && curPeers > 0 && !showPeers) // partial complete or seeding
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "seeding.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Seeding") + if (isRunning) {
String img;
String txt;
if (remaining == 0) {
img = "seeding";
txt = _("Seeding");
} else {
// partial
img = "complete";
txt = _("Complete");
}
if (curPeers > 0 && !showPeers)
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + img + ".png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + txt +
": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + "\">" + ": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + "\">" +
curPeers + thinsp(noThinsp) + curPeers + thinsp(noThinsp) +
ngettext("1 peer", "{0} peers", knownPeers) + "</a>"; ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
else if (isRunning) else
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "seeding.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Seeding") + statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + img + ".png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + txt +
": " + curPeers + thinsp(noThinsp) + ": " + curPeers + thinsp(noThinsp) +
ngettext("1 peer", "{0} peers", knownPeers); ngettext("1 peer", "{0} peers", knownPeers);
else } else {
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "complete.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Complete"); statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "complete.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Complete");
}
} else { } else {
if (isRunning && curPeers > 0 && downBps > 0 && !showPeers) if (isRunning && curPeers > 0 && downBps > 0 && !showPeers)
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "downloading.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("OK") + statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "downloading.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("OK") +
@ -1062,7 +1080,7 @@ public class I2PSnarkServlet extends DefaultServlet {
out.write(formatSize(uploaded)); out.write(formatSize(uploaded));
out.write("</td>\n\t"); out.write("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentRateDown\">"); out.write("<td align=\"right\" class=\"snarkTorrentRateDown\">");
if(isRunning && remaining != 0) if(isRunning && needed > 0)
out.write(formatSize(downBps) + "ps"); out.write(formatSize(downBps) + "ps");
out.write("</td>\n\t"); out.write("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentRateUp\">"); out.write("<td align=\"right\" class=\"snarkTorrentRateUp\">");
@ -1108,7 +1126,7 @@ public class I2PSnarkServlet extends DefaultServlet {
out.write("\" onclick=\"if (!confirm('"); out.write("\" onclick=\"if (!confirm('");
// Can't figure out how to escape double quotes inside the onclick string. // Can't figure out how to escape double quotes inside the onclick string.
// Single quotes in translate strings with parameters must be doubled. // Single quotes in translate strings with parameters must be doubled.
// Then the remaining single quite must be escaped // Then the remaining single quote must be escaped
out.write(_("Are you sure you want to delete the file \\''{0}.torrent\\'' (downloaded data will not be deleted) ?", fullFilename)); out.write(_("Are you sure you want to delete the file \\''{0}.torrent\\'' (downloaded data will not be deleted) ?", fullFilename));
out.write("')) { return false; }\""); out.write("')) { return false; }\"");
out.write(" src=\"" + _imgPath + "remove.png\" alt=\""); out.write(" src=\"" + _imgPath + "remove.png\" alt=\"");
@ -1127,7 +1145,7 @@ public class I2PSnarkServlet extends DefaultServlet {
out.write("\" onclick=\"if (!confirm('"); out.write("\" onclick=\"if (!confirm('");
// Can't figure out how to escape double quotes inside the onclick string. // Can't figure out how to escape double quotes inside the onclick string.
// Single quotes in translate strings with parameters must be doubled. // Single quotes in translate strings with parameters must be doubled.
// Then the remaining single quite must be escaped // Then the remaining single quote must be escaped
out.write(_("Are you sure you want to delete the torrent \\''{0}\\'' and all downloaded data?", fullFilename)); out.write(_("Are you sure you want to delete the torrent \\''{0}\\'' and all downloaded data?", fullFilename));
out.write("')) { return false; }\""); out.write("')) { return false; }\"");
out.write(" src=\"" + _imgPath + "delete.png\" alt=\""); out.write(" src=\"" + _imgPath + "delete.png\" alt=\"");
@ -1194,7 +1212,7 @@ public class I2PSnarkServlet extends DefaultServlet {
out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">"); out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">");
out.write("</td>\n\t"); out.write("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">"); out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">");
if (remaining > 0) { if (needed > 0) {
if (peer.isInteresting() && !peer.isChoked()) { if (peer.isInteresting() && !peer.isChoked()) {
out.write("<span class=\"unchoked\">"); out.write("<span class=\"unchoked\">");
out.write(formatSize(peer.getDownloadRate()) + "ps</span>"); out.write(formatSize(peer.getDownloadRate()) + "ps</span>");
@ -1886,6 +1904,9 @@ public class I2PSnarkServlet extends DefaultServlet {
else else
buf.append("<br>").append(_("Complete")); buf.append("<br>").append(_("Complete"));
// else unknown // else unknown
long needed = snark.getNeededLength();
if (needed > 0)
buf.append("<br>").append(_("Remaining")).append(": ").append(formatSize(needed));
buf.append("<br>").append(_("Size")).append(": ").append(formatSize(snark.getTotalLength())); buf.append("<br>").append(_("Size")).append(": ").append(formatSize(snark.getTotalLength()));
MetaInfo meta = snark.getMetaInfo(); MetaInfo meta = snark.getMetaInfo();
if (meta != null) { if (meta != null) {