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

Skip to content
Snippets Groups Projects
TrackerClient.java 20.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • /* 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.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    
    zzz's avatar
    zzz committed
    import java.net.MalformedURLException;
    import java.net.URL;
    
    import java.util.ArrayList;
    import java.util.Collections;
    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.Hash;
    
    zzz's avatar
    zzz committed
    import net.i2p.util.I2PAppThread;
    
    jrandom's avatar
    jrandom committed
    import net.i2p.util.Log;
    
    
    /**
     * Informs metainfo tracker of events and gets new peers for peer
     * coordinator.
     *
     * @author Mark Wielaard (mark@klomp.org)
     */
    
    zzz's avatar
    zzz committed
    public class TrackerClient extends I2PAppThread
    
      private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(TrackerClient.class);
    
      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";
    
    zzz's avatar
    zzz committed
      private static final String NOT_REGISTERED  = "torrent not registered"; //bytemonsoon
    
    
      private final static int SLEEP = 5; // 5 minutes.
    
    zzz's avatar
    zzz committed
      private final static int DELAY_MIN = 2000; // 2 secs.
      private final static int DELAY_MUL = 1500; // 1.5 secs.
    
    zzz's avatar
    zzz committed
      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
    
    zzz's avatar
    zzz committed
      private I2PSnarkUtil _util;
    
    zzz's avatar
    zzz committed
      private final String additionalTrackerURL;
    
      private final PeerCoordinator coordinator;
    
      private final Snark snark;
    
      private final int port;
    
      private boolean stop;
    
    jrandom's avatar
    jrandom committed
      private boolean started;
    
    zzz's avatar
    zzz committed
      private List<Tracker> trackers;
    
    zzz's avatar
    zzz committed
      /**
       * @param meta null if in magnet mode
    
    zzz's avatar
    zzz committed
       * @param additionalTrackerURL may be null, from the ?tr= param in magnet mode, otherwise ignored
    
    zzz's avatar
    zzz committed
      public TrackerClient(I2PSnarkUtil util, MetaInfo meta, String additionalTrackerURL,
                           PeerCoordinator coordinator, Snark snark)
    
    zzz's avatar
    zzz committed
        super();
    
        String id = urlencode(snark.getID());
    
    zzz's avatar
    zzz committed
        setName("TrackerClient " + id.substring(id.length() - 12));
    
    zzz's avatar
    zzz committed
        _util = util;
    
    zzz's avatar
    zzz committed
        this.additionalTrackerURL = additionalTrackerURL;
    
        this.snark = snark;
    
    sponge's avatar
    sponge committed
        @Override
    
    jrandom's avatar
    jrandom committed
      public void start() {
    
    jrandom's avatar
    jrandom committed
          if (stop) throw new RuntimeException("Dont rerun me, create a copy");
    
    jrandom's avatar
    jrandom committed
          super.start();
    
    jrandom's avatar
    jrandom committed
          started = true;
    
    jrandom's avatar
    jrandom committed
      }
      
      public boolean halted() { return stop; }
    
    jrandom's avatar
    jrandom committed
      public boolean started() { return started; }
    
    jrandom's avatar
    jrandom committed
      
    
      /**
       * Interrupts this Thread to stop it.
       */
      public void halt()
      {
        stop = true;
        this.interrupt();
      }
    
    
    jrandom's avatar
    jrandom committed
      private boolean verifyConnected() {
    
    zzz's avatar
    zzz committed
        while (!stop && !_util.connected()) {
            boolean ok = _util.connect();
    
    jrandom's avatar
    jrandom committed
            if (!ok) {
                try { Thread.sleep(30*1000); } catch (InterruptedException ie) {}
            }
        }
    
    zzz's avatar
    zzz committed
        return !stop && _util.connected();
    
    jrandom's avatar
    jrandom committed
      }
      
    
    sponge's avatar
    sponge committed
        @Override
    
    zzz's avatar
    zzz committed
        String infoHash = urlencode(snark.getInfoHash());
    
        String peerID = urlencode(snark.getID());
    
    zzz's avatar
    zzz committed
        // 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.
    
    zzz's avatar
    zzz committed
        // todo: check for b32 matches as well
    
    zzz's avatar
    zzz committed
        trackers = new ArrayList(2);
    
        String primary = null;
    
    zzz's avatar
    zzz committed
        if (meta != null)
    
    zzz's avatar
    zzz committed
            primary = meta.getAnnounce();
    
    zzz's avatar
    zzz committed
        else if (additionalTrackerURL != null)
            primary = additionalTrackerURL;
        if (primary != null) {
    
    zzz's avatar
    zzz committed
            if (isValidAnnounce(primary)) {
    
    zzz's avatar
    zzz committed
                trackers.add(new Tracker(primary, true));
    
                _log.debug("Announce: [" + primary + "] infoHash: " + infoHash);
    
    zzz's avatar
    zzz committed
            } else {
                _log.warn("Skipping invalid or non-i2p announce: " + primary);
            }
    
    zzz's avatar
    zzz committed
        } else {
            _log.warn("No primary announce");
    
    zzz's avatar
    zzz committed
        }
    
    zzz's avatar
    zzz committed
        List tlist = _util.getOpenTrackers();
    
        if (tlist != null && (meta == null || !meta.isPrivate())) {
    
    zzz's avatar
    zzz committed
            for (int i = 0; i < tlist.size(); i++) {
                 String url = (String)tlist.get(i);
    
    zzz's avatar
    zzz committed
                 if (!isValidAnnounce(url)) {
    
    zzz's avatar
    zzz committed
                    _log.error("Bad announce URL: [" + url + "]");
                    continue;
                 }
                 int slash = url.indexOf('/', 7);
                 if (slash <= 7) {
                    _log.error("Bad announce URL: [" + url + "]");
                    continue;
                 }
    
    zzz's avatar
    zzz committed
                 if (primary.startsWith(url.substring(0, slash)))
    
    zzz's avatar
    zzz committed
                    continue;
    
    zzz's avatar
    zzz committed
                 String dest = _util.lookup(url.substring(7, slash));
    
    zzz's avatar
    zzz committed
                 if (dest == null) {
    
    zzz's avatar
    zzz committed
                    _log.error("Announce host unknown: [" + url.substring(7, slash) + "]");
    
    zzz's avatar
    zzz committed
                    continue;
                 }
    
    zzz's avatar
    zzz committed
                 if (primary.startsWith("http://" + dest))
    
    zzz's avatar
    zzz committed
                    continue;
    
    zzz's avatar
    zzz committed
                 if (primary.startsWith("http://i2p/" + dest))
    
    zzz's avatar
    zzz committed
                    continue;
    
                 // opentrackers are primary if we don't have primary
                 trackers.add(new Tracker(url, primary.equals("")));
    
    zzz's avatar
    zzz committed
                 _log.debug("Additional announce: [" + url + "] for infoHash: " + infoHash);
            }
        }
    
    
        if (trackers.isEmpty()) {
    
    zzz's avatar
    zzz committed
            stop = true;
    
    zzz's avatar
    zzz committed
            // FIXME translate
            SnarkManager.instance().addMessage("No valid trackers for " + this.snark.getBaseName() + " - enable opentrackers?");
            _log.error("No valid trackers for " + this.snark.getBaseName());
    
    zzz's avatar
    zzz committed
            // FIXME keep going if DHT enabled
    
    zzz's avatar
    zzz committed
            this.snark.stopTorrent();
    
    zzz's avatar
    zzz committed
            return;
        }
    
    
        long uploaded = coordinator.getUploaded();
        long downloaded = coordinator.getDownloaded();
        long left = coordinator.getLeft();
    
        boolean completed = (left == 0);
        try
          {
    
    jrandom's avatar
    jrandom committed
            if (!verifyConnected()) return;
    
    sponge's avatar
    sponge committed
            boolean runStarted = false;
    
    zzz's avatar
    zzz committed
            boolean firstTime = true;
            int consecutiveFails = 0;
    
    zzz's avatar
    zzz committed
            Random r = I2PAppContext.getGlobalContext().random();
    
    zzz's avatar
    zzz committed
                // Local DHT tracker announce
                if (_util.getDHT() != null)
                    _util.getDHT().announce(snark.getInfoHash());
    
    zzz's avatar
    zzz committed
                    // Sleep the minimum interval for all the trackers, but 60s minimum
                    // except for the first time...
    
    zzz's avatar
    zzz committed
                    int delay;
    
    zzz's avatar
    zzz committed
                    int random = r.nextInt(120*1000);
                    if (firstTime) {
                      delay = r.nextInt(30*1000);
                      firstTime = false;
    
    sponge's avatar
    sponge committed
                    } else if (completed && runStarted)
    
    zzz's avatar
    zzz committed
                      delay = 3*SLEEP*60*1000 + random;
    
                    else if (snark.getTrackerProblems() != null && ++consecutiveFails < MAX_CONSEC_FAILS)
    
    zzz's avatar
    zzz committed
                      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;
    
    
    zzz's avatar
    zzz committed
                    if (delay > 0)
                      Thread.sleep(delay);
    
                  }
                catch(InterruptedException interrupt)
                  {
                    // ignore
                  }
    
                if (stop)
                  break;
    
    jrandom's avatar
    jrandom committed
           
                if (!verifyConnected()) return;
    
                
                uploaded = coordinator.getUploaded();
                downloaded = coordinator.getDownloaded();
    
                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;
                
    
    zzz's avatar
    zzz committed
                // *** loop once for each tracker
                int maxSeenPeers = 0;
                for (Iterator iter = trackers.iterator(); iter.hasNext(); ) {
                  Tracker tr = (Tracker)iter.next();
                  if ((!stop) && (!tr.stop) &&
    
    zzz's avatar
    zzz committed
                      (completed || coordinator.needOutboundPeers()) &&
    
    sponge's avatar
    sponge committed
                      (event.equals(COMPLETED_EVENT) || System.currentTimeMillis() > tr.lastRequestTime + tr.interval))
    
    zzz's avatar
    zzz committed
                        if (!tr.started)
                          event = STARTED_EVENT;
                        TrackerInfo info = doRequest(tr, infoHash, peerID,
    
                        snark.setTrackerProblems(null);
    
    zzz's avatar
    zzz committed
                        tr.trackerProblems = null;
                        tr.registerFails = 0;
                        tr.consecutiveFails = 0;
                        if (tr.isPrimary)
                            consecutiveFails = 0;
    
    sponge's avatar
    sponge committed
                        runStarted = true;
    
    zzz's avatar
    zzz committed
                        tr.started = true;
    
    
                        Set<Peer> peers = info.getPeers();
    
    zzz's avatar
    zzz committed
                        tr.seenPeers = info.getPeerCount();
    
                        if (snark.getTrackerSeenPeers() < tr.seenPeers) // update rising number quickly
                            snark.setTrackerSeenPeers(tr.seenPeers);
    
    
                        // pass everybody over to our tracker
                        if (_util.getDHT() != null) {
                            for (Peer peer : peers) {
                                _util.getDHT().announce(snark.getInfoHash(), peer.getPeerID().getDestHash());
                            }
                        }
    
    
    zzz's avatar
    zzz committed
                        if (coordinator.needOutboundPeers()) {
    
    jrandom's avatar
    jrandom committed
                            // we only want to talk to new people if we need things
                            // from them (duh)
    
                            List<Peer> ordered = new ArrayList(peers);
    
                            Collections.shuffle(ordered, r);
    
                            Iterator<Peer> it = ordered.iterator();
    
    zzz's avatar
    zzz committed
                            while ((!stop) && it.hasNext() && coordinator.needOutboundPeers()) {
    
                              Peer cur = it.next();
    
    zzz's avatar
    zzz committed
                              // FIXME if id == us || dest == us continue;
    
    zzz's avatar
    zzz committed
                              // only delay if we actually make an attempt to add peer
    
    zzz's avatar
    zzz committed
                              if(coordinator.addPeer(cur) && it.hasNext()) {
                                int delay = (DELAY_MUL * r.nextInt(10)) + DELAY_MIN;
    
    zzz's avatar
    zzz committed
                                try { Thread.sleep(delay); } catch (InterruptedException ie) {}
                              }
    
    jrandom's avatar
    jrandom committed
                            }
                        }
    
                      }
                    catch (IOException ioe)
                      {
                        // Probably not fatal (if it doesn't last to long...)
    
    zzz's avatar
    zzz committed
                        _util.debug
    
                          ("WARNING: Could not contact tracker at '"
    
    zzz's avatar
    zzz committed
                           + tr.announce + "': " + ioe, Snark.WARNING);
                        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)) {
    
    zzz's avatar
    zzz committed
                          // Give a guy some time to register it if using opentrackers too
                          if (trackers.size() == 1) {
                            stop = true;
    
                            snark.stopTorrent();
    
    zzz's avatar
    zzz committed
                          } else { // hopefully each on the opentrackers list is really open
                            if (tr.registerFails++ > MAX_REGISTER_FAILS)
                              tr.stop = true;
                          }
    
    zzz's avatar
    zzz committed
                        }
    
                        if (++tr.consecutiveFails == MAX_CONSEC_FAILS) {
                            tr.seenPeers = 0;
                            if (tr.interval < LONG_SLEEP)
                                tr.interval = LONG_SLEEP;  // slow down
                        }
    
    zzz's avatar
    zzz committed
                  if ((!tr.stop) && maxSeenPeers < tr.seenPeers)
                      maxSeenPeers = tr.seenPeers;
                }  // *** end of trackers loop here
    
    
    zzz's avatar
    zzz committed
                // Get peers from PEX
    
    zzz's avatar
    zzz committed
                if (coordinator.needOutboundPeers() && (meta == null || !meta.isPrivate()) && !stop) {
    
    zzz's avatar
    zzz committed
                    Set<PeerID> pids = coordinator.getPEXPeers();
                    if (!pids.isEmpty()) {
                        _util.debug("Got " + pids.size() + " from PEX", Snark.INFO);
                        List<Peer> peers = new ArrayList(pids.size());
                        for (PeerID pID : pids) {
                            peers.add(new Peer(pID, snark.getID(), snark.getInfoHash(), snark.getMetaInfo()));
                        }
                        Collections.shuffle(peers, r);
                        Iterator<Peer> it = peers.iterator();
    
    zzz's avatar
    zzz committed
                        while ((!stop) && it.hasNext() && coordinator.needOutboundPeers()) {
    
    zzz's avatar
    zzz committed
                            Peer cur = it.next();
                            if (coordinator.addPeer(cur) && it.hasNext()) {
                                int delay = (DELAY_MUL * r.nextInt(10)) + DELAY_MIN;
                                try { Thread.sleep(delay); } catch (InterruptedException ie) {}
                             }
                        }
                    }
                }
    
    
    zzz's avatar
    zzz committed
                // Get peers from DHT
                // FIXME this needs to be in its own thread
    
                if (_util.getDHT() != null && (meta == null || !meta.isPrivate()) && !stop) {
    
    zzz's avatar
    zzz committed
                    int numwant;
    
    zzz's avatar
    zzz committed
                    if (event.equals(STOPPED_EVENT) || !coordinator.needOutboundPeers())
    
    zzz's avatar
    zzz committed
                        numwant = 1;
                    else
                        numwant = _util.getMaxConnections();
                    List<Hash> hashes = _util.getDHT().getPeers(snark.getInfoHash(), numwant, 2*60*1000);
                    _util.debug("Got " + hashes + " from DHT", Snark.INFO);
                    // announce  ourselves while the token is still good
                    // FIXME this needs to be in its own thread
                    if (!stop) {
                        int good = _util.getDHT().announce(snark.getInfoHash(), 8, 5*60*1000);
                        _util.debug("Sent " + good + " good announces to DHT", Snark.INFO);
                    }
    
                    // now try these peers
                    if ((!stop) && !hashes.isEmpty()) {
                        List<Peer> peers = new ArrayList(hashes.size());
                        for (Hash h : hashes) {
                            PeerID pID = new PeerID(h.getData());
    
                            peers.add(new Peer(pID, snark.getID(), snark.getInfoHash(), snark.getMetaInfo()));
    
    zzz's avatar
    zzz committed
                        }
                        Collections.shuffle(peers, r);
                        Iterator<Peer> it = peers.iterator();
    
    zzz's avatar
    zzz committed
                        while ((!stop) && it.hasNext() && coordinator.needOutboundPeers()) {
    
    zzz's avatar
    zzz committed
                            Peer cur = it.next();
    
    zzz's avatar
    zzz committed
                            if (coordinator.addPeer(cur) && it.hasNext()) {
                                int delay = (DELAY_MUL * r.nextInt(10)) + DELAY_MIN;
    
    zzz's avatar
    zzz committed
                                try { Thread.sleep(delay); } catch (InterruptedException ie) {}
                             }
                        }
                    }
                }
    
    
    
    zzz's avatar
    zzz committed
                // we could try and total the unique peers but that's too hard for now
    
                snark.setTrackerSeenPeers(maxSeenPeers);
    
    sponge's avatar
    sponge committed
                if (!runStarted)
    
    zzz's avatar
    zzz committed
                    _util.debug("         Retrying in one minute...", Snark.DEBUG);
    
    zzz's avatar
    zzz committed
              } // *** end of while loop
          } // try
    
    zzz's avatar
    zzz committed
            _util.debug("TrackerClient: " + t, Snark.ERROR, t);
    
    jrandom's avatar
    jrandom committed
            if (t instanceof OutOfMemoryError)
                throw (OutOfMemoryError)t;
    
    zzz's avatar
    zzz committed
            // Local DHT tracker unannounce
            if (_util.getDHT() != null)
                _util.getDHT().unannounce(snark.getInfoHash());
    
    zzz's avatar
    zzz committed
                // try to contact everybody we can
    
                // Don't try to restart I2CP connection just to say goodbye
    
    zzz's avatar
    zzz committed
                for (Iterator iter = trackers.iterator(); iter.hasNext(); ) {
    
                  if (!_util.connected()) return;
    
    zzz's avatar
    zzz committed
                  Tracker tr = (Tracker)iter.next();
                  if (tr.started && (!tr.stop) && tr.trackerProblems == null)
                      doRequest(tr, infoHash, peerID, uploaded,
    
    zzz's avatar
    zzz committed
                }
    
    zzz's avatar
    zzz committed
      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=");
    
    zzz's avatar
    zzz committed
        if (left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needOutboundPeers())
    
    zzz's avatar
    zzz committed
        else
    
            buf.append(_util.getMaxConnections());
        String s = buf.toString();
    
    zzz's avatar
    zzz committed
        _util.debug("Sending TrackerClient request: " + s, Snark.INFO);
    
    zzz's avatar
    zzz committed
        tr.lastRequestTime = System.currentTimeMillis();
    
    zzz's avatar
    zzz committed
        File fetched = _util.get(s);
    
        if (fetched == null) {
            throw new IOException("Error fetching " + s);
        }
        
    
    jrandom's avatar
    jrandom committed
        InputStream in = null;
    
    jrandom's avatar
    jrandom committed
        try {
    
    jrandom's avatar
    jrandom committed
            in = new FileInputStream(fetched);
    
            TrackerInfo info = new TrackerInfo(in, snark.getID(),
    
                                               snark.getInfoHash(), snark.getMetaInfo());
    
    zzz's avatar
    zzz committed
            _util.debug("TrackerClient response: " + info, Snark.INFO);
    
    jrandom's avatar
    jrandom committed
    
            String failure = info.getFailureReason();
            if (failure != null)
              throw new IOException(failure);
    
    
    zzz's avatar
    zzz committed
            tr.interval = info.getInterval() * 1000;
    
    jrandom's avatar
    jrandom committed
            return info;
        } finally {
    
    jrandom's avatar
    jrandom committed
            if (in != null) try { in.close(); } catch (IOException ioe) {}
    
    jrandom's avatar
    jrandom committed
            fetched.delete();
        }
    
    zzz's avatar
    zzz committed
       * 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
       *
    
    zzz's avatar
    zzz committed
      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;
    
    zzz's avatar
    zzz committed
            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));
            }
    
    zzz's avatar
    zzz committed
    
    
    zzz's avatar
    zzz committed
      /**
       *  @return true for i2p hosts only
       *  @since 0.7.12
       */
      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;
      }
    
    
    zzz's avatar
    zzz committed
      private static class Tracker
    
    zzz's avatar
    zzz committed
      {
          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;
              lastRequestTime = 0;
              trackerProblems = null;
              stop = false;
              started = false;
              registerFails = 0;
              consecutiveFails = 0;
              seenPeers = 0;
          }
      }