* 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.
*/
public interface CoordinatorListener
interface CoordinatorListener
{
/**
* 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.
*/
public class PeerCoordinator implements PeerListener
class PeerCoordinator implements PeerListener
{
private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerCoordinator.class);
@ -116,6 +116,12 @@ public class PeerCoordinator implements PeerListener
*/
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 */
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()
{
if (metainfo == null || storage == null)
if (metainfo == null || storage == null) {
wantedBytes = -1;
return;
}
// Make a list of pieces
synchronized(wantedPieces) {
wantedPieces.clear();
BitField bitfield = storage.getBitField();
int[] pri = storage.getPiecePriorities();
long count = 0;
for (int i = 0; i < metainfo.getPieces(); i++) {
// only add if we don't have and the priority is >= 0
if ((!bitfield.get(i)) &&
@ -189,8 +201,10 @@ public class PeerCoordinator implements PeerListener
if (pri != null)
p.setPriority(pri[i]);
wantedPieces.add(p);
count += metainfo.getPieceLength(i);
}
}
wantedBytes = count;
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
*/
public long getLeft()
@ -244,6 +260,15 @@ public class PeerCoordinator implements PeerListener
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.
*/
@ -330,10 +355,24 @@ public class PeerCoordinator implements PeerListener
return infohash;
}
/**
* Inbound.
* Not halted, peers < max.
* @since 0.9.1
*/
public boolean needPeers()
{
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
@ -472,7 +511,10 @@ public class PeerCoordinator implements PeerListener
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)
{
if (halted)
@ -755,6 +797,7 @@ public class PeerCoordinator implements PeerListener
if (!want.get(i)) {
Piece piece = new Piece(i);
wantedPieces.add(piece);
wantedBytes += metainfo.getPieceLength(i);
// As connections are already up, new Pieces will
// not have their PeerID list populated, so do that.
for (Peer p : peers) {
@ -777,6 +820,7 @@ public class PeerCoordinator implements PeerListener
} else {
iter.remove();
toCancel.add(p);
wantedBytes -= metainfo.getPieceLength(p.getId());
}
}
if (_log.shouldLog(Log.DEBUG))
@ -910,11 +954,14 @@ public class PeerCoordinator implements PeerListener
throw new RuntimeException(msg, ioe);
}
wantedPieces.remove(p);
wantedBytes -= metainfo.getPieceLength(p.getId());
}
// just in case
removePartialPiece(piece);
boolean done = wantedBytes <= 0;
// Announce to the world we have it!
// Disconnect from other seeders when we get the last piece
List<Peer> toDisconnect = new ArrayList();
@ -924,7 +971,7 @@ public class PeerCoordinator implements PeerListener
Peer p = it.next();
if (p.isConnected())
{
if (completed() && p.isCompleted())
if (done && p.isCompleted())
toDisconnect.add(p);
else
p.have(piece);
@ -937,7 +984,11 @@ public class PeerCoordinator implements PeerListener
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) {
for (PartialPiece ppp : partialPieces) {
ppp.release();
@ -1262,11 +1313,12 @@ public class PeerCoordinator implements PeerListener
}
/**
* Get peers from PEX -
* PeerListener callback
* @since 0.8.4
*/
public void gotPeers(Peer peer, List<PeerID> peers) {
if (completed() || !needPeers())
if (!needOutboundPeers())
return;
Destination myDest = _util.getMyDestination();
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.
* getNeeded() * pieceLength(0) isn't accurate if last piece
* 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
* @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 pri default 0; <0 to disable
* @since 0.8.1

View File

@ -23,7 +23,7 @@ package org.klomp.snark;
/**
* Callback used when Storage changes.
*/
public interface StorageListener
interface StorageListener
{
/**
* 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 started;
private List trackers;
private List<Tracker> trackers;
/**
* @param meta null if in magnet mode
@ -260,7 +260,7 @@ public class TrackerClient extends I2PAppThread
for (Iterator iter = trackers.iterator(); iter.hasNext(); ) {
Tracker tr = (Tracker)iter.next();
if ((!stop) && (!tr.stop) &&
(completed || coordinator.needPeers()) &&
(completed || coordinator.needOutboundPeers()) &&
(event.equals(COMPLETED_EVENT) || System.currentTimeMillis() > tr.lastRequestTime + tr.interval))
{
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
// from them (duh)
List<Peer> ordered = new ArrayList(peers);
@ -341,7 +341,7 @@ public class TrackerClient extends I2PAppThread
} // *** end of trackers loop here
// 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();
if (!pids.isEmpty()) {
_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
if (_util.getDHT() != null && (meta == null || !meta.isPrivate()) && !stop) {
int numwant;
if (left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needPeers())
if (event.equals(STOPPED_EVENT) || !coordinator.needOutboundPeers())
numwant = 1;
else
numwant = _util.getMaxConnections();
@ -459,7 +459,7 @@ public class TrackerClient extends I2PAppThread
if (! event.equals(NO_EVENT))
buf.append("&event=").append(event);
buf.append("&numwant=");
if (left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needPeers())
if (left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needOutboundPeers())
buf.append('0');
else
buf.append(_util.getMaxConnections());

View File

@ -890,14 +890,19 @@ public class I2PSnarkServlet extends DefaultServlet {
}
}
long total = snark.getTotalLength();
// includes skipped files, -1 for magnet mode
long remaining = snark.getRemainingLength();
if (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 upBps = snark.getUploadRate();
long remainingSeconds;
if (downBps > 0)
remainingSeconds = remaining / downBps;
if (downBps > 0 && needed > 0)
remainingSeconds = needed / downBps;
else
remainingSeconds = -1;
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") +
"<br>" + err;
}
} else if (remaining == 0) { // < 0 means no meta size yet
if (isRunning && curPeers > 0 && !showPeers)
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "seeding.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Seeding") +
} else if (remaining == 0 || needed == 0) { // < 0 means no meta size yet
// partial complete or 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()) + "\">" +
curPeers + thinsp(noThinsp) +
ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
else if (isRunning)
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "seeding.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Seeding") +
else
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + img + ".png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + txt +
": " + curPeers + thinsp(noThinsp) +
ngettext("1 peer", "{0} peers", knownPeers);
else
} else {
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "complete.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Complete");
}
} else {
if (isRunning && curPeers > 0 && downBps > 0 && !showPeers)
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("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentRateDown\">");
if(isRunning && remaining != 0)
if(isRunning && needed > 0)
out.write(formatSize(downBps) + "ps");
out.write("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentRateUp\">");
@ -1108,7 +1126,7 @@ public class I2PSnarkServlet extends DefaultServlet {
out.write("\" onclick=\"if (!confirm('");
// Can't figure out how to escape double quotes inside the onclick string.
// 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("')) { return false; }\"");
out.write(" src=\"" + _imgPath + "remove.png\" alt=\"");
@ -1127,7 +1145,7 @@ public class I2PSnarkServlet extends DefaultServlet {
out.write("\" onclick=\"if (!confirm('");
// Can't figure out how to escape double quotes inside the onclick string.
// 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("')) { return false; }\"");
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>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">");
if (remaining > 0) {
if (needed > 0) {
if (peer.isInteresting() && !peer.isChoked()) {
out.write("<span class=\"unchoked\">");
out.write(formatSize(peer.getDownloadRate()) + "ps</span>");
@ -1886,6 +1904,9 @@ public class I2PSnarkServlet extends DefaultServlet {
else
buf.append("<br>").append(_("Complete"));
// 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()));
MetaInfo meta = snark.getMetaInfo();
if (meta != null) {