I2P Address: [http://git.idk.i2p]

Skip to content
Snippets Groups Projects
  • zzz's avatar
    70a2e330
    * i2psnark: · 70a2e330
    zzz authored
       - More DHT limits
       - Announce to backup trackers if DHT is empty
       - Use PEX and DHT info in torrent peer count
       - Don't use temp files for announces
       - TrackerClient refactoring
       - cleanups
    70a2e330
    History
    * i2psnark:
    zzz authored
       - More DHT limits
       - Announce to backup trackers if DHT is empty
       - Use PEX and DHT info in torrent peer count
       - Don't use temp files for announces
       - TrackerClient refactoring
       - cleanups
TrackerClient.java 30.21 KiB
/* TrackerClient - Class that informs a tracker and gets new peers.
   Copyright (C) 2003 Mark J. Wielaard

   This file is part of Snark.
   
   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2, or (at your option)
   any later version.
 
   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.
 
   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software Foundation,
   Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/

package org.klomp.snark;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.Set;

import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.util.I2PAppThread;
import net.i2p.util.Log;
import net.i2p.util.SimpleTimer2;

import org.klomp.snark.bencode.InvalidBEncodingException;
import org.klomp.snark.dht.DHT;

/**
 * Informs metainfo tracker of events and gets new peers for peer
 * coordinator.
 *
 * start() creates a thread and starts it.
 * At the end of each run, a TimedEvent is queued on the SimpleTimer2 queue.
 * The TimedEvent creates a new thread and starts it, so it does not
 * clog SimpleTimer2.
 *
 * The thread runs one pass through the trackers, the PEX, and the DHT,
 * then queues a new TimedEvent and exits.
 *
 * Thus there are only threads that are actively announcing, not one thread per torrent forever.
 *
 * start() may be called again after halt().
 *
 * @author Mark Wielaard (mark@klomp.org)
 */
public class TrackerClient implements Runnable {
  private final Log _log;
  private static final String NO_EVENT = "";
  private static final String STARTED_EVENT = "started";
  private static final String COMPLETED_EVENT = "completed";
  private static final String STOPPED_EVENT = "stopped";
  private static final String NOT_REGISTERED  = "torrent not registered"; //bytemonsoon
  /** this is our equivalent to router.utorrent.com for bootstrap */
  private static final String DEFAULT_BACKUP_TRACKER = "http://tracker.welterde.i2p/a";

  private final static int SLEEP = 5; // 5 minutes.
  private final static int DELAY_MIN = 2000; // 2 secs.
  private final static int DELAY_RAND = 6*1000;
  private final static int MAX_REGISTER_FAILS = 10; // * INITIAL_SLEEP = 15m to register
  private final static int INITIAL_SLEEP = 90*1000;
  private final static int MAX_CONSEC_FAILS = 5;    // slow down after this
  private final static int LONG_SLEEP = 30*60*1000; // sleep a while after lots of fails
  private final static long MIN_TRACKER_ANNOUNCE_INTERVAL = 15*60*1000;
  private final static long MIN_DHT_ANNOUNCE_INTERVAL = 10*60*1000;

  private final I2PSnarkUtil _util;
  private final MetaInfo meta;
  private final String infoHash;
  private final String peerID;
  private final String additionalTrackerURL;
  private final PeerCoordinator coordinator;
  private final Snark snark;
  private final int port;
  private final String _threadName;

  private volatile boolean stop = true;
  private volatile boolean started;
  private volatile boolean _initialized;
  private volatile int _runCount;
  // running thread so it can be interrupted
  private volatile Thread _thread;
  // queued event so it can be cancelled
  private volatile SimpleTimer2.TimedEvent _event;
  // these 2 used in loop()
  private volatile boolean runStarted;
  private volatile  int consecutiveFails;
  private boolean completed;
  private volatile boolean _fastUnannounce;
  private long lastDHTAnnounce;
  private final List<Tracker> trackers;
  private final List<Tracker> backupTrackers;

  /**
   * Call start() to start it.
   *
   * @param meta null if in magnet mode
   * @param additionalTrackerURL may be null, from the ?tr= param in magnet mode, otherwise ignored
   */
  public TrackerClient(I2PSnarkUtil util, MetaInfo meta, String additionalTrackerURL,
                       PeerCoordinator coordinator, Snark snark)
  {
    super();
    // Set unique name.
    String id = urlencode(snark.getID());
    _threadName = "TrackerClient " + id.substring(id.length() - 12);
    _util = util;
    _log = util.getContext().logManager().getLog(TrackerClient.class);
    this.meta = meta;
    this.additionalTrackerURL = additionalTrackerURL;
    this.coordinator = coordinator;
    this.snark = snark;

    this.port = 6881; //(port == -1) ? 9 : port;
    this.infoHash = urlencode(snark.getInfoHash());
    this.peerID = urlencode(snark.getID());
    this.trackers = new ArrayList(2);
    this.backupTrackers = new ArrayList(2);
  }

  public synchronized void start() {
      if (!stop) {
          if (_log.shouldLog(Log.WARN))
              _log.warn("Already started: " + _threadName);
          return;
      }
      stop = false;
      consecutiveFails = 0;
      runStarted = false;
      _fastUnannounce = false;
      _thread = new I2PAppThread(this, _threadName + " #" + (++_runCount), true);
      _thread.start();
      started = true;
  }
  
  public boolean halted() { return stop; }
  public boolean started() { return started; }
  
  /**
   * Interrupts this Thread to stop it.
   * @param fast if true, limit the life of the unannounce threads
   */
  public synchronized void halt(boolean fast) {
    boolean wasStopped = stop;
    if (wasStopped) {
        if (_log.shouldLog(Log.WARN))
            _log.warn("Already stopped: " + _threadName);
    } else {
        if (_log.shouldLog(Log.WARN))
            _log.warn("Stopping: " + _threadName);
        stop = true;
    }
    SimpleTimer2.TimedEvent e = _event;
    if (e != null) {
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("Cancelling next announce " + _threadName);
        e.cancel();
        _event = null;
    }
    Thread t = _thread;
    if (t != null) {
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("Interrupting " + t.getName());
        t.interrupt();
    }
    _fastUnannounce = true;
    if (!wasStopped)
        unannounce();
  }

  private void queueLoop(long delay) {
      _event = new Runner(delay);
  }

  private class Runner extends SimpleTimer2.TimedEvent {
      public Runner(long delay) {
          super(_util.getContext().simpleTimer2(), delay);
      }

      public void timeReached() {
          _event = null;
          _thread = new I2PAppThread(TrackerClient.this, _threadName + " #" + (++_runCount), true);
          _thread.start();
      }
  }

  private boolean verifyConnected() {
    while (!stop && !_util.connected()) {
        boolean ok = _util.connect();
        if (!ok) {
            try { Thread.sleep(30*1000); } catch (InterruptedException ie) {}
        }
    }
    return !stop && _util.connected();
  }
  
  /**
   *  Setup the first time only,
   *  then one pass (usually) through the trackers, PEX, and DHT.
   *  This will take several seconds to several minutes.
   */
  public void run() {
      long begin = _util.getContext().clock().now();
      if (_log.shouldLog(Log.DEBUG))
          _log.debug("Start " + Thread.currentThread().getName());
      try {
          if (!_initialized) {
              setup();
          }
          if (trackers.isEmpty() && _util.getDHT() == null) {
              stop = true;
              this.snark.addMessage(_util.getString("No valid trackers for {0} - enable opentrackers or DHT?",
                                              this.snark.getBaseName()));
              _log.error("No valid trackers for " + this.snark.getBaseName());
              this.snark.stopTorrent();
              return;
          }
          if (!_initialized) {
              _initialized = true;
              // FIXME only when starting everybody at once, not for a single torrent
              long delay = _util.getContext().random().nextInt(30*1000);
              try {
                  Thread.sleep(delay);
              } catch (InterruptedException ie) {}
          }
          loop();
      } finally {
          // don't hold ref
          _thread = null;
          if (_log.shouldLog(Log.DEBUG))
              _log.debug("Finish " + Thread.currentThread().getName() +
                         " after " + DataHelper.formatDuration(_util.getContext().clock().now() - begin));
      }
  }

  /**
   *  Do this one time only (not every time it is started).
   *  @since 0.9.1
   */
  private void setup() {
    // Construct the list of trackers for this torrent,
    // starting with the primary one listed in the metainfo,
    // followed by the secondary open trackers
    // It's painful, but try to make sure if an open tracker is also
    // the primary tracker, that we don't add it twice.
    // todo: check for b32 matches as well
    String primary = null;
    if (meta != null)
        primary = meta.getAnnounce();
    else if (additionalTrackerURL != null)
        primary = additionalTrackerURL;
    if (primary != null) {
        if (isValidAnnounce(primary)) {
            trackers.add(new Tracker(primary, true));
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Announce: [" + primary + "] infoHash: " + infoHash);
        } else {
            if (_log.shouldLog(Log.WARN))
                _log.warn("Skipping invalid or non-i2p announce: " + primary);
        }
    } else {
        _log.warn("No primary announce");
        primary = "";
    }
    if (meta == null || !meta.isPrivate()) {
        List<String> tlist = _util.getOpenTrackers();
        for (int i = 0; i < tlist.size(); i++) {
             String url = tlist.get(i);
             if (!isValidAnnounce(url)) {
                _log.error("Bad announce URL: [" + url + "]");
                continue;
             }
             int slash = url.indexOf('/', 7);
             if (slash <= 7) {
                _log.error("Bad announce URL: [" + url + "]");
                continue;
             }
             if (primary.startsWith(url.substring(0, slash)))
                continue;
             String dest = _util.lookup(url.substring(7, slash));
             if (dest == null) {
                _log.error("Announce host unknown: [" + url.substring(7, slash) + "]");
                continue;
             }
             if (primary.startsWith("http://" + dest))
                continue;
             if (primary.startsWith("http://i2p/" + dest))
                continue;
             // opentrackers are primary if we don't have primary
             trackers.add(new Tracker(url, primary.equals("")));
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("Additional announce: [" + url + "] for infoHash: " + infoHash);
        }
    }

    // backup trackers if DHT needs bootstrapping
    if (trackers.isEmpty() && (meta == null || !meta.isPrivate())) {
        List<String> tlist = _util.getBackupTrackers();
        for (int i = 0; i < tlist.size(); i++) {
             String url = tlist.get(i);
             if (!isValidAnnounce(url)) {
                _log.error("Bad announce URL: [" + url + "]");
                continue;
             }
             int slash = url.indexOf('/', 7);
             if (slash <= 7) {
                _log.error("Bad announce URL: [" + url + "]");
                continue;
             }
             String dest = _util.lookup(url.substring(7, slash));
             if (dest == null) {
                _log.error("Announce host unknown: [" + url.substring(7, slash) + "]");
                continue;
             }
             backupTrackers.add(new Tracker(url, false));
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("Backup announce: [" + url + "] for infoHash: " + infoHash);
        }
        if (backupTrackers.isEmpty())
            backupTrackers.add(new Tracker(DEFAULT_BACKUP_TRACKER, false));
    }
    this.completed = coordinator.getLeft() == 0;
  }

  /**
   *  Announce to all the trackers, get peers from PEX and DHT, then queue up a SimpleTimer2 event.
   *  This will take several seconds to several minutes.
   *  @since 0.9.1
   */
  private void loop() {
    try
      {
        // normally this will only go once, then call queueLoop() and return
        while(!stop)
          {
            if (!verifyConnected()) {
                stop = true;
                return;
            }

            // Local DHT tracker announce
            DHT dht = _util.getDHT();
            if (dht != null && (meta == null || !meta.isPrivate()))
                dht.announce(snark.getInfoHash());

            int maxSeenPeers = 0;
            if (!trackers.isEmpty())
                maxSeenPeers = getPeersFromTrackers(trackers);
            int p = getPeersFromPEX();
            if (p > maxSeenPeers)
                maxSeenPeers = p;
            p = getPeersFromDHT();
            if (p > maxSeenPeers)
                maxSeenPeers = p;
            // backup if DHT needs bootstrapping
            if (trackers.isEmpty() && !backupTrackers.isEmpty() && dht != null && dht.size() < 16) {
                p = getPeersFromTrackers(backupTrackers);
                if (p > maxSeenPeers)
                    maxSeenPeers = p;
            }

            // we could try and total the unique peers but that's too hard for now
            snark.setTrackerSeenPeers(maxSeenPeers);

            if (stop)
                return;

            try {
                // Sleep some minutes...
                // Sleep the minimum interval for all the trackers, but 60s minimum
                int delay;
                Random r = _util.getContext().random();
                int random = r.nextInt(120*1000);
                if (completed && runStarted)
                  delay = 3*SLEEP*60*1000 + random;
                else if (snark.getTrackerProblems() != null && ++consecutiveFails < MAX_CONSEC_FAILS)
                  delay = INITIAL_SLEEP;
                else if ((!runStarted) && _runCount < MAX_CONSEC_FAILS)
                  delay = INITIAL_SLEEP;
                else
                  // sleep a while, when we wake up we will contact only the trackers whose intervals have passed
                  delay = SLEEP*60*1000 + random;

                if (delay > 20*1000) {
                  // put ourselves on SimpleTimer2
                  if (_log.shouldLog(Log.DEBUG))
                      _log.debug("Requeueing in " + DataHelper.formatDuration(delay) + ": " + Thread.currentThread().getName());
                  queueLoop(delay);
                  return;
                } else if (delay > 0) {
                  Thread.sleep(delay);
                }
              } catch(InterruptedException interrupt) {}
          } // *** end of while loop
      } // try
    catch (Throwable t)
      {
        _log.error("TrackerClient: " + t, t);
        if (t instanceof OutOfMemoryError)
            throw (OutOfMemoryError)t;
      }
  }

  /**
   *  @return max peers seen
   */
  private int getPeersFromTrackers(List<Tracker> trckrs) {
            long uploaded = coordinator.getUploaded();
            long downloaded = coordinator.getDownloaded();
            long left = coordinator.getLeft();   // -1 in magnet mode
            
            // First time we got a complete download?
            String event;
            if (!completed && left == 0)
              {
                completed = true;
                event = COMPLETED_EVENT;
              }
            else
              event = NO_EVENT;

            // *** loop once for each tracker
            int maxSeenPeers = 0;
            for (Tracker tr : trckrs) {
              if ((!stop) && (!tr.stop) &&
                  (completed || coordinator.needOutboundPeers() || !tr.started) &&
                  (event.equals(COMPLETED_EVENT) || System.currentTimeMillis() > tr.lastRequestTime + tr.interval))
              {
                try
                  {
                    if (!tr.started)
                      event = STARTED_EVENT;
                    TrackerInfo info = doRequest(tr, infoHash, peerID,
                                                 uploaded, downloaded, left,
                                                 event);

                    snark.setTrackerProblems(null);
                    tr.trackerProblems = null;
                    tr.registerFails = 0;
                    tr.consecutiveFails = 0;
                    if (tr.isPrimary)
                        consecutiveFails = 0;
                    runStarted = true;
                    tr.started = true;

                    Set<Peer> peers = info.getPeers();
                    tr.seenPeers = info.getPeerCount();
                    if (snark.getTrackerSeenPeers() < tr.seenPeers) // update rising number quickly
                        snark.setTrackerSeenPeers(tr.seenPeers);

                    // pass everybody over to our tracker
                    DHT dht = _util.getDHT();
                    if (dht != null) {
                        for (Peer peer : peers) {
                            dht.announce(snark.getInfoHash(), peer.getPeerID().getDestHash());
                        }
                    }

                    if (coordinator.needOutboundPeers()) {
                        // we only want to talk to new people if we need things
                        // from them (duh)
                        List<Peer> ordered = new ArrayList(peers);
                        Random r = _util.getContext().random();
                        Collections.shuffle(ordered, r);
                        Iterator<Peer> it = ordered.iterator();
                        while ((!stop) && it.hasNext() && coordinator.needOutboundPeers()) {
                          Peer cur = it.next();
                          // FIXME if id == us || dest == us continue;
                          // only delay if we actually make an attempt to add peer
                          if(coordinator.addPeer(cur) && it.hasNext()) {
                            int delay = r.nextInt(DELAY_RAND) + DELAY_MIN;
                            try { Thread.sleep(delay); } catch (InterruptedException ie) {}
                          }
                        }
                    }
                  }
                catch (IOException ioe)
                  {
                    // Probably not fatal (if it doesn't last to long...)
                    if (_log.shouldLog(Log.WARN))
                        _log.warn
                      ("WARNING: Could not contact tracker at '"
                       + tr.announce + "': " + ioe);
                    tr.trackerProblems = ioe.getMessage();
                    // don't show secondary tracker problems to the user
                    if (tr.isPrimary)
                      snark.setTrackerProblems(tr.trackerProblems);
                    if (tr.trackerProblems.toLowerCase(Locale.US).startsWith(NOT_REGISTERED)) {
                      // Give a guy some time to register it if using opentrackers too
                      //if (trckrs.size() == 1) {
                      //  stop = true;
                      //  snark.stopTorrent();
                      //} else { // hopefully each on the opentrackers list is really open
                        if (tr.registerFails++ > MAX_REGISTER_FAILS)
                          tr.stop = true;
                      //
                    }
                    if (++tr.consecutiveFails == MAX_CONSEC_FAILS) {
                        tr.seenPeers = 0;
                        if (tr.interval < LONG_SLEEP)
                            tr.interval = LONG_SLEEP;  // slow down
                    }
                  }
              } else {
                  if (_log.shouldLog(Log.INFO))
                      _log.info("Not announcing to " + tr.announce + " last announce was " +
                               new Date(tr.lastRequestTime) + " interval is " + DataHelper.formatDuration(tr.interval));
              }
              if ((!tr.stop) && maxSeenPeers < tr.seenPeers)
                  maxSeenPeers = tr.seenPeers;
            }  // *** end of trackers loop here

            return maxSeenPeers;
  }

  /**
   *  @return max peers seen
   */
  private int getPeersFromPEX() {
            // Get peers from PEX
            int rv = 0;
            if (coordinator.needOutboundPeers() && (meta == null || !meta.isPrivate()) && !stop) {
                Set<PeerID> pids = coordinator.getPEXPeers();
                if (!pids.isEmpty()) {
                    if (_log.shouldLog(Log.INFO))
                        _log.info("Got " + pids.size() + " from PEX");
                    List<Peer> peers = new ArrayList(pids.size());
                    for (PeerID pID : pids) {
                        peers.add(new Peer(pID, snark.getID(), snark.getInfoHash(), snark.getMetaInfo()));
                    }
                    Random r = _util.getContext().random();
                    Collections.shuffle(peers, r);
                    Iterator<Peer> it = peers.iterator();
                    while ((!stop) && it.hasNext() && coordinator.needOutboundPeers()) {
                        Peer cur = it.next();
                        if (coordinator.addPeer(cur) && it.hasNext()) {
                            int delay = r.nextInt(DELAY_RAND) + DELAY_MIN;
                            try { Thread.sleep(delay); } catch (InterruptedException ie) {}
                         }
                    }
                    rv = pids.size();
                }
            } else {
                if (_log.shouldLog(Log.INFO))
                    _log.info("Not getting PEX peers");
            }
            return rv;
    }

  /**
   *  @return max peers seen
   */
  private int getPeersFromDHT() {
            // Get peers from DHT
            // FIXME this needs to be in its own thread
            int rv = 0;
            DHT dht = _util.getDHT();
            if (dht != null && (meta == null || !meta.isPrivate()) && (!stop) &&
                _util.getContext().clock().now() >  lastDHTAnnounce + MIN_DHT_ANNOUNCE_INTERVAL) {
                int numwant;
                if (!coordinator.needOutboundPeers())
                    numwant = 1;
                else
                    numwant = _util.getMaxConnections();
                Collection<Hash> hashes = dht.getPeers(snark.getInfoHash(), numwant, 2*60*1000);
                if (!hashes.isEmpty()) {
                    runStarted = true;
                    lastDHTAnnounce = _util.getContext().clock().now();
                    rv = hashes.size();
                }
                if (_log.shouldLog(Log.INFO))
                    _log.info("Got " + hashes + " from DHT");
                // announce  ourselves while the token is still good
                // FIXME this needs to be in its own thread
                if (!stop) {
                    // announce only to the 1 closest
                    int good = dht.announce(snark.getInfoHash(), 1, 5*60*1000);
                    if (_log.shouldLog(Log.INFO))
                        _log.info("Sent " + good + " good announces to DHT");
                }

                // now try these peers
                if ((!stop) && !hashes.isEmpty()) {
                    List<Peer> peers = new ArrayList(hashes.size());
                    for (Hash h : hashes) {
                        try {
                            PeerID pID = new PeerID(h.getData(), _util);
                            peers.add(new Peer(pID, snark.getID(), snark.getInfoHash(), snark.getMetaInfo()));
                        } catch (InvalidBEncodingException ibe) {}
                    }
                    Random r = _util.getContext().random();
                    Collections.shuffle(peers, r);
                    Iterator<Peer> it = peers.iterator();
                    while ((!stop) && it.hasNext() && coordinator.needOutboundPeers()) {
                        Peer cur = it.next();
                        if (coordinator.addPeer(cur) && it.hasNext()) {
                            int delay = r.nextInt(DELAY_RAND) + DELAY_MIN;
                            try { Thread.sleep(delay); } catch (InterruptedException ie) {}
                         }
                    }
                }
            } else {
                if (_log.shouldLog(Log.INFO))
                    _log.info("Not getting DHT peers");
            }
            return rv;
  }

  /**
   *  Creates a thread for each tracker in parallel if tunnel is still open
   *  @since 0.9.1
   */
  private void unannounce() {
      // Local DHT tracker unannounce
      DHT dht = _util.getDHT();
      if (dht != null)
          dht.unannounce(snark.getInfoHash());
      int i = 0;
      for (Tracker tr : trackers) {
          if (_util.connected() &&
              tr.started && (!tr.stop) && tr.trackerProblems == null) {
              try {
                  (new I2PAppThread(new Unannouncer(tr), _threadName + " U" + (++i), true)).start();
              } catch (OutOfMemoryError oom) {
                  // probably ran out of threads, ignore
                  tr.reset();
              }
          } else {
              tr.reset();
          }
      }
  }

  /**
   *  Send "stopped" to a single tracker
   *  @since 0.9.1
   */
  private class Unannouncer implements Runnable {
     private final Tracker tr;

     public Unannouncer(Tracker tr) {
         this.tr = tr;
     }

     public void run() {
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("Running unannounce " + _threadName + " to " + tr.announce);
        long uploaded = coordinator.getUploaded();
        long downloaded = coordinator.getDownloaded();
        long left = coordinator.getLeft();
        try
          {
            // Don't try to restart I2CP connection just to say goodbye
              if (_util.connected()) {
                  if (tr.started && (!tr.stop) && tr.trackerProblems == null)
                      doRequest(tr, infoHash, peerID, uploaded,
                                         downloaded, left, STOPPED_EVENT);
              }
          }
        catch(IOException ioe) { /* ignored */ }
        tr.reset();
     }
  }
  
  private TrackerInfo doRequest(Tracker tr, String infoHash,
                                String peerID, long uploaded,
                                long downloaded, long left, String event)
    throws IOException
  {
    StringBuilder buf = new StringBuilder(512);
    buf.append(tr.announce);
    if (tr.announce.contains("?"))
        buf.append('&');
    else
        buf.append('?');
    buf.append("info_hash=").append(infoHash)
       .append("&peer_id=").append(peerID)
       .append("&port=").append(port)
       .append("&ip=" ).append(_util.getOurIPString()).append(".i2p")
       .append("&uploaded=").append(uploaded)
       .append("&downloaded=").append(downloaded)
       .append("&left=");
    // What do we send for left in magnet mode? Can we omit it?
    if (left >= 0)
        buf.append(left);
    else
        buf.append('1');
    buf.append("&compact=1");  // NOTE: opentracker will return 400 for &compact alone
    if (! event.equals(NO_EVENT))
        buf.append("&event=").append(event);
    buf.append("&numwant=");
    boolean small = left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needOutboundPeers();
    if (small)
        buf.append('0');
    else
        buf.append(_util.getMaxConnections());
    String s = buf.toString();
    if (_log.shouldLog(Log.INFO))
        _log.info("Sending TrackerClient request: " + s);
      
    tr.lastRequestTime = System.currentTimeMillis();
    // Don't wait for a response to stopped when shutting down
    boolean fast = _fastUnannounce && event.equals(STOPPED_EVENT);
    byte[] fetched = _util.get(s, true, fast ? -1 : 0, small ? 128 : 1024, small ? 1024 : 8*1024);
    if (fetched == null) {
        throw new IOException("Error fetching " + s);
    }
    
        InputStream in = new ByteArrayInputStream(fetched);

        TrackerInfo info = new TrackerInfo(in, snark.getID(),
                                           snark.getInfoHash(), snark.getMetaInfo(), _util);
        if (_log.shouldLog(Log.INFO))
            _log.info("TrackerClient response: " + info);

        String failure = info.getFailureReason();
        if (failure != null)
          throw new IOException(failure);

        tr.interval = Math.max(MIN_TRACKER_ANNOUNCE_INTERVAL, info.getInterval() * 1000l);
        return info;
  }

  /**
   * Very lazy byte[] to URL encoder.  Just encodes almost everything, even
   * some "normal" chars.
   * By not encoding about 1/4 of the chars, we make random data like hashes about 16% smaller.
   *
   * RFC1738: 0-9a-zA-Z$-_.+!*'(),
   * Us:      0-9a-zA-Z
   *
   */
  public static String urlencode(byte[] bs)
  {
    StringBuilder sb = new StringBuilder(bs.length*3);
    for (int i = 0; i < bs.length; i++)
      {
        int c = bs[i] & 0xFF;
        if ((c >= '0' && c <= '9') ||
            (c >= 'A' && c <= 'Z') ||
            (c >= 'a' && c <= 'z')) {
            sb.append((char)c);
        } else {
            sb.append('%');
            if (c < 16)
              sb.append('0');
            sb.append(Integer.toHexString(c));
        }
      }
         
    return sb.toString();
  }

  /**
   *  @return true for i2p hosts only
   *  @since 0.7.12
   */
  public static boolean isValidAnnounce(String ann) {
    URL url;
    try {
       url = new URL(ann);
    } catch (MalformedURLException mue) {
       return false;
    }
    return url.getProtocol().equals("http") &&
           (url.getHost().endsWith(".i2p") || url.getHost().equals("i2p")) &&
           url.getPort() < 0;
  }

  private static class Tracker
  {
      String announce;
      boolean isPrimary;
      long interval;
      long lastRequestTime;
      String trackerProblems;
      boolean stop;
      boolean started;
      int registerFails;
      int consecutiveFails;
      int seenPeers;

      public Tracker(String a, boolean p)
      {
          announce = a;
          isPrimary = p;
          interval = INITIAL_SLEEP;
      }

      /**
       *  Call before restarting
       *  @since 0.9.1
       */
      public void reset() {
          lastRequestTime = 0;
          trackerProblems = null;
          stop = false;
          started = false;
          registerFails = 0;
          consecutiveFails = 0;
          seenPeers = 0;
      }
  }
}