// Commented for the Learning branch
package com.limegroup.bittorrent;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.limegroup.gnutella.Downloader;
import com.limegroup.gnutella.InsufficientDataException;
import com.limegroup.gnutella.MessageService;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.UploadManager;
import com.limegroup.bittorrent.TorrentManager;
import com.limegroup.gnutella.filters.IPFilter;
import com.limegroup.gnutella.io.NIODispatcher;
import com.limegroup.gnutella.io.Throttle;
import com.limegroup.bittorrent.settings.BittorrentSettings;
import com.limegroup.bittorrent.messages.BTHave;
import com.limegroup.gnutella.util.CoWList;
import com.limegroup.gnutella.util.FixedSizeExpiringSet;
import com.limegroup.gnutella.util.ManagedThread;
import com.limegroup.gnutella.util.NetworkUtils;
import com.limegroup.gnutella.util.ProcessingQueue;
/**
* A ManagedTorrent object represents a torrent we're downloading and sharing with BitTorrent.
*
* === Objects related to ManagedTorrent ===
*
* TorrentManager, _manager
* The program's one TorrentManager object keeps a list of all the ManagedTorrent objects.
* Each ManagedTorrent links up to it with _manager.
*
* ManagedTorrent, this
* This ManagedTorrent object represents a torrent the program is sharing online.
*
* BTConnectionFetcher, _connectionFetcher
* This ManagedTorrent's BTConnection Fetcher makes connections to 5 remote computers sharing the same torrent.
*
* BTConnection, _connections
* A BTConnection object represents a TCP socket connection to a remote computer running BitTorrent software.
* The remote computer has this torrent, and we're sharing it data through this connection.
* This connection started with the BitTorrent handshake, followed by BitTorrent packets.
*
* BTDownloader and BTUploader, _downloader and _uploader
* A BTDownloader lets LimeWire's GUI list this torrent alongside the Gnutella downloads.
*
* BTMetaInfo, _info
* This ManagedTorrent's BTMetaInfo represents the bencoded data from the .torrent file.
*
* VerifyingFolder, _folder
* This ManagedTorrent's VerifyingFolder saves files to disk and checks their hashes.
*
* === Lists of addresses and connections ===
*
* A ManagedTorrent keeps 2 lists:
* _connections is a list of BTConnection objects.
* These are open TCP socket connections to remote computers we're sharing this torrent through.
* _peers is a list of TorrentLocation objects.
* These are IP addresses and port numbers of remote computers we can try connecting to.
* Our tracker on the Web gave us these addresses.
*
* === Contacting our tracker ===
*
* Every 5 minutes or longer, we'll contact our tracker to get an up-to-date list of IP addresses of peers sharing the same torrent as us.
* ManagedTorrent contains the code that contacts the tracker.
* 4 methods run forever in a loop, trading off control between several threads:
*
* scheduleTrackerRequest(delay, url) - have the RouterService call the next method delay milliseconds from now.
* announce(url) - make a new thread named "TrackerRequest", and have it call the next method.
* announceBlocking(url, event) - contact the tracker, blocks waiting for the response, and give it to the next method.
* handleTrackerResponse(response, url) - add the IP addresses our tracker told us to our peers list, and call scheduleTrackerRequest() with the delay the tracker gave us.
*
* === Sending Choke and Unchoke messages ===
*
* Every 30 seconds, a BitTorrent program sends each of its connections a Choke or Unchoke message.
* scheduleRechoke(), rechoke(), PeriodicChoker, and Rechoker do this for LimeWire.
* Rechoker.run() contains the code that decides which of our connectionsn we'll choke and which we'll unchoke.
* It unchokes the connections that are sending us data the fastest.
* It also selects 1 or 2 connections randomly and unchokes them.
* The nested DownloadSpeedComparator class has the code that can sort a list of connections by how fast they are giving us data.
*
* setGlobalChoke() isn't a part of BitTorrent.
* It just suspends us from sending data to anyone while we move the files we saved from the "Incomplete" folder to the "Shared" folder.
*
* === Sending Request and Have messages ===
*
* request() picks a range to request from a given remote computer, and sends it a Request message.
* notifyOfComplete() sends a Have message to all our connections, telling them we have a numbered piece.
*/
public class ManagedTorrent {
/** A debugging log we can write lines of text to as the program runs. */
private static final Log LOG = LogFactory.getLog(ManagedTorrent.class);
/** Not used. */
static final byte[] ZERO_BYTES = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
/** 5, if we try and fail to contact our tracker 5 times, we'll give up. */
private static final int MAX_TRACKER_FAILURES = 5;
/**
* A DownloadSpeedComparator that can tell which of 2 computers is sending us data the fastest.
* Rechoker.run() uses DOWNLOAD_SPEED_COMPARATOR to sort our list of interested remote computers into order.
*/
private static final Comparator DOWNLOAD_SPEED_COMPARATOR = new DownloadSpeedComparator();
/**
* 30 seconds in milliseconds.
* Every 30 seconds we'll send each of our connections a Choke or Unchoke message.
* scheduleRechoke() and the PeriodicChoker use this.
*/
private static final int RECHOKE_TIMEOUT = 30 * 1000;
/** A link back up to the program's TorrentManager object, which has this ManagedTorrent in its list. */
private final TorrentManager _manager;
/**
* True if we've stopped this torrent.
* This means we're not sending or receiving file data for it any longer.
*
* When the program calls stopNow() to stop transferring data for this torrent, it sets _stopped to true.
*/
private volatile boolean _stopped = true;
/**
* Our list of IP addresses of remote BitTorrent computers that are sharing this torrent.
* We're not connected to these computers now, we just know their addresses.
*
* Before our BTConnectinFetcher starts trying to connect to an address, getTorrentLocation() removes it from the _peers list.
* Additinally, isConnectedTo() makes sure we don't already have a connection to that address.
* _peers contains addresses we can try, and aren't connected to right now.
*
* A synchronized HashSet of TorrentLocation objects.
* Synchronize on this ManagedTorrent to access _peers.
*/
private Set _peers;
/**
* Addresses of peers that have this torrent, but we tried and failed to connect to in the last hour.
*
* OutgoingBTHandshaker.handleIOException() calls addBadEndpoint() with the address in a TorrentLocation object.
* This TorrentLocation isn't in the _peers list anymore, because getTorrentLocation() removed it before we tried to connect.
*
* A synchronized FixedSizeExpiringSet of TorrentLocation objects.
* TorrentLocation objects only last in _badPeers for 1 hour, and only keeps the most recent 500.
*/
private Set _badPeers;
/** The BTMetaInfo object we made for this .torrent file, which has information like the list of file paths. */
private BTMetaInfo _info;
/** The VerifyingFolder object that represents the folder we're saving files to, and can check piece hashes. */
private volatile VerifyingFolder _folder;
/**
* Counts the number of times our tracker on the Web has failed.
* We tried to contact it, and couldn't.
* Or we did, and it said "failure reason".
*/
private int _trackerFailures = 0;
/** True if the VerifyingFolder object tried to setup the folder where we'll save the files of this torrent, and was unable to. */
private boolean _couldNotSave = false;
/** This ManagedTorrent has a BTConnectionFetcher object that tries to open new TCP socket connections to remote computers. */
private volatile BTConnectionFetcher _connectionFetcher;
/**
* True when we're not uploading any file data to anyone.
* setGlobalChoke() does this when we're moving files from the "Incomplete" to "Saved" folder, so they're not open for reading.
*/
private volatile boolean _globalChoke = false;
/**
* True if this torrent is paused, and not transferring any data because of that.
* Use the pause() and resume() methods to set this condition.
*/
private boolean _paused = false;
/**
* True when we've finished the download, and moved the files from the "Incomplete" folder to the "Shared" folder.
* saveFiles() sets this to true, and uses it to avoid running a second time.
*
* The user can restart a completed download.
* _saved lets this object remember that we've saved and moved the files already.
*/
private boolean _saved = false;
/** A list of BTConnection objects that represent the TCP socket connections we have open to remote computers sharing this torrent with us. */
private List _connections;
/**
* A ProcessingQueue that can have another thread run code.
* enqueueTask() adds objects with run() methods to _processingQueue, which keeps them in a list.
* One by one, the "ManagedTorrent" thread calls run() on them and discards them.
*/
private ProcessingQueue _processingQueue;
/** A BTDownloader object that lets LimeWire's GUI list this torrent with the Gnutella downloads. */
private BTDownloader _downloader;
/** A BTUploader object that lets LimeWire's GUI list this torrent with the Gnutella uploads. */
private BTUploader _uploader;
/**
* A PeriodicChoker makes a Rechoker object which sends a Choke or Unchoke message to each of our connections, and then does that again 30 seconds later.
* The scheduleRechoke() method makes this ManagedTorrent's PeriodicChoker object, and saves it as choker.
*/
private PeriodicChoker choker;
/**
* Make a new ManagedTorrent object to represent a torrent we're downloading and sharing with other BitTorrent programs on the Internet.
*
* @param info The BTMetaInfo object we made from the bencoded data inside the .torrent file
* @param manager A reference up to the program's one TorrentManager object, which keeps a list of ManagedTorrent objects like this one
*/
public ManagedTorrent(BTMetaInfo info, TorrentManager manager) {
// Link the objects used for a torrent together
_info = info; // Link us to the BTMetaInfo object that has the information from the .torrent file
_info.setManagedTorrent(this); // Link the BTMetaInfo object back up to us
_manager = manager; // Link us up to the program's one TorrentManager object
_folder = info.getVerifyingFolder(); // Link to the VerifyingFolder object the BTMetaInfo object makes to represent our save folder
// Make a new empty ArrayList for the _connections, we'll list our BTConnection objects here
_connections = new CoWList(CoWList.ARRAY_LIST);
// Setup our ProcessingQueue, name the thread it will start "ManagedTorrent"
_processingQueue = new ProcessingQueue("ManagedTorrent");
// Make BTDownloader and BTUploader objects that will let LimeWire's GUI list this torrent in the download and upload lists
_downloader = new BTDownloader(this, _info);
_uploader = new BTUploader(this, _info);
// Point our lists of remote computer addresses at empty lists so they're not null
_peers = Collections.EMPTY_SET;
_badPeers = Collections.EMPTY_SET;
}
/**
* Get the info hash, the hash BitTorrent uses to identify this torrent and its file.
*
* To compute the info hash, take the SHA1 hash of the value of "info" in the bencoded data of the .torrent file.
* The value of "info" is a bencoded dictionary.
*
* @return The 20-byte SHA1 info hash of this .torrent file
*/
public byte[] getInfoHash() {
// Get it from the BTMetaInfo object, which hashes the "info" dictionary of bencoded data to make it
return _info.getInfoHash();
}
/**
* Get the BTMetaInfo object that represents the .torrent file we opened.
* It holds the information from the bencoded data of the .torrent file, like the list of file names and sizes.
*
* @return Our BTMetaInfo object
*/
public BTMetaInfo getMetaInfo() {
// Return the reference the constructor saved
return _info;
}
/**
* Determine if we've finished downloading this torrent and checking its data.
*
* @return true if we're done, false if we're still getting data
*/
public boolean isComplete() {
// Ask the VerifyingFolder this
return _folder.isComplete();
}
/**
* Start sharing this torrent.
*
* Sets up this object.
* Has the VerifyingFolder open the folder it represents.
* Connects to peers, and contacts our tracker.
*/
public void start() {
// Make a note in our debugging log
if (LOG.isDebugEnabled()) LOG.debug("requesting torrent start", new Exception());
// The "ManagedTorrent" thread will call this run() method
enqueueTask(new Runnable() {
public void run() {
// If we're stopped, leave without doing anything
if (!_stopped) return;
_stopped = false;
// Set up the contents of this object
initializeTorrent();
// Have the VerifyingFolder open the folder it represents
initializeFolder();
if (_stopped || _folder.isVerifying()) return; // If the VerifyingFolder is still checking its contents, don't connect yet
// Connect to peers, and contact our tracker
startConnecting();
}
});
}
/**
* Connect to peers sharing our torrent, connect to our tracker, and send Choke and Unchoke messages in 30 seconds.
*
* startConnecting does 3 things:
* It tries to open new TCP socket connections to remote computers sharing our torrent, and does the handshake with them.
* It contacts our tracker on the Web with "event=start".
* It sends each of our connections a Choke or Unchoke message 30 seconds from now, and each 30 seconds afterwards.
*/
private void startConnecting() {
// If we already know of some peer computer addresses, have our BTConnectionFetcher try to open connections
if (_peers.size() > 0) _connectionFetcher.fetch();
// Connect to the tracker
announceStart(); // The tracker's address is at the start of the .torrent file
// Send each of our connections a Choke or Unchoke message 30 seconds from now, and every 30 seconds afterwards.
scheduleRechoke();
}
/**
* Stop Internet communications related to this torrent, and remove this object from the program.
*
* Has the "ManagedTorrent" thead perform the following tasks:
* Remove us from the GUI's list of uploads.
* Close all the open files we have.
* Contact all our trackers with "event=stop".
* Close all our TCP socket connections to peers sharing the same torrent.
* Remove us from the TorrentManager's list.
*/
public void stop() {
// Have the "ManagedTorrent" thread call stopNow()
if (LOG.isDebugEnabled()) LOG.debug("requested torrent stop", new Exception());
enqueueTask(new Runnable() {
public void run() {
stopNow();
}
});
}
/**
* Stop Internet communications related to this torrent, and remove this object from the program.
*
* Removes us from the GUI's list of uploads.
* Closes all the open files we have.
* Contacts all our trackers with "event=stop".
* Closes all our TCP socket connections to peers sharing the same torrent.
* Removes us from the TorrentManager's list.
*
* Only let the "ManagedTorrent" thread call stopNow().
*/
private void stopNow() {
// Only do this once
if (_stopped) return;
_stopped = true;
// Remove this torrent from the GUI's list of uploads
RouterService.getCallback().removeUpload(_uploader);
// Have the VerifyingFolder close all the files it has open
_folder.close();
// Tell all our trackers that we're leaving by contacting their Web addresses with "event=stop"
for (int i = 0; i < _info.getTrackers().length; i++) announceBlocking(_info.getTrackers()[i], TrackerRequester.EVENT_STOP);
// Close all our TCP socket connections to remote computers we've been sharing this torrent with
for (Iterator iter = getConnections().iterator(); iter.hasNext(); ) ((BTConnection)iter.next()).close();
// Stop our BTConnectionFetcher from connecting to new remote computers sharing the torrent
_connectionFetcher.shutdown();
// Remove this ManagedTorrent object from the list of them the program's single TorrentManager object keeps
_manager.removeTorrent(this, _folder.isComplete());
// Make a note we're stopped
if (LOG.isDebugEnabled()) LOG.debug("Torrent stopped!");
}
/**
* Stop Internet communications related to this torrent, and remove this object from the program.
*
* Call pause() to force this torrent to make room for others, if the program has any.
*/
public void pause() {
// Have the "ManagedTorrent" thread call this run() method we're defining right here
enqueueTask(new Runnable() {
public void run() {
// Mark this ManagedTorrent as paused
_paused = true;
// Disconnect from remote computers, tell our tracker goodbye, and remove this ManagedTorrent from the program
stopNow();
}
});
}
/**
* Unpause this torrent, starting new Internet communications to share it.
*
* @return false if it's already going, so we can't unpause.
* false if we had an error saving it, so we can't unpause.
* true on success.
*/
public boolean resume() {
// If we're not stopped, return false, there is nothing else to do
if (!_stopped) return false;
// If our VerifyingFolder didn't have an error
if (!_couldNotSave) {
// Remove our record of our paused status
_paused = false;
// Resume this ManagedTorrent with the program's single TorrentManager
_manager.wakeUp(this);
// Report success
return true;
}
// Our VerifyingFolder had an error, we can't resume, return false
return false;
}
/**
* Remove a given BTConnection object from our _connections list.
*
* Only BTConnection.close() calls this method, giving it the connection that we're closing.
* We are closing the connection because we don't want it anymore, or because we tried to connect it and it didn't work.
*
* @param btc The BTConnection object that represents the connection we're closing
*/
void connectionClosed(BTConnection btc) {
// If we had this connection open for a while before closing it
if (btc.isWorthRetrying()) {
// Get the IP address and port number from the BTConnection, and make a new TorrentLocation from it
TorrentLocation ep = new TorrentLocation(btc.getEndpoint());
// Have it record that we've tried and failed to connect
ep.strike();
// Add it back into our _peers list of addresses to try
_peers.add(ep);
}
// Remove a given BTConnection object from our _connections list
removeConnection(btc); // If that remote computer was interested in our data, and we weren't choking it, send Choke and Unchoke messages right now
// If this torrent isn't stopped, have our BTConnectionFetcher try to get more connections to have 5
if (!_stopped) _connectionFetcher.fetch();
}
/**
* Set up the contents of this object.
* Only start() calls this method.
*
* Sets flags for save error, choke everyone, and paused to false.
* Makes our lists to hold the addresses of peers and bad peers, those we couldn't connect to.
* Registers our BTUploader object with LimeWire's GUI.
* Makes a BTConnectionFetcher that will try to connect to remote computers with the same torrent.
*/
private void initializeTorrent() {
// Set flags for the start of sharing this torrent
_couldNotSave = false; // No, we haven't run into a problem setting up the folder where we'll save the files of this torrent yet
_globalChoke = false; // No, we're not moving the files right now making us unable to give any pieces to anyone
_paused = false; // No, we're not paused transferring data for this torrent
// Setup our lists for peers we failed connecting to, and more we could try connecting to
_badPeers = Collections.synchronizedSet(new FixedSizeExpiringSet(500, 60 * 60 * 1000)); // Holds 500, and only keeps them for an hour
_peers = Collections.synchronizedSet(new HashSet());
// Does nothing
if (_info.getLocations() != null) _peers.addAll(_info.getLocations());
// Add our BTUploader object to the list of them the GUI keeps
RouterService.getCallback().addUpload(_uploader);
// Make a new BTConnectionFetcher that will try to connect to remote computers sharing this torrent
_connectionFetcher = new BTConnectionFetcher(this, _manager.getPeerId());
// Make a note in the debugging log
if (LOG.isDebugEnabled()) LOG.debug("Starting torrent");
}
/**
* Prepares the folder where we'll save this torrent.
* Calls VerifyingFolder.open(this).
* If that throws an IOException, sets _couldNotSave and _stopped to true.
*/
private void initializeFolder() {
try {
// Open the VerifyingFolder, which will save data to disk and hash and check it
_folder.open(this);
// There was an error preparing the save folder
} catch (IOException ioe) {
// Set flags to true and leave now
if (LOG.isDebugEnabled()) LOG.debug("unrecoverable error", ioe);
_couldNotSave = true;
_stopped = true;
return;
}
// If our VerifyingFolder object says we've already downloaded this torrent, move the files we downloaded from the "Incomplete" folder to the "Shared" folder
if (_folder.isComplete()) saveCompleteFiles();
}
/**
* When our VerifyingFolder is done hashing the files on disk we saved before, call startConnecting().
* This connects to peers sharing our torrent, connects to our tracker, and sends Choke and Unchoke messages in 30 seconds.
*
* Only VerifyingFolder.open() calls this method.
* It calls it as a notification that its verification of our saved data is complete.
*/
void verificationComplete() {
// Have the "ManagedTorrent" thread call this run() method
enqueueTask(new Runnable() {
public void run() {
// If no one has stopped this torrent, and we need more data to complete it, connect to everything to stare this torrent
if (!_stopped && !_folder.isComplete()) startConnecting();
}
});
}
/**
* Hit our tracker on the Web with "event=start".
* The first time we talk to a tracker, we should do it this way.
*/
private void announceStart() {
// Loop for each of our trackers, we can have more than one
for (int i = 0; i < _info.getTrackers().length; i++) {
// Contact our tracker with "event=start", and add the IP addresses of its response to our _peers list
announceBlocking(_info.getTrackers()[i], TrackerRequester.EVENT_START);
}
}
/**
* Pick a range to request from a remote computer, and send it a Request message to ask for it.
*
* Calls btc.getAvailableRanges() to get a list of the parts of the file the remote computer has.
* Calls _folder.leaseRandom() to pick one for us to ask for.
* Calls btc.sendRequest() to send the remote computer a BitTorrent Request message, asking for a range in a piece.
*
* Calls btc.sendNotInterested() to send the remote computer a Not Interested message, telling it we don't want anything it has.
*
* @param btc The BTConnection object that represents the remote computer this method will request data from
*/
public void request(final BTConnection btc) {
// Make a note that we're going to ask the given remote computer for data
if (LOG.isDebugEnabled()) LOG.debug("requesting ranges from " + btc.toString());
// If this torrent is done or stopped, don't ask a remote computer for anything
if (_folder.isComplete() || _stopped) return;
// Choose a random part to ask for from amongst the ranges of the file btc has
BTInterval in = _folder.leaseRandom(btc.getAvailableRanges());
// We found a range to ask for
if (in != null) {
// Send the given remote computer a Request message, asking it for the range it has and we randomly selected
btc.sendRequest(in);
// We didn't find a range to ask for because btc doesn't have any
} else if (btc.getAvailableRanges().isEmpty()) {
// Send the given remote computer a Not Interested message, telling it we don't need any of its data
if (LOG.isDebugEnabled()) LOG.debug("leaseRarest returned null, btc connection not interesting anymore");
btc.sendNotInterested();
// We didn't find a range to ask for because of some other reason
} else {
// Make a note, this shouldn't happen
if (LOG.isDebugEnabled()) LOG.debug("leaseRarest returned null, btc connection still interesting ??!?!?");
}
}
/**
* Send a Have message with the given piece number to all our connections sharing this torrent.
* If the VerifyingFolder has downloaded the complete torrent, move it into our "Shared" folder.
*
* Only BTMetaInfo.notifyOfComplete() calls this method.
* Here's how the call comes here:
* VerifyingFolder.handleVerified(int) gets a piece number we've received and hashed to find is correct.
* VerifyingFolder.notifyOfChunkCompletion(int) just calls the next method.
* BTMetaInfo.notifyOfComplete(int) just calls this method.
*
* @param The piece number we've received and verified
*/
void notifyOfComplete(int in) {
// Make a note we just received a good piece
if (LOG.isDebugEnabled()) LOG.debug("got completed chunk " + in);
// If our VerifyingFolder is using files on the disk right now, don't do anything
if (_folder.isVerifying()) return;
// Make a Have message that can tell a remote computer we have this piece number
final BTHave have = BTHave.createMessage(in); // Give it the piece number
// Have the "NIODispatch" thread run this run() method
Runnable haveNotifier = new Runnable() {
public void run() {
// Loop through all our connections sharing this torrent
for (Iterator iter = getConnections().iterator(); iter.hasNext(); ) {
BTConnection btc = (BTConnection) iter.next();
// Tell each remote computer we have this piece
btc.sendHave(have);
}
}
};
NIODispatcher.instance().invokeLater(haveNotifier);
// If we have the whole torrent now
if (_folder.isComplete()) {
// Make a note we're done
LOG.info("file is complete");
// Have the "ManagedTorrent" thread call this run() method
enqueueTask(new Runnable() {
public void run() {
// Move the files we downloaded from the "Incomplete" folder to the "Shared" folder
saveCompleteFiles();
}
});
}
}
/**
* Find out how many remote computers we are connected to and sharing this torrent with.
* Counts the connections that we initiated, not remote computers that connected to us.
*
* @return Our number of open, outgoing connections for this torrent
*/
public int getNumAltLocs() {
// Start the count at 0
int ret = 0;
// Loop through all our connections to remote computers sharing this torrent
for (Iterator iter = getConnections().iterator(); iter.hasNext(); ) {
BTConnection btc = (BTConnection) iter.next();
// If we initiated this connection to the remote computer, count it
if (btc.isOutgoing()) ret++;
}
// Return the count we made
return ret;
}
/**
* Determine what Downloader state this torrent is in right now.
*
* The Downloader class defines a list of different states that a download can be in.
* They are like 4 Download.COMPLETE or 1 Download.CONNECTING.
* getState() figures out which of those states this torrent is in right now, and returns the appropriate one.
*
* @return The number code of a download state, like Download.COMPLETE or Download.CONNECTING
*/
public int getState() {
// This torrent is stopped, it can't perform network activity
if (_stopped) {
// Return a specific reason, or just QUEUED
if (_couldNotSave) return Downloader.DISK_PROBLEM; // The VerifyingFolder had an error from the disk
else if (_folder.isComplete()) return Downloader.COMPLETE; // We're stopped because it's done
else if (_trackerFailures > MAX_TRACKER_FAILURES) return Downloader.GAVE_UP; // The tracker broke more than 5 times
else if (_paused) return Downloader.PAUSED; // We're stopped because it's paused
else return Downloader.QUEUED; // Otherwise, say we're queued
}
/*
* We're not stopped, we're still sharing this torrent.
*/
// If we have the whole thing, we're seeding
if (_folder.isComplete()) return Downloader.SEEDING;
/*
* We're still downloading this torrent
*/
// The VerifyingFolder is hashing data
if (_folder.isVerifying()) {
// We're hashing
return Downloader.HASHING;
// We have some open connections to remote computers we're sharing this torrent with
} else if (_connections.size() > 0) {
// If we have a connection to a remote computer not choking us, we're downloading data from it
if (isDownloading()) return Downloader.DOWNLOADING;
return Downloader.REMOTE_QUEUED; // Otherwise all our connections are choking us, we're in our remote computers' queues
// We don't have any open connections, but we do have addresses we can try to connect to
} else if (_peers != null && _peers.size() > 0) {
// We're connecting
return Downloader.CONNECTING;
// We don't have any open connections, and we don't know of any addresses to connect to
} else if (_peers == null || _peers.size() == 0) {
// We're still waiting for our tracker to give us some addresses
return Downloader.WAITING_FOR_TRACKER;
}
// Otherwise, say we're busy
return Downloader.BUSY;
}
/**
* Determine if we have a connection to a remote computer not choking us.
* This means it's giving us data now.
*
* Only getState() above calls this method.
* If it returns true, we're downloading right now.
*
* @return true if we have a connection choking us, false otherwise
*/
private boolean isDownloading() {
// Loop through our connections, returning true if we have one not choking us
for (Iterator iter = getConnections().iterator(); iter.hasNext(); ) {
if (!((BTConnection)iter.next()).isChoking()) return true;
}
// All of our connections are choking us, or we don't have any connections
return false;
}
/**
* Add the given TorrentLocation object to our _peers list.
*
* The TorrentLocation holds the IP address and port number of a remote computer sharing the same torrent as us.
* _peers is our list of addresses we can try to connect to in order to share our torrent.
*
* @param to A TorrentLocation that has the IP address and port number of a remote computer sharing the same torent as us.
* @return true if we didn't have it and added it.
* false if we already have it, we're already connected to it, it's on our bad IP list, or it's us, so we can't connect to it.
*/
public boolean addEndpoint(TorrentLocation to) {
// If we already have the given TorrentLocation in our _peers list, or we're already connected to it, return false, we already have it
if (_peers.contains(to) || isConnectedTo(to)) return false;
// If the given IP address is on our list of government, institutional, and spamer IP addresses to avoid, return false, we can't connect to it
if (!IPFilter.instance().allow(to.getAddress())) return false;
// If the given IP address is our external Internet IP address, return false, we can't connect to it
if (NetworkUtils.isMe(to.getAddress(), to.getPort())) return false;
// Add the given TorrentLocation to _peers if it's not already there
if (_peers.add(to)) { // Returns true if it wasn't there yet, and add() added it
// Have the BTConnectionFetcher get connections for this torrent
_connectionFetcher.fetch();
// Report true, we added it
return true;
}
// Report false, we already had it
return false;
}
/**
* Add a given IP address and port number to the list of addresses this ManagedTorrent wasn't able to connect to.
*
* OutgingBTHandshaker.handleIOException() calls this method.
* It's tried to open a new TCP socket connection to a remote computer.
* NIO couldn't do this, and threw it an IOException.
*
* Adds the given TorrentLocation to this ManagedTorrent's _badPeers list.
*
* @param to A TorrentLocation object that has the IP address and port number we couldn't connect to
*/
public void addBadEndpoint(TorrentLocation to) {
// Add the given TorrentLocation to the _badPeers list
_badPeers.add(to); // 1 hour later, the _badPeers list will throw it out
}
/**
* Add the IP addresses our tracker told us to our peers list, and schedule the next time we'll contact it with a call to scheduleTrackerRequest().
* This is method 4 in the tracker contact loop, and calls the first method to complete the loop.
*
* Parses a response from our tracker.
* Looks for "failure reason", and increments _trackerFailures if it's found.
* Adds all the IP addresses the tracker told us to our _peers list.
* Finds out when the tracker wants to hear from us again, and schedules the RouterService to call announce() then.
*
* @param response A TrackerResponse object that represents the bencoded data of the tracker's response
* @param url The Web address of the tracker
*/
private void handleTrackerResponse(TrackerResponse response, URL url) {
// Make a note we're doing this
LOG.debug("handling tracker response " + url.toString());
// Make minWaitTime 5 minutes in milliseconds, we use this if we can't read the tracker's response
long minWaitTime = BittorrentSettings.TRACKER_MIN_REASK_INTERVAL.getValue() * 1000;
try {
// We couldn't contact the tracker
if (response == null) {
LOG.debug("null response");
throw new IOException();
}
// Loop through the list of peers the tracker gave us, these are the IP addresses and port numbers of remote computers sharing the same torrent as us
for (Iterator iter = response.PEERS.iterator(); iter.hasNext(); ) {
TorrentLocation next = (TorrentLocation)iter.next();
// Add the IP address and port number to our _peers list of remote computers we'll try to connect to
addEndpoint(next);
}
// Set minWaitTime from the tracker's response
minWaitTime = response.INTERVAL * 1000; // Convert from seconds to milliseconds
// If the tracker said "failure reason", show an error to the user and throw an IOException
if (response.FAILURE_REASON != null && // The tracker sent a bencoded key "failure reason", indicating it couldn't help us, and
_trackerFailures == 0) { // This is the first time our tracker has done this
// Show an error to the user and throw an IOException
MessageService.showError("TORRENTS_TRACKER_FAILURE", _info.getName() + "\n" + response.FAILURE_REASON);
throw new IOException("Tracker request failed.");
}
// Record that the tracker hasn't failed
_trackerFailures = 0;
// Nothing seems to throw this
} catch (ValueException ve) {
// Never runs
if (LOG.isDebugEnabled()) LOG.debug(ve);
_trackerFailures++;
// We couldn't contact the tracker, or we did and it said "failure reason"
} catch (IOException ioe) {
// Count this tracker failure
if (LOG.isDebugEnabled()) LOG.debug(ioe);
_trackerFailures++;
}
// If this ManagedTorrent isn't stopped and our tracker hasn't failed us 5 times yet
if (!_stopped && _trackerFailures < MAX_TRACKER_FAILURES) {
// Schedule the next time we'll contact our tracker, minWaitTime milliseconds from now
scheduleTrackerRequest(minWaitTime, url);
}
}
/**
* Determine if we need more connections.
* Returns true if we have less than 80.
* 80 is a lot for one torrent, so needsMoreConnections() will almost always return true.
*
* Only BTConnectionFetcher.fetch() calls this method.
*
* @return true if we need more connections.
* false if we have many and don't need more.
*/
boolean needsMoreConnections() {
// If we're externally contactable for remote computers to connect to our TCP listening socket
if (RouterService.acceptedIncomingConnection()) {
// If we have fewer than 80 connections, return true, we need more
return _connections.size() < BittorrentSettings.TORRENT_MAX_CONNECTIONS.getValue() * 4 / 5;
// Remote computers can't connect to us
} else {
// If we have fewer than 100 connections, return true, we need more
return _connections.size() < BittorrentSettings.TORRENT_MAX_CONNECTIONS.getValue();
}
}
/**
* Not used.
*
* @param ep the TorrentLocation for the connection to add
* @return true if we want this connection, false if not
*/
boolean allowIncomingConnection(TorrentLocation ep) {
// happens if we stopped this torrent but we still receive an incoming
// connection because it took some time to read the headers & stuff
if (_stopped) return false;
// this could still happen, although we don't usually accept any
// locations we are already connected to.
if (isConnectedTo(ep)) return false;
// don't allow connections to self
if (NetworkUtils.isMe(ep.getAddress(), ep.getPort())) return false;
// we do a little bit of preferencing here, - we support some features
// others don't - and really, LimeWire users should help each other.
// we won't do any nasty stuff like preferring LimeWire's when
// uploading b/c that would just be mean
if (ep.isLimePeer()) {
return _connections.size() < BittorrentSettings.TORRENT_RESERVED_LIME_SLOTS.getValue() + BittorrentSettings.TORRENT_MAX_CONNECTIONS.getValue();
}
return _connections.size() < BittorrentSettings.TORRENT_MAX_CONNECTIONS.getValue();
}
/**
* Add a given BitTorrent connection to the list of them this ManagedTorrent object keeps.
* Adds the given BTConnection object to our _connections list.
*
* Only BTHandshaker.tryToFinishHandshakes() calls this method.
*
* @param btc A BTConnection object that represents a remote computer we have a new connection with to share this torrent
*/
public void addConnection(final BTConnection btc) {
// Make a note we're adding the connection
if (LOG.isDebugEnabled()) LOG.debug("trying to add connection " + btc.toString());
/*
* this check prevents a few exceptions that may be thrown if a
* connection initialization is completed after we have already stopped
* happens especially when a user quits a torrent manually.
*/
// We're not supposed to make network communications any longer
if (_stopped) {
// Close the connection, and leave without adding it to anything
btc.close();
return;
}
// Add the given BTConnection object to our _connections list
_connections.add(btc);
if (LOG.isDebugEnabled()) LOG.debug("added connection " + btc.toString());
}
/**
* Remove a given BTConnection object from our _connections list.
* If that remote computer was interested in our data, and we weren't choking it, send all our connections a Choke or Unchoke message right now.
*
* Only connectionClosed() calls this method.
*
* @param btc A BTConnection object that we're closing
*/
private void removeConnection(final BTConnection btc) {
// Make a note we're removing the given connection from our list
if (LOG.isDebugEnabled()) LOG.debug("removing connection " + btc.toString());
// Remove the given connection from the _connections list
_connections.remove(btc);
// If this remote computer is interested in our data, and we're not witholding data from it, send all our connections a Choke or Unchoke message right now
if (btc.isInterested() && !btc.isChoked()) rechoke();
}
/**
* Move files and make network communications for finishing downloading this torrent.
*
* Closes or chokes all our connections.
* Moves the files we downloaded from the "Incomplete" folder to the "Shared" folder.
* Contacts our tracker with "event=complete".
*/
private void saveCompleteFiles() {
// If we've already moved the files of this torrent into the "Shared" folder, don't do it again
if (_saved) return;
// Have the "NIODispatcher" thread run this run() method that will close or choke our connections
Runnable r = new Runnable() {
public void run() {
// Stop sending data
LOG.debug("global choke");
setGlobalChoke(true);
// Loop for each connection to a remote computer we're sharing this torrent with
for (Iterator iter = getConnections().iterator(); iter.hasNext(); ) {
BTConnection btc = (BTConnection)iter.next();
/*
* close connections that aren't interested in any part of a
* complete torrent
*/
// If this peer isn't interested in us even though we have the whole file
if (!btc.isInterested()) {
// Close our connection to it
btc.close();
// This peer is interested in our data
} else {
/*
* cancel all requests, if there are any left. (This should not
* be the case at this point anymore)
*/
// Leave the connection open, but tell the remote computer we don't want anything
btc.cancelAllRequests();
btc.sendNotInterested();
}
}
}
};
NIODispatcher.instance().invokeLater(r);
// Move the files we downloaded from the "Incomplete" folder to the "Shared" folder
saveFiles();
// Contact our tracker with "event=complete", telling it we've completed the file while in contact with it
announceComplete();
}
/**
* Move the files we downloaded for this torrent from the "Incomplete" folder to the "Shared" folder.
*
* Only runs once, and sets _saved to true.
* Closes all the VerifyingFolder object's open files.
* Moves what we downloaded from LimeWire's "Incomplete" folder to the "Shared" folder.
* Resumes a normal choking pattern of giving data to our peers.
*
* this is executed within the timer thread
* but since we don't want to upload or download anything while we are
* saving the files, it should be okay
*/
private void saveFiles() {
// Only do this once
if (_saved) return;
// Close all the VerifyingFolder object's open files
_folder.close();
if (LOG.isDebugEnabled()) LOG.debug("folder closed");
// Move the torrent file or folder we downloaded from the "Incomplete" folder to the "Shared" folder
_couldNotSave = !_info.moveToCompleteFolder(); // Returns false on error, set _couldNotSave to true
if (LOG.isDebugEnabled()) LOG.debug("could not save: " + _couldNotSave);
// Have the BTMetaInfo object remake its VerifyingFolder object, and save the new one as _folder
_folder = _info.getVerifyingFolder(); // Save the new VerifyingFolder object as _folder, overwriting the old one
if (LOG.isDebugEnabled()) LOG.debug("new veryfing folder"); // Make a note that we have a new VerifyingFolder object
try {
// Open the new VerifyingFolder for writing
_folder.open();
// Unable to open it, record that we had a problem saving
} catch (IOException ioe) {
LOG.debug(ioe);
_couldNotSave = true;
}
if (LOG.isDebugEnabled()) LOG.debug("folder opened");
// If the VerifyingFolder had an error, stop all our Internet communications
if (_couldNotSave) stopNow();
// Instead of not giving data to anyone, resume a normal pattern of giving data to some peers
setGlobalChoke(false);
// Set _saved to true so we won't do this again
_saved = true;
}
/**
* Contact our tracker with "event=complete".
* If we finish downloading the torrent while in contact with our tracker, we should tell it this.
* Don't tell a tracker "event=complete" if you connect to it with a complete file ready to share.
*/
private void announceComplete() {
/*
* should we announce how much we've downloaded if we just resumed
* the torrent? Its not mentioned in the spec...
*/
// Loop for each of our trackers
for (int i = 0; i < _info.getTrackers().length; i++) {
// Contact our tracker with "event=complete", and add the IP addresses of its response to our _peers list
announceBlocking(_info.getTrackers()[i], TrackerRequester.EVENT_COMPLETE);
}
}
/**
* Schedule the RouterService to call announce(url) minDelay milliseconds from now.
* This is method 1 in the tracker contact loop.
*
* @param minDelay The time in milliseconds to wait before contacting our tracker
* @param url Our tracker's address on the Web
*/
private void scheduleTrackerRequest(long minDelay, final URL url) {
/*
* a tracker request can take quite a few seconds (easily up to 30)
* it will slow us down since we cannot enqueue any further pieces
* during that time - it may become necessary to do tracker requests
* in their own thread
*/
// Have the RouterService call this run() method minDelay milliseconds from now
Runnable announcer = new Runnable() {
public void run() {
// Contact our BitTorent tracker on the Web, add the IP addresses it gives us to our _peers list, and schedule the next time we'll contact it
if (LOG.isDebugEnabled()) LOG.debug("announcing to " + url.toString());
announce(url);
}
};
RouterService.schedule(announcer, minDelay, 0); // 0 to not repeat this, just run it once minDelay from now
}
/**
* Determine if we're connected to a given IP address.
*
* Takes a TorrentLocation that has the IP address and port number of a remote computer sharing the same torrent as us.
* Loops through our _connections list, looking for a BTConnection object that has the given IP address.
* Returns true if it finds one.
*
* @return true if we're connected to that address, false if we're not
*/
private boolean isConnectedTo(TorrentLocation to) {
// Loop through the BTConnection objects in our _connections list, these are the remote computers we have TCP socket connections to sharing this torrent
for (Iterator iter = getConnections().iterator(); iter.hasNext(); ) {
BTConnection btc = (BTConnection)iter.next();
/*
* compare by address only. there's no way of comparing ports or peer ids
*/
// If the addresses match, we're already connected, return true
if (btc.getEndpoint().getAddress().equals(to.getAddress())) return true;
}
// No match found, we're not connected to the given address, return false
return false;
}
/** Not used. */
long calculateWaitTime() {
if (_peers.size() == 0) return 0;
long ret = Long.MAX_VALUE;
long now = System.currentTimeMillis();
synchronized(_peers) {
for (Iterator iter = _peers.iterator(); iter.hasNext(); ) {
ret = Math.min(ret, ((TorrentLocation) iter.next()).getWaitTime(now));
if (ret == 0) return 0;
}
}
return ret;
}
/**
* Get an IP address of a remote computer sharing this torrent for us to try to connect to.
*
* Takes a TorrentLocation object from the _peers list.
* Removes it from the _peers list and returns it, making the _peers list only contain addresses we're not trying to connect to.
*
* Only BTConnectionFetcher.fetchConnection() calls this.
*
* @return A TorrentLocation object that has an IP address and port number we can try to connect to.
* null if the _peers list doesn't have any.
*/
TorrentLocation getTorrentLocation() {
// Get the time now
long now = System.currentTimeMillis();
// Only let 1 thread access the _peers list at a time
synchronized (_peers) {
// Loop for each TorrentLocation in the _peers list
Iterator iter = _peers.iterator();
while (iter.hasNext()) { // These are the IP addresses of remote computers we're not connected to that our tracker said are sharing the same torrent as us
TorrentLocation temp = (TorrentLocation)iter.next();
// If we've tried and failed to connect to this address in the last 5 minutes, skip it and loop to get the next one
if (temp.isBusy(now)) continue;
// Remove this TorrentLocation from the _peers list, now that we are going to try to connect to it, we don't want it there anymore
iter.remove();
// If we're not already connected to the address
if (!isConnectedTo(temp)) {
// Return it
return temp;
}
}
}
// The _peers list doesn't have an address we can connect to
return null;
}
/**
* Send each of our connections a Choke or Unchoke message 30 seconds from now, and every 30 seconds afterwards.
* This method doesn't send the messages right now, for that, call rechoke().
*
* Makes a PeriodicChoker object that send a Choke or Unchoke message to each of our connections, and then does it again 30 seconds later.
* Has the RouterService call its run() method 30 seconds from now.
*/
private void scheduleRechoke() {
// If we have a PeriodicChoker from the last time, set it's stopped boolean to true
if (choker != null) choker.stopped = true; // This prevents its run() method from doing anything when the RouterService calls it within the next 30 seconds
// Make a new PeriodicChoker, and have the RouterService call its run() method 30 seconds from now
choker = new PeriodicChoker();
RouterService.schedule(choker, RECHOKE_TIMEOUT, 0); // It will reschedule itself with the RouterService to run every 30 seconds
}
/**
* Send each of this torrent's connections a Choke or Unchoke message right now.
*
* Makes a new Rechoker object, giving it our current list of BTConnection objects.
* Has the "NIODispatch" thread call the run() method on that Rechoker object right now.
*/
void rechoke() {
// Make a new Rechoker object, giving it our current list of connections, and have the "NIODispatch" thread call its run() method right now
NIODispatcher.instance().invokeLater(new Rechoker(_connections));
}
/**
* A PeriodicChoker makes a Rechoker object which sends a Choke or Unchoke message to each of our connections, and then does that again 30 seconds later.
*
* The scheduleRechoke() method above makes this ManagedTorrent's PeriodicChoker object, and saves it as choker.
* 30 seconds later, the RouterService calls the run() method here.
* It makes a new Rechoker object, which sends a Choke or Unchoke message to each of our connections right away.
* The run() method also schedules itself with the RouterService to get called 30 seconds later.
*/
private class PeriodicChoker implements Runnable {
/**
* Count the number of times this runs.
*
* scheduleRechoke() makes a PeriodicChoker each time it's called, but that PeriodicChoker can live a long time.
* Every 30 seconds, the RouterService will call the PeriodicChoker's run() method.
*/
private int run = 0;
/**
* True if the ManagedTorrent has replaced this PeriodicChoker with a new one, so this old one shouldn't do anything.
*
* scheduleRechoke() sets stopped to true before making a new PeriodicChoker().
* The new one will send out Choked and Unchoked messages 30 seconds from then, and every 30 seconds afterwards.
* When this happens, it won't schedule itself again, and it will get garbage collected.
*/
volatile boolean stopped;
/**
* Send each of our connections a Choke or Unchoke method, and schedule this to run again 30 seconds from now.
*
* This run() method does 2 things:
* It makes a new Rechoker object, which will send each of our connections a Choke or Unchoke method.
* It has the RouterService call this run() method again 30 seconds from now.
*/
public void run() {
// If the scheduleRechoke() method is going to replace this PeriodicChoker with a new one, it has set stopped to true
if (stopped) return;
// Make a note we're going to send Choke and Unchoke mesages to our connections
if (LOG.isDebugEnabled()) LOG.debug("scheduling rechoke");
// A List we'll point at _connections, or at a shuffled copy of that list
List l;
// If run is a multiple of 3, like 0, 3, 6, 9, 12
if (run++ % 3 == 0) { // After it determines if run is a multiple of 3, move run to the next number
// Make a copy of the _connections list named l, and shuffle the BTConnections in it into random order
l = new ArrayList(_connections);
Collections.shuffle(l);
// run wasn't a multiple of 3
} else {
// No need to shuffle it, just point l and the _connections list
l = _connections;
}
// Make a new Rechoker object, giving it the _connections list, and have the "NIODispatch" thread call its run() method
NIODispatcher.instance().invokeLater(new Rechoker(l));
// Have the RouterService call this run() method 30 seconds from now
RouterService.schedule(this, RECHOKE_TIMEOUT, 0);
}
}
/**
* Make a Rechoker object and have a thread run it to send a Choke or Unchoke message to each of our connections.
*
* Rechoker implements the Runnable interface, which means it has a run() method.
* You can give a Rechoker object to a thread, and the thread will call the run() method once and then exit.
*
* The constructor takes a list of BTConnection objects, they represent our connected peers.
* The run() method chooses which to choke and unchoke, and sends the appropriate message to each.
*/
private class Rechoker implements Runnable {
/** A list of BTConnection objects that represent our BitTorrent connections to remote computers sharing our torrent. */
private final List connections;
/**
* Make a new Rechoker object, which will send each of our connections a Choke or Unchoke message.
*
* @param connections A list of BTConnection objects.
* These represent our TCP socket connections to remote computers sharing the same torrent as us.
* PeriodicChoker.run() shuffled this list before it gave it to this constructor, so it's in random order.
*/
Rechoker(List connections) {
// Save the given shuffled list of BTConnection objects in this new object
this.connections = connections;
}
/**
* Send each of our connetions a Choke or Unchoke message.
*
* Loops through all this torrent's connections, sending each a Choke or Unchoke message.
* Unchokes the connections that have been giving us the most data.
* Also chooses one connection randomly to unchoke, giving it a chance to see how fast it can send us data after we start sending it some.
*
* When you give a Rechoker object to a thread, it will call this run() method once and then exit.
* The "NIODispatch" thread calls this run() method.
*/
public void run() {
// If we're moving files from the "Incomplete" folder to the "Saved" folder, we can't give any peer any data, leave without doing anything
if (_globalChoke) {
LOG.debug("global choke");
return;
}
// Loop for each of our connections, adding those that are interested in our data to the fastest list
List fastest = new ArrayList(connections.size()); // Make an ArrayList we'll add the interested connections to
for (Iterator iter = connections.iterator(); iter.hasNext(); ) {
BTConnection con = (BTConnection) iter.next();
// If this remote computer is interested in the parts of the torrent that we have, add it to the fastest list
if (con.isInterested() && con.shouldBeInterested()) fastest.add(con);
}
// Sort the fastest list into order, now the remote computer that's giving us data the fastest will be listed first
Collections.sort(fastest, DOWNLOAD_SPEED_COMPARATOR); // Have Collections.sort call DownloadSpeedComparator.compare(c1, c2) to pick the fastest
/*
* Unchoke the fastest connectsion that are interested in us.
*
* In BitTorrent, unchoking a connection means we're going to give it data.
* An intersted connection is one that wants data from us.
*
* We want to unchoke the connections that have been giving us data the fastest.
* This rewards them for being nice to us by being nice to them in return.
*
* The code below doesn't actually change the fastest list.
* This means we're going to unchoke all the connections that are interested in us, regardless of how quickly they're giving us data.
*/
// Doesn't actually change the fastest list at all
int numFast = getNumUploads() - 1;
for (int i = fastest.size() - 1; i >= numFast; i--) fastest.remove(i);
/*
* Optimistically unchoke one interested connection.
*
* In BitTorrent, we randomly unchoke a connection interested in our data.
* We don't choose it based on how much data it's giving us, we just randomly choose it.
* This gives new connections a chance.
*
* The line of code below is:
*
* 4 - the number of connections interested in our data
*
* So, if we only have 2 connections interested in our data, it will be 4 - 2 = 2.
* Math.max then takes that answer, if it's bigger than 1.
*
* The idea here is that if we only have a few connections interested in us, we'll unchoke more than just 1.
*/
// Choose how many connections we will optimistically unchoke, set optimistic to 1 or more
int optimistic = Math.max(1, BittorrentSettings.TORRENT_MIN_UPLOADS.getValue() - fastest.size());
// Loop for each of our connections
for (Iterator iter = connections.iterator(); iter.hasNext(); ) {
BTConnection con = (BTConnection)iter.next();
// Look for con in the fastest list, the list of connections that are interested in our data
if (fastest.remove(con)) { // Returns true if the fastest list contained the con object, meaning con is interested in our data
// Tell that connection that we're not choking it any longer
con.sendUnchoke();
// con wasn't in the fastest list, meaning it's not interested in our data, and
} else if (
optimistic > 0 && // Yes, optimistic will be 1 or more, and
con.shouldBeInterested()) { // This connection should want our data, even though it hasn't told us it's interested officially
/*
* This is weird, but this is how Bram does it.
*/
// Tell the connection that we're not choking it any longer
con.sendUnchoke();
// Does this run? if isInterested() is true, then con would have been in the fastest list
if (con.isInterested()) optimistic--;
// con isn't intersted and shouldn't be
} else {
// Tell it we're choking it
con.sendChoke();
}
}
}
}
/**
* Determine how many of our connections we should give data right now.
* This is the number of our peers that should be unchoked.
*
* As written, this method returns 100.
* It looks like it should return a number like 7, 2, 3, or 4, though, depending on how fast our Internet connection is letting us send data.
*
* @return 100
*/
private static int getNumUploads() {
// Read 100 from settings, and return it
int uploads = BittorrentSettings.TORRENT_MAX_CONNECTIONS.getValue();
if (uploads > 0) return uploads;
/*
* This method was copied directly from Bram Cohen's BitTorrent client.
*/
// Find out the speed the user is letting the program upload data at
float rate = UploadManager.getUploadSpeed(); // Returns the speed in bytes/s
// Sort by the speed
if (rate == Float.MAX_VALUE) return 7; // "unlimited, just guess something here..." -Bram
else if (rate < 9000) return 2; // Less than 9000 bytes/s, only send 2 computers data per torrent
else if (rate < 15000) return 3;
else if (rate < 42000) return 4;
else return (int)Math.sqrt(rate * 0.6f); // Calculate the number with this equation
}
/**
* Send all our connections Choke messages right now, or pick which to choke and send each a Choke or Unchoke message right now.
*
* Chokes all connections instantly and does not unchoke any of them until
* it is set to false again. This effectively suspends all uploads and it is
* used while we are moving the files from the incomplete folder to the
* complete folder.
*
* @param choke true to send each of our connections a Choke message right now
* false to pick which connections we want to choke or unchoke, and send a Choke or Unchoke message to each one
*/
private void setGlobalChoke(boolean choke) {
// Save the given setting
_globalChoke = choke;
// The caller told us to choke all connections
if (choke) {
// Loop for each of our connections
for (Iterator iter = getConnections().iterator(); iter.hasNext(); ) {
BTConnection btc = (BTConnection)iter.next();
// Send the remote computer a Choke message, telling it we won't be sending it any more data
btc.sendChoke();
}
// The caller told us to no longer choke all connections
} else {
// Decide which of our connections to choke or rechoke, and send each one a Choke or Unchoke message right now
rechoke();
}
}
/**
* Make a new thread named "TrackerRequest", and have it call announceBlocking(url, TrackerRequest.EVENT_NONE) right now.
* This is method 2 in the tracker contact loop.
*
* @param url The tracker's address on the Web
*/
private void announce(final URL url) {
/*
* offload tracker requests, - it simply takes too long even to execute
* it in our timer thread
*/
// Make a new thread named "TrackerRequest" that will run this run() method right now
Runnable trackerRequest = new Runnable() {
public void run() {
// Make a note
if (LOG.isDebugEnabled()) LOG.debug("announce thread for " + url.toString());
// Shouldn't this be if _stopped, not if not stopped? (do)
if (!_stopped) return;
// Contact our BitTorent tracker on the Web, add the IP addresses it gives us to our _peers list, and schedule the next time we'll contact it
announceBlocking(url, TrackerRequester.EVENT_NONE);
}
};
ManagedThread thread = new ManagedThread(trackerRequest, "TrackerRequest");
thread.setDaemon(true);
thread.start();
}
/**
* Contact our BitTorrent tracker on the Web, waiting here for up to 25 seconds for it to respond, and then give its response to handleTrackerResponse(tr, url).
* This is method 3 in the tracker contact loop.
*
* In addition to getting called in the tracker contact loop, announceComplete(), stopNow(), and announceStart() call this method.
*
* @param url A Java URL object that has the Web address of the tracker
* @param event The event to tell the tracker, like TrackerRequester.EVENT_STOP to say "event=stop"
*/
private void announceBlocking(final URL url, final int event) {
// Make a note in the debugging log
if (LOG.isDebugEnabled()) LOG.debug("connecting to tracker " + url.toString() + " for event " + event);
// Contact our BitTorent tracker on the Web, this method block while we're navigating to the tracker's address
TrackerResponse tr = TrackerRequester.request( // Returns a TrackerResponse object that represents the bencoded data of the tracker's response
url, // The Web address of the tracker
_info, // The BTMetaInfo object that has the information from the .torrent file
ManagedTorrent.this, // This ManagedTorrent object (do) why isn't this just "this" instead of "ManagedTorrent.this"
event); // The requested event, like TrackerRequester.EVENT_STOP to include "event=stop"
// Add the IP addresses the tracker told us to our _peers list, and schedule the next time we'll contact it
handleTrackerResponse(tr, url);
}
/**
* Get the number of IP addresses to remote computers sharing this torrent that we know about.
* Our tracker told us these addresses.
* We can try connecting to them to share this torrent with them.
*
* Returns the size of the _peers list, which contains TorrentLocation objects.
*
* @return The number of addresses we know
*/
public int getNumberOfAlternateLocations() {
// Return the number of TorrentLocations we have in the _peers list
return _peers.size();
}
/**
* Get the number of addresses the tracker told us that we tried to connect to, but couldn't, in the last hour.
*
* @return The number of TorrentLocation objects in our _badPeers list
*/
public int getNumberOfInvalidAlternateLocations() {
// Return the number of TorrentLocation objects we've added to _badPeers after having trouble connecting to them
return _badPeers.size();
}
/**
* Determine if this torrent is paused, and not transferring any data because of that.
* Use the pause() and resume() methods to set this condition.
*
* @return true if this torrent is paused, false if it's not paused
*/
public boolean isPaused() {
// Return the value of the _paused flag
return _paused;
}
/**
* Determine if this ManagedTorrent object is the same as another one.
* Compares their info hashes, the SHA1 hash of the "info" bencoded dictionary in the .torrent file.
*
* @param o The ManagedTorrent object to compare this one to
*/
public boolean equals(Object o) {
// Make sure the given object is a ManagedTorrent
if (!(o instanceof ManagedTorrent)) return false; // Different kind of object, false, not the same
ManagedTorrent mt = (ManagedTorrent)o;
// Compare their info hashes
return Arrays.equals(mt.getInfoHash(), getInfoHash());
}
/**
* Determine which remote computers are giving us data the fastest.
*
* The DownloadSpeedComparator object implements Java's Comparator class.
* This means you can hand a DownloadSpeedComparator object to a method like Collections.sort().
* It will use the compare() method here to determine which of two objects should be listed first.
*
* The ManagedTorrent class makes a single DownloadSpeedComparator, named DOWNLOAD_SPEED_COMPARATOR.
* Rechoker.run() uses it to sort our list of interested remote computers into order based on how fast they are giving us data.
*/
public static class DownloadSpeedComparator implements Comparator {
/**
* Determine which of two remote computers is giving us data the fastest.
*
* @param o1 A BTConnection object that represents our TCP socket connection to a remote computer sharing the same torrent as us.
* @param o2 A BTConnection object that represents the same thing, the second computer to compare with the first.
* @return -1 if the first computer is giving us data faster than the second.
* 1 if the second computer is giving us data the fastest.
* 0 if it is a tie.
*/
public int compare(Object o1, Object o2) {
// Look at both given objects as BTConnection objects
BTConnection c1 = (BTConnection)o1;
BTConnection c2 = (BTConnection)o2;
// A number to compare the download speeds
float bw = 0;
try {
// Subtract the BTConnection object's current speeds, bw will be positive if c1 is giving us data faster
bw = c1.getMessageReader().getBandwidthTracker().getMeasuredBandwidth() - c2.getMessageReader().getBandwidthTracker().getMeasuredBandwidth();
// Only base our decision on download speed if the difference is bigger than 0.1 KB/s
if (bw > 0.1) return -1; // The first computer is giving us data faster than the second, return -1
else if (bw < -0.1) return 1; // The second computer is giving us data the fastest, return 1
// One of the calls to getMeasuredBandwidth() couldn't calculate the current speed because we don't have 3 seconds of data yet, decide below
} catch (InsufficientDataException ide) {}
/*
* The computer's current speeds are too close, or we don't have enough information about one or both.
* Use their total average speeds instead.
*/
// Compare their total average bandwidth instead, bw will be positive if c1 has given us more data faster
bw = c1.getMessageReader().getBandwidthTracker().getAverageBandwidth() - c2.getMessageReader().getBandwidthTracker().getAverageBandwidth();
// Look at the result
if (bw > 0) return -1; // The first computer has given us more data faster, return -1
else if (bw < 0) return 1; // The second computer is faster, return 1
return 0; // It's a tie
}
/**
* Not implemented.
*
* @return false
*/
public boolean equals(Object o) {
// Always return false, say the objects are different
return false;
}
}
/**
* We don't have a download throttle yet.
*
* @return null
*/
public Throttle getDownloadThrottle() {
// Return null because we have no download throttle
return null;
}
/**
* Get a reference to the TorrentManager's NBThrottle object.
*
* The program has a single TorrentManager object that keeps a list of ManagedTorrent objects like this one.
* The TorrentManager object has a NBThrottle object, a non-blocking throttle.
*
* @return The program's NBThrottle for BitTorrent
*/
public Throttle getUploadThrottle() {
// Return a reference to the TorrentManager object's single NBThrottle object
return _manager.getUploadThrottle();
}
/**
* Add a given object to our ProcessingQueue, which will have the "ManagedTorrent" thread call run() on it.
*
* @param runnable An object with a run() method
*/
public void enqueueTask(Runnable runnable) {
// Add the given object to our ProcessingQueue
_processingQueue.add(runnable);
}
/**
* Determine if we have any TCP socket connections open to remote computers we're sharing this torrent with.
*
* @return true if we're sharing this torrent with at least one computer, false if we're not
*/
public boolean isConnected() {
// If our _connections list has at least 1 BTConnection object, return true
return _connections.size() > 0;
}
/**
* Determine if we've stopped this torrent.
* This means we're not sending or receiving file data for it any longer.
*
* When the program calls stopNow() to stop transferring data for this torrent, it sets _stopped to true, and hasStopped() returns true.
*
* @return True if the program has called stopNow() to stop this torrent
*/
public boolean hasStopped() {
// Check the _stopped flag
return _stopped;
}
/**
* Get a list of the TCP socket connections we've made to remote computers sharing this torrent.
*
* @return A CoWList of BTConnection objects
*/
public List getConnections() {
// Return a reference to our connections list
return _connections;
}
/**
* Find out how many remote computers we have open connections to sharing this torrent through.
*
* @return The size of our connections list
*/
public int getNumConnections() {
// Return the number of BTConnection objects in the _connections list
return _connections.size();
}
/**
* Find out how many addresses we've failed connecting to in the last hour.
* We tried to connect to these addresses to share this torrent.
*
* @return The number of addresses we were unable to connect to in the last hour
*/
public int getNumBadPeers() {
// Return the number of TorrentLocation objects addBadEndpoint() has put in the _badPeers list
return _badPeers.size();
}
/**
* Find out how many addresses we know of remote computers sharing this torrent.
* The tracker told us these addresses.
* We're not connected to any of these addresses now, and haven't tried any of them yet.
* We could try connecting to them to share this torrent.
*
* @return The number of addresses in our list
*/
public int getNumPeers() {
// Return the number of TorrentLocation objects in our _peers list
return _peers.size();
}
/**
* Find out how many connections we have open to remote computers that don't have any parts of this torrent that we want.
* Loops through our connections, counting those that we're not interested in.
* These computers have nothing we want.
*
* @return The number of remote computers we're connected to, but not interested in
*/
public int getNumBusyPeers() {
// Make an int to count how many computers have nothing we want
int busy = 0;
// Loop for each of our open connections
for (Iterator iter = _connections.iterator(); iter.hasNext(); ) {
BTConnection con = (BTConnection)iter.next();
// If this remote computer isn't interesting to us, count it
if (!con.isInteresting()) busy++;
}
// Return the number of computers that have nothing we want
return busy;
}
/**
* Get the BTUploader object that lets LimeWire's GUI list this torrent with the Gnutella uploads.
*
* @return A reference to this ManagedTorrent's BTUploader
*/
public BTUploader getUploader() {
// Return the object our constructor made
return _uploader;
}
/**
* Get the BTDownloader object that lets LimeWire's GUI list this torrent with the Gnutella downloads.
*
* @return A reference to this ManagedTorrent's BTDownloader
*/
public BTDownloader getDownloader() {
// Return the object our constructor made
return _downloader;
}
/**
* Determine if we have any addresses to try connecting to.
* Looks in our _peers list for a TorrentLocation object that we haven't tried in the last 5 minutes.
*
* @return True if we have a TorrentLocation we can try now
*/
boolean hasNonBusyLocations() {
// Get the time now
long now = System.currentTimeMillis();
// Make sure only one thread accesses the _peers list at a time
synchronized (_peers) {
// Loop for each of the TorrentLocation objects in our _peers list, these are addresses our tracker told us that we can try connecting to
Iterator iter = _peers.iterator();
while (iter.hasNext()) {
TorrentLocation to = (TorrentLocation)iter.next();
// If we haven't failed connecting to this address in the last 5 minutes, return true
if (!to.isBusy(now)) return true;
}
}
// Our _peers list doesn't have any addresses for us to try right now
return false;
}
/**
* Determine if we should stop sharing this torrent.
* Looks for several conditions to return true, indicating we should give up.
* If we don't have any connections nor any addresses to try, and we've had trouble connecting to our tracker, give up.
* If we don't have any connections nor any addresses to try, and the TorrentManager says we should make room for other torrents, give up.
* If the TorrentManager says we should make room for other torrents, and we've uploaded more data for this torrent than we've downloaded, give up.
*
* @return true if we should give up, false to keep sharing this torrent
*/
boolean shouldStop() {
// We don't have any connections nor any addresses to try
if (_connections.size() == 0 && // If we don't have any open connections to peers sharing this torrent, and
_peers.size() == 0) { // We don't have any addresses to try to connect to
// If we've had trouble connecting to our tracker more than 5 times
if (_trackerFailures > MAX_TRACKER_FAILURES) {
// Give up
if (LOG.isDebugEnabled()) LOG.debug("giving up, trackerFailures " + _trackerFailures);
return true;
// Our tracker is reachable, but the TorrentManager says we should make room for other torrents
} else if (_manager.shouldYield()) {
// Give up
if (LOG.isDebugEnabled()) LOG.debug("making way for other downloader");
return true;
}
// We have some connections or addresses, and we're done with this torrent and the TorrentManager says we should make room for another
} else if (_folder.isComplete() && _manager.shouldYield()) {
/*
* we stop if we uploaded more than we downloaded
* AND there are other torrents waiting for a slot
*/
// Make a note of the upload and download sizes we'll use to make a decision
if (LOG.isDebugEnabled()) LOG.debug("uploaded data " + _uploader.getTotalAmountUploaded() + " downloaded data " + _downloader.getTotalAmountDownloaded());
// If we've given out more data for this torrent than we've received, give up
if (_uploader.getTotalAmountUploaded() > _downloader.getTotalAmountDownloaded()) return true;
}
// Keep sharing this torrent
return false;
}
/**
* Get the BTConnectionFetcher this ManagedTorrent uses to keep 5 open connections to remote computers sharing this torrent.
*
* @return A reference to our BTConnectionFetcher object
*/
public BTConnectionFetcher getFetcher() {
// Return the BTConnectionFetcher that initializeTorrent() made
return _connectionFetcher;
}
}