From 580bb5a6fec7c0daaaf8ac11d0f440b5ae40ade5 Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Wed, 23 May 2012 16:36:37 +0000
Subject: [PATCH]  * 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

---
 .../org/klomp/snark/CoordinatorListener.java  |  2 +-
 .../src/org/klomp/snark/PeerCoordinator.java  | 68 ++++++++++++++++---
 .../java/src/org/klomp/snark/Snark.java       | 15 ++++
 .../java/src/org/klomp/snark/Storage.java     |  3 +-
 .../src/org/klomp/snark/StorageListener.java  |  2 +-
 .../src/org/klomp/snark/TrackerClient.java    | 12 ++--
 .../org/klomp/snark/web/I2PSnarkServlet.java  | 45 ++++++++----
 7 files changed, 118 insertions(+), 29 deletions(-)

diff --git a/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java b/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java
index 478c17bb50..767979d061 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java
@@ -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.
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
index 7a4e2bc7b0..01260f1831 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
@@ -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)
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java
index 75b6672694..0d8d451a8f 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java
@@ -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
      */
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Storage.java b/apps/i2psnark/java/src/org/klomp/snark/Storage.java
index 467094d7fc..3c8be63846 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Storage.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Storage.java
@@ -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
diff --git a/apps/i2psnark/java/src/org/klomp/snark/StorageListener.java b/apps/i2psnark/java/src/org/klomp/snark/StorageListener.java
index 573e18d7c5..279d958e4d 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/StorageListener.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/StorageListener.java
@@ -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.
diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
index 2ac9cac9aa..d0e4ca8d9b 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java
@@ -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());
diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
index 01dd3a4f01..a8a99887af 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
@@ -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) {
-- 
GitLab