diff --git a/apps/i2psnark/java/src/org/klomp/snark/CompleteListener.java b/apps/i2psnark/java/src/org/klomp/snark/CompleteListener.java
index 5e2388aeb5539e2e71c7af36e4953fbc81571d84..77fa9c98b4395c459f4ccbcd626c5123ee941b66 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/CompleteListener.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/CompleteListener.java
@@ -49,6 +49,11 @@ public interface CompleteListener {
      */
     public void addMessage(Snark snark, String message);
 
+    /**
+     * @since 0.9.4
+     */
+    public void gotPiece(Snark snark);
+
     // not really listeners but the easiest way to get back to an optional SnarkManager
     public long getSavedTorrentTime(Snark snark);
     public BitField getSavedTorrentBitField(Snark snark);
diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
index 8ffde7363b9fbcf158d3009e00fbad4be62ec1fa..08c7109ef977dbf354367677d1fc27887d238f5a 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java
@@ -515,8 +515,8 @@ class PeerCoordinator implements PeerListener
             peerCount = peers.size();
             unchokePeer();
 
-            if (listener != null)
-              listener.peerChange(this, peer);
+            //if (listener != null)
+            //  listener.peerChange(this, peer);
           }
       }
     if (toDisconnect != null) {
@@ -652,8 +652,8 @@ class PeerCoordinator implements PeerListener
    */
   public boolean gotHave(Peer peer, int piece)
   {
-    if (listener != null)
-      listener.peerChange(this, peer);
+    //if (listener != null)
+    //  listener.peerChange(this, peer);
 
     synchronized(wantedPieces) {
         for (Piece pc : wantedPieces) {
@@ -672,8 +672,8 @@ class PeerCoordinator implements PeerListener
    */
   public boolean gotBitField(Peer peer, BitField bitfield)
   {
-    if (listener != null)
-      listener.peerChange(this, peer);
+    //if (listener != null)
+    //  listener.peerChange(this, peer);
 
     boolean rv = false;
     synchronized(wantedPieces) {
@@ -919,8 +919,8 @@ class PeerCoordinator implements PeerListener
   {
     uploaded += size;
 
-    if (listener != null)
-      listener.peerChange(this, peer);
+    //if (listener != null)
+    //  listener.peerChange(this, peer);
   }
 
   /**
@@ -930,8 +930,8 @@ class PeerCoordinator implements PeerListener
   {
     downloaded += size;
 
-    if (listener != null)
-      listener.peerChange(this, peer);
+    //if (listener != null)
+    //  listener.peerChange(this, peer);
   }
 
   /**
@@ -1040,8 +1040,8 @@ class PeerCoordinator implements PeerListener
     if (_log.shouldLog(Log.INFO))
       _log.info("Got choke(" + choke + "): " + peer);
 
-    if (listener != null)
-      listener.peerChange(this, peer);
+    //if (listener != null)
+    //  listener.peerChange(this, peer);
   }
 
   public void gotInterest(Peer peer, boolean interest)
@@ -1060,8 +1060,8 @@ class PeerCoordinator implements PeerListener
               }
       }
 
-    if (listener != null)
-      listener.peerChange(this, peer);
+    //if (listener != null)
+    //  listener.peerChange(this, peer);
   }
 
   public void disconnected(Peer peer)
@@ -1081,8 +1081,8 @@ class PeerCoordinator implements PeerListener
         peerCount = peers.size();
       }
 
-    if (listener != null)
-      listener.peerChange(this, peer);
+    //if (listener != null)
+    //  listener.peerChange(this, peer);
   }
   
   /** Called when a peer is removed, to prevent it from being used in 
diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java
index 531f3ddff4dbef973da629a786fd8a43ad06a22c..8a65bd51ed9bdfa2e067b1ff8d0cddf9056b2017 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java
@@ -1115,7 +1115,12 @@ public class Snark
       }
   }
 
+
+  ///////////// Begin StorageListener methods
+
   //private boolean allocating = false;
+
+  /** does nothing */
   public void storageCreateFile(Storage storage, String name, long length)
   {
     //if (allocating)
@@ -1129,6 +1134,7 @@ public class Snark
   // How much storage space has been allocated
   private long allocated = 0;
 
+  /** does nothing */
   public void storageAllocated(Storage storage, long length)
   {
     //allocating = true;
@@ -1140,7 +1146,8 @@ public class Snark
 
   private boolean allChecked = false;
   private boolean checking = false;
-  private boolean prechecking = true;
+  //private boolean prechecking = true;
+
   public void storageChecked(Storage storage, int num, boolean checked)
   {
     //allocating = false;
@@ -1158,6 +1165,8 @@ public class Snark
     if (!checking) {
         if (_log.shouldLog(Log.INFO))
             _log.info("Got " + (checked ? "" : "BAD ") + "piece: " + num);
+        if (completeListener != null)
+            completeListener.gotPiece(this);
     }
   }
 
@@ -1187,6 +1196,9 @@ public class Snark
     coordinator.setWantedPieces();
   }
 
+  ///////////// End StorageListener methods
+
+
   /** SnarkSnutdown callback unused */
   public void shutdown()
   {
diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
index 4399b5f232f8cfdd40e94c79e28cb7c7e1e9ff8c..81d72ee84e0996fafd8d6babd8f31635575a34a8 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java
@@ -1517,6 +1517,12 @@ public class SnarkManager implements CompleteListener {
         addMessage(message);
     }
 
+    /**
+     * A Snark.CompleteListener method.
+     * @since 0.9.4
+     */
+    public void gotPiece(Snark snark) {}
+
     // End Snark.CompleteListeners
 
     /**
diff --git a/apps/i2psnark/java/src/org/klomp/snark/UpdateRunner.java b/apps/i2psnark/java/src/org/klomp/snark/UpdateRunner.java
index bb9906a30a22dda3a8ff6328db6458848d1d8b79..a676f33ab8059d7ee3b37f921e83aeda60f55ff9 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/UpdateRunner.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/UpdateRunner.java
@@ -1,17 +1,15 @@
 package org.klomp.snark;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.net.URI;
 import java.util.List;
-import java.util.StringTokenizer;
 
 import net.i2p.I2PAppContext;
 import net.i2p.crypto.TrustedUpdate;
 import net.i2p.data.DataHelper;
 import net.i2p.update.*;
 import net.i2p.util.Log;
+import net.i2p.util.SimpleTimer2;
 import net.i2p.util.VersionComparator;
 
 /**
@@ -25,15 +23,16 @@ class UpdateRunner implements UpdateTask, CompleteListener {
     private final UpdateManager _umgr;
     private final SnarkManager _smgr;
     private final List<URI> _urls;
-    private final String _updateFile;
     private volatile boolean _isRunning;
+    private volatile boolean _hasMetaInfo;
+    private volatile boolean _isComplete;
     private final String _newVersion;
-    private ByteArrayOutputStream _baos;
     private URI _currentURI;
     private Snark _snark;
-    private boolean _hasMetaInfo;
 
     private static final long MAX_LENGTH = 30*1024*1024;
+    private static final long METAINFO_TIMEOUT = 30*60*1000;
+    private static final long COMPLETE_TIMEOUT = 3*60*60*1000;
 
     public UpdateRunner(I2PAppContext ctx, UpdateManager umgr, SnarkManager smgr,
                         List<URI> uris, String newVersion) { 
@@ -43,7 +42,6 @@ class UpdateRunner implements UpdateTask, CompleteListener {
         _smgr = smgr;
         _urls = uris;
         _newVersion = newVersion;
-        _updateFile = (new File(ctx.getTempDir(), "update" + ctx.random().nextInt() + ".tmp")).getAbsolutePath();
     }
 
     //////// begin UpdateTask methods
@@ -79,7 +77,6 @@ class UpdateRunner implements UpdateTask, CompleteListener {
      *  If it is, get the whole thing.
      */
     private void update() {
-
         if (_urls.isEmpty()) {
             _umgr.notifyTaskFailed(this, "", null);
             return;
@@ -93,9 +90,16 @@ class UpdateRunner implements UpdateTask, CompleteListener {
                 String name = magnet.getName();
                 byte[] ih = magnet.getInfoHash();
                 String trackerURL = magnet.getTrackerURL();
+                if (trackerURL == null && !_smgr.util().shouldUseDHT() &&
+                    !_smgr.util().shouldUseOpenTrackers()) {
+                    // but won't we use OT as a failsafe even if disabled?
+                    _umgr.notifyAttemptFailed(this, "No tracker, no DHT, no OT", null);
+                    continue;
+                }
                 _snark = _smgr.addMagnet(name, ih, trackerURL, true, true, this);
                 if (_snark != null) {
                     updateStatus("<b>" + _smgr.util().getString("Updating from {0}", updateURL) + "</b>");
+                    new Timeout();
                     break;
                 }
             } catch (IllegalArgumentException iae) {}
@@ -104,6 +108,32 @@ class UpdateRunner implements UpdateTask, CompleteListener {
             fatal("No valid URLs");
     }
 
+    /**
+     *  This will run twice, once at the metainfo timeout and
+     *  once at the complete timeout.
+     */
+    private class Timeout extends SimpleTimer2.TimedEvent {
+        private final long _start = _context.clock().now();
+
+        public Timeout() {
+            super(_context.simpleTimer2(), METAINFO_TIMEOUT);
+        }
+
+        public void timeReached() {
+            if (_isComplete || !_isRunning)
+                return;
+            if (!_hasMetaInfo) {
+                fatal("Metainfo timeout");
+                return;
+            }
+            if (_context.clock().now() - _start >= COMPLETE_TIMEOUT) {
+                fatal("Complete timeout");
+                return;
+            }
+            reschedule(COMPLETE_TIMEOUT - METAINFO_TIMEOUT);
+        }
+    }
+
     private void fatal(String error) {
             if (_snark != null) {
                 if (_hasMetaInfo) {
@@ -138,15 +168,21 @@ class UpdateRunner implements UpdateTask, CompleteListener {
         }
         _umgr.notifyComplete(this, _newVersion, f);
         _smgr.torrentComplete(snark);
+        _isComplete = true;
     }
 
+    /**
+     *  This is called by stopTorrent() among others
+     */
     public void updateStatus(Snark snark) {
-
+        if (snark.isStopped()) {
+            if (!_isComplete)
+                fatal("stopped by user");
+        }
         _smgr.updateStatus(snark);
     }
 
     public String gotMetaInfo(Snark snark) {
-        Storage storage = snark.getStorage();
         MetaInfo info = snark.getMetaInfo();
         if (info.getFiles() != null) {
             fatal("more than 1 file");
@@ -173,6 +209,16 @@ class UpdateRunner implements UpdateTask, CompleteListener {
         _smgr.addMessage(snark, message);
     }
 
+    public void gotPiece(Snark snark) {
+        if (_hasMetaInfo) {
+            long total = snark.getTotalLength();
+            long remaining = snark.getRemainingLength(); 
+            String status = "<b>" + _smgr.util().getString("Updating") + "</b>";
+            _umgr.notifyProgress(this, status, total - remaining, total);
+        }
+        _smgr.gotPiece(snark);
+    }
+
     public long getSavedTorrentTime(Snark snark) {
         return _smgr.getSavedTorrentTime(snark);
     }