/* 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.*; import java.net.*; import java.util.*; import org.klomp.snark.bencode.*; import net.i2p.util.I2PThread; import net.i2p.util.Log; /** * Informs metainfo tracker of events and gets new peers for peer * coordinator. * * @author Mark Wielaard (mark@klomp.org) */ public class TrackerClient extends I2PThread { private static final Log _log = new Log(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"; private final static int SLEEP = 5; // 5 minutes. private final static int DELAY_MIN = 2000; // 2 secs. private final static int DELAY_MUL = 1500; // 1.5 secs. private final MetaInfo meta; private final PeerCoordinator coordinator; private final int port; private boolean stop; private boolean started; private long interval; private long lastRequestTime; public TrackerClient(MetaInfo meta, PeerCoordinator coordinator) { // Set unique name. super("TrackerClient-" + urlencode(coordinator.getID())); this.meta = meta; this.coordinator = coordinator; this.port = 6881; //(port == -1) ? 9 : port; stop = false; started = false; } public void start() { if (stop) throw new RuntimeException("Dont rerun me, create a copy"); super.start(); started = true; } public boolean halted() { return stop; } public boolean started() { return started; } /** * Interrupts this Thread to stop it. */ public void halt() { stop = true; this.interrupt(); } private boolean verifyConnected() { while (!stop && !I2PSnarkUtil.instance().connected()) { boolean ok = I2PSnarkUtil.instance().connect(); if (!ok) { try { Thread.sleep(30*1000); } catch (InterruptedException ie) {} } } return !stop && I2PSnarkUtil.instance().connected(); } public void run() { // XXX - Support other IPs String announce = meta.getAnnounce(); //I2PSnarkUtil.instance().rewriteAnnounce(meta.getAnnounce()); String infoHash = urlencode(meta.getInfoHash()); String peerID = urlencode(coordinator.getID()); _log.debug("Announce: [" + meta.getAnnounce() + "] infoHash: " + infoHash + " xmitAnnounce: [" + announce + "]"); long uploaded = coordinator.getUploaded(); long downloaded = coordinator.getDownloaded(); long left = coordinator.getLeft(); boolean completed = (left == 0); int sleptTime = 0; try { if (!verifyConnected()) return; boolean started = false; while (!started) { sleptTime = 0; try { // Send start. TrackerInfo info = doRequest(announce, infoHash, peerID, uploaded, downloaded, left, STARTED_EVENT); Set peers = info.getPeers(); coordinator.trackerSeenPeers = peers.size(); coordinator.trackerProblems = null; if (!completed) { Iterator it = peers.iterator(); while (it.hasNext()) { Peer cur = (Peer)it.next(); coordinator.addPeer(cur); int delay = DELAY_MUL; delay *= ((int)cur.getPeerID().getAddress().calculateHash().toBase64().charAt(0)) % 10; delay += DELAY_MIN; sleptTime += delay; try { Thread.sleep(delay); } catch (InterruptedException ie) {} } } started = true; } catch (IOException ioe) { // Probably not fatal (if it doesn't last to long...) Snark.debug ("WARNING: Could not contact tracker at '" + announce + "': " + ioe, Snark.WARNING); coordinator.trackerProblems = ioe.getMessage(); } if (stop) break; if (!started) { Snark.debug(" Retrying in one minute...", Snark.DEBUG); try { // Sleep one minutes... Thread.sleep(60*1000); } catch(InterruptedException interrupt) { // ignore } } } Random r = new Random(); while(!stop) { try { // Sleep some minutes... int delay; if(coordinator.trackerProblems != null && !completed) { delay = 60*1000; } else if(completed) { delay = 3*SLEEP*60*1000 + r.nextInt(120*1000); } else { delay = SLEEP*60*1000 + r.nextInt(120*1000); delay -= sleptTime; } if (delay > 0) Thread.sleep(delay); } catch(InterruptedException interrupt) { // ignore } if (stop) break; if (!verifyConnected()) return; uploaded = coordinator.getUploaded(); downloaded = coordinator.getDownloaded(); left = coordinator.getLeft(); // First time we got a complete download? String event; if (!completed && left == 0) { completed = true; event = COMPLETED_EVENT; } else event = NO_EVENT; // Only do a request when necessary. sleptTime = 0; if (event == COMPLETED_EVENT || coordinator.needPeers() || System.currentTimeMillis() > lastRequestTime + interval) { try { TrackerInfo info = doRequest(announce, infoHash, peerID, uploaded, downloaded, left, event); coordinator.trackerProblems = null; Set peers = info.getPeers(); coordinator.trackerSeenPeers = peers.size(); if ( (left > 0) && (!completed) ) { // we only want to talk to new people if we need things // from them (duh) List ordered = new ArrayList(peers); Collections.shuffle(ordered); Iterator it = ordered.iterator(); while (it.hasNext()) { Peer cur = (Peer)it.next(); // only delay if we actually make an attempt to add peer if(coordinator.addPeer(cur)) { int delay = DELAY_MUL; delay *= ((int)cur.getPeerID().getAddress().calculateHash().toBase64().charAt(0)) % 10; delay += DELAY_MIN; sleptTime += delay; try { Thread.sleep(delay); } catch (InterruptedException ie) {} } } } } catch (IOException ioe) { // Probably not fatal (if it doesn't last to long...) Snark.debug ("WARNING: Could not contact tracker at '" + announce + "': " + ioe, Snark.WARNING); coordinator.trackerProblems = ioe.getMessage(); } } } } catch (Throwable t) { I2PSnarkUtil.instance().debug("TrackerClient: " + t, Snark.ERROR, t); if (t instanceof OutOfMemoryError) throw (OutOfMemoryError)t; } finally { try { if (!verifyConnected()) return; TrackerInfo info = doRequest(announce, infoHash, peerID, uploaded, downloaded, left, STOPPED_EVENT); } catch(IOException ioe) { /* ignored */ } } } private TrackerInfo doRequest(String announce, String infoHash, String peerID, long uploaded, long downloaded, long left, String event) throws IOException { String s = announce + "?info_hash=" + infoHash + "&peer_id=" + peerID + "&port=" + port + "&ip=" + I2PSnarkUtil.instance().getOurIPString() + ".i2p" + "&uploaded=" + uploaded + "&downloaded=" + downloaded + "&left=" + left + ((event != NO_EVENT) ? ("&event=" + event) : ""); if (Snark.debug >= Snark.INFO) Snark.debug("Sending TrackerClient request: " + s, Snark.INFO); File fetched = I2PSnarkUtil.instance().get(s); if (fetched == null) { throw new IOException("Error fetching " + s); } InputStream in = null; try { in = new FileInputStream(fetched); TrackerInfo info = new TrackerInfo(in, coordinator.getID(), coordinator.getMetaInfo()); if (Snark.debug >= Snark.INFO) Snark.debug("TrackerClient response: " + info, Snark.INFO); lastRequestTime = System.currentTimeMillis(); String failure = info.getFailureReason(); if (failure != null) throw new IOException(failure); interval = info.getInterval() * 1000; return info; } finally { if (in != null) try { in.close(); } catch (IOException ioe) {} fetched.delete(); } } /** * Very lazy byte[] to URL encoder. Just encodes everything, even * "normal" chars. */ static String urlencode(byte[] bs) { StringBuffer sb = new StringBuffer(bs.length*3); for (int i = 0; i < bs.length; i++) { int c = bs[i] & 0xFF; sb.append('%'); if (c < 16) sb.append('0'); sb.append(Integer.toHexString(c)); } return sb.toString(); } }