package com.limegroup.gnutella; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.methods.HeadMethod; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.bitzi.util.Base32; import com.limegroup.gnutella.browser.MagnetOptions; import com.limegroup.gnutella.downloader.CantResumeException; import com.limegroup.gnutella.downloader.IncompleteFileManager; import com.limegroup.gnutella.downloader.MagnetDownloader; import com.limegroup.gnutella.downloader.ManagedDownloader; import com.limegroup.gnutella.downloader.RequeryDownloader; import com.limegroup.gnutella.downloader.ResumeDownloader; import com.limegroup.gnutella.downloader.InNetworkDownloader; import com.limegroup.gnutella.filters.IPFilter; import com.limegroup.gnutella.http.HttpClientManager; import com.limegroup.gnutella.io.ConnectObserver; import com.limegroup.gnutella.io.Shutdownable; import com.limegroup.gnutella.messages.BadPacketException; import com.limegroup.gnutella.messages.Message; import com.limegroup.gnutella.messages.PushRequest; import com.limegroup.gnutella.messages.QueryReply; import com.limegroup.gnutella.messages.QueryRequest; import com.limegroup.gnutella.search.HostData; import com.limegroup.gnutella.settings.ConnectionSettings; import com.limegroup.gnutella.settings.DownloadSettings; import com.limegroup.gnutella.settings.SharingSettings; import com.limegroup.gnutella.settings.UpdateSettings; import com.limegroup.gnutella.statistics.DownloadStat; import com.limegroup.gnutella.statistics.HTTPStat; import com.limegroup.gnutella.udpconnect.UDPConnection; import com.limegroup.gnutella.util.CommonUtils; import com.limegroup.gnutella.util.ConverterObjectInputStream; import com.limegroup.gnutella.util.DefaultThreadPool; import com.limegroup.gnutella.util.DualIterator; import com.limegroup.gnutella.util.FileUtils; import com.limegroup.gnutella.util.IOUtils; import com.limegroup.gnutella.util.IpPort; import com.limegroup.gnutella.util.ManagedThread; import com.limegroup.gnutella.util.NetworkUtils; import com.limegroup.gnutella.util.ProcessingQueue; import com.limegroup.gnutella.util.ThreadPool; import com.limegroup.gnutella.util.ThreadFactory; import com.limegroup.gnutella.util.URLDecoder; import com.limegroup.gnutella.util.IntWrapper; import com.limegroup.gnutella.version.DownloadInformation; import com.limegroup.gnutella.version.UpdateHandler; import com.limegroup.gnutella.version.UpdateInformation; /** * The list of all downloads in progress. DownloadManager has a fixed number * of download slots given by the MAX_SIM_DOWNLOADS property. It is * responsible for starting downloads and scheduling and queueing them as * needed. This class is thread safe.<p> * * As with other classes in this package, a DownloadManager instance may not be * used until initialize(..) is called. The arguments to this are not passed * in to the constructor in case there are circular dependencies.<p> * * DownloadManager provides ways to serialize download state to disk. Reads * are initiated by RouterService, since we have to wait until the GUI is * initiated. Writes are initiated by this, since we need to be notified of * completed downloads. Downloads in the COULDNT_DOWNLOAD state are not * serialized. */ public class DownloadManager implements BandwidthTracker, ConnectionAcceptor { private static final Log LOG = LogFactory.getLog(DownloadManager.class); /** The time in milliseconds between checkpointing downloads.dat. The more * often this is written, the less the lost data during a crash, but the * greater the chance that downloads.dat itself is corrupt. */ private int SNAPSHOT_CHECKPOINT_TIME=30*1000; //30 seconds /** The callback for notifying the GUI of major changes. */ private DownloadCallback callback; /** The callback for innetwork downloaders. */ private DownloadCallback innetworkCallback; /** The message router to use for pushes. */ private MessageRouter router; /** Used to check if the file exists. */ private FileManager fileManager; /** The repository of incomplete files * INVARIANT: incompleteFileManager is same as those of all downloaders */ private IncompleteFileManager incompleteFileManager =new IncompleteFileManager(); /** The list of all ManagedDownloader's attempting to download. * INVARIANT: active.size()<=slots() && active contains no duplicates * LOCKING: obtain this' monitor */ private List /* of ManagedDownloader */ active=new LinkedList(); /** The list of all queued ManagedDownloader. * INVARIANT: waiting contains no duplicates * LOCKING: obtain this' monitor */ private List /* of ManagedDownloader */ waiting=new LinkedList(); /** * Whether or not the GUI has been init'd. */ private volatile boolean guiInit = false; /** The number if IN-NETWORK active downloaders. We don't count these when * determing how many downloaders are active. */ private int innetworkCount = 0; /** * number of files that we have sent a udp push for and are waiting a connection. * LOCKING: obtain UDP_FAILOVER if manipulating the contained sets as well! */ private final Map /* of byte[] guids -> IntWrapper */ UDP_FAILOVER = new TreeMap(new GUID.GUIDByteComparator()); /** * A sequentially processed list of PushFailoverRequestors, used to process * TCP pushes a short bit of time after the UDP push is sent. */ private final ProcessingQueue FAILOVERS = new ProcessingQueue("udp failovers"); /** * how long we think should take a host that receives an udp push * to connect back to us. */ private static long UDP_PUSH_FAILTIME=5000; /** The global minimum time between any two requeries, in milliseconds. * @see com.limegroup.gnutella.downloader.ManagedDownloader#TIME_BETWEEN_REQUERIES*/ public static long TIME_BETWEEN_REQUERIES = 45 * 60 * 1000; /** The last time that a requery was sent. */ private long lastRequeryTime = 0; /** This will hold the MDs that have sent requeries. * When this size gets too big - meaning bigger than active.size(), then * that means that all MDs have been serviced at least once, so you can * clear it and start anew.... */ private List querySentMDs = new ArrayList(); /** * The number of times we've been bandwidth measures */ private int numMeasures = 0; /** * The average bandwidth over all downloads */ private float averageBandwidth = 0; /** * The runnable that pumps inactive downloads to the correct state. */ private Runnable _waitingPump; //////////////////////// Creation and Saving ///////////////////////// /** * Initializes this manager. <b>This method must be called before any other * methods are used.</b> * @uses RouterService.getCallback for the UI callback * to notify of download changes * @uses RouterService.getMessageRouter for the message * router to use for sending push requests * @uses RouterService.getFileManager for the FileManager * to check if files exist */ public void initialize() { initialize( RouterService.getCallback(), RouterService.getMessageRouter(), RouterService.getFileManager() ); } protected void initialize(DownloadCallback guiCallback, MessageRouter router, FileManager fileManager) { this.callback = guiCallback; this.innetworkCallback = new InNetworkCallback(); this.router = router; this.fileManager = fileManager; scheduleWaitingPump(); RouterService.getConnectionDispatcher(). addConnectionAcceptor(this, new String[]{"GIV"}, false, true); } /** * Performs the slow, low-priority initialization tasks: reading in * snapshots and scheduling snapshot checkpointing. */ public void postGuiInit() { File real = SharingSettings.DOWNLOAD_SNAPSHOT_FILE.getValue(); File backup = SharingSettings.DOWNLOAD_SNAPSHOT_BACKUP_FILE.getValue(); // Try once with the real file, then with the backup file. if( !readSnapshot(real) ) { LOG.debug("Reading real downloads.dat failed"); // if backup succeeded, copy into real. if( readSnapshot(backup) ) { LOG.debug("Reading backup downloads.bak succeeded."); copyBackupToReal(); // only show the error if the files existed but couldn't be read. } else if(backup.exists() || real.exists()) { LOG.debug("Reading both downloads files failed."); MessageService.showError("DOWNLOAD_COULD_NOT_READ_SNAPSHOT"); } } else { LOG.debug("Reading downloads.dat worked!"); } Runnable checkpointer=new Runnable() { public void run() { if (downloadsInProgress() > 0) { //optimization // If the write failed, move the backup to the real. if(!writeSnapshot()) copyBackupToReal(); } } }; RouterService.schedule(checkpointer, SNAPSHOT_CHECKPOINT_TIME, SNAPSHOT_CHECKPOINT_TIME); guiInit = true; } /** * Is the GUI init'd? */ public boolean isGUIInitd() { return guiInit; } /** * Determines if an 'In Network' download exists in either active or waiting. */ public synchronized boolean hasInNetworkDownload() { if(innetworkCount > 0) return true; for(Iterator i = waiting.iterator(); i.hasNext(); ) { if(i.next() instanceof InNetworkDownloader) return true; } return false; } /** * Kills all in-network downloaders that are not present in the list of URNs * @param urns a current set of urns that we are downloading in-network. */ public synchronized void killDownloadersNotListed(Collection updates) { if (updates == null) return; Set urns = new HashSet(updates.size()); for (Iterator iter = updates.iterator(); iter.hasNext();) { UpdateInformation ui = (UpdateInformation) iter.next(); urns.add(ui.getUpdateURN().httpStringValue()); } for (Iterator iter = new DualIterator(waiting.iterator(),active.iterator()); iter.hasNext();) { Downloader d = (Downloader)iter.next(); if (d instanceof InNetworkDownloader && !urns.contains(d.getSHA1Urn().httpStringValue())) d.stop(); } Set hopeless = UpdateSettings.FAILED_UPDATES.getValue(); hopeless.retainAll(urns); UpdateSettings.FAILED_UPDATES.setValue(hopeless); } /** * Schedules the runnable that pumps through waiting downloads. */ public void scheduleWaitingPump() { if(_waitingPump != null) return; _waitingPump = new Runnable() { public void run() { pumpDownloads(); } }; RouterService.schedule(_waitingPump, 1000, 1000); } /** * Pumps through each waiting download, either removing it because it was * stopped, or adding it because there's an active slot and it requires * attention. */ private synchronized void pumpDownloads() { int index = 1; for(Iterator i = waiting.iterator(); i.hasNext(); ) { ManagedDownloader md = (ManagedDownloader)i.next(); if(md.isAlive()) { continue; } else if(md.isCancelled() ||md.isCompleted()) { i.remove(); cleanupCompletedDownload(md, false); } else if(hasFreeSlot() && (md.hasNewSources() || md.getRemainingStateTime() <= 0)) { i.remove(); if(md instanceof InNetworkDownloader) innetworkCount++; active.add(md); md.startDownload(); } else { if(!md.isPaused()) md.setInactivePriority(index++); md.handleInactivity(); } } } /** * Copies the backup downloads.dat (downloads.bak) file to the * the real downloads.dat location. */ private synchronized void copyBackupToReal() { File real = SharingSettings.DOWNLOAD_SNAPSHOT_FILE.getValue(); File backup = SharingSettings.DOWNLOAD_SNAPSHOT_BACKUP_FILE.getValue(); real.delete(); CommonUtils.copy(backup, real); } /** * Determines if the given URN has an incomplete file. */ public boolean isIncomplete(URN urn) { return incompleteFileManager.getFileForUrn(urn) != null; } /** * Returns the IncompleteFileManager used by this DownloadManager * and all ManagedDownloaders. */ public IncompleteFileManager getIncompleteFileManager() { return incompleteFileManager; } public synchronized int downloadsInProgress() { return active.size() + waiting.size(); } public synchronized int getNumIndividualDownloaders() { int ret = 0; for (Iterator iter=active.iterator(); iter.hasNext(); ) { //active ManagedDownloader md=(ManagedDownloader)iter.next(); ret += md.getNumDownloaders(); } return ret; } public synchronized int getNumActiveDownloads() { return active.size() - innetworkCount; } public synchronized int getNumWaitingDownloads() { return waiting.size(); } public ManagedDownloader getDownloaderForURN(URN sha1) { synchronized(this) { for (Iterator iter = active.iterator(); iter.hasNext();) { ManagedDownloader current = (ManagedDownloader) iter.next(); if (current.getSHA1Urn() != null && sha1.equals(current.getSHA1Urn())) return current; } for (Iterator iter = waiting.iterator(); iter.hasNext();) { ManagedDownloader current = (ManagedDownloader) iter.next(); if (current.getSHA1Urn() != null && sha1.equals(current.getSHA1Urn())) return current; } } return null; } public synchronized boolean isGuidForQueryDownloading(GUID guid) { for (Iterator iter=active.iterator(); iter.hasNext(); ) { GUID dGUID = ((ManagedDownloader) iter.next()).getQueryGUID(); if ((dGUID != null) && (dGUID.equals(guid))) return true; } for (Iterator iter=waiting.iterator(); iter.hasNext(); ) { GUID dGUID = ((ManagedDownloader) iter.next()).getQueryGUID(); if ((dGUID != null) && (dGUID.equals(guid))) return true; } return false; } /** * Clears all downloads. */ public void clearAllDownloads() { List buf; synchronized(this) { buf = new ArrayList(active.size() + waiting.size()); buf.addAll(active); buf.addAll(waiting); active.clear(); waiting.clear(); } for(Iterator i = buf.iterator(); i.hasNext(); ) { ManagedDownloader md = (ManagedDownloader)i.next(); md.stop(); } } /** Writes a snapshot of all downloaders in this and all incomplete files to * the file named DOWNLOAD_SNAPSHOT_FILE. It is safe to call this method * at any time for checkpointing purposes. Returns true iff the file was * successfully written. */ boolean writeSnapshot() { List buf; synchronized(this) { buf = new ArrayList(active.size() + waiting.size()); buf.addAll(active); buf.addAll(waiting); } File outFile = SharingSettings.DOWNLOAD_SNAPSHOT_FILE.getValue(); //must delete in order for renameTo to work. SharingSettings.DOWNLOAD_SNAPSHOT_BACKUP_FILE.getValue().delete(); outFile.renameTo( SharingSettings.DOWNLOAD_SNAPSHOT_BACKUP_FILE.getValue()); // Write list of active and waiting downloaders, then block list in // IncompleteFileManager. ObjectOutputStream out = null; try { out=new ObjectOutputStream( new BufferedOutputStream( new FileOutputStream( SharingSettings.DOWNLOAD_SNAPSHOT_FILE.getValue()))); out.writeObject(buf); //Blocks can be written to incompleteFileManager from other threads //while this downloader is being serialized, so lock is needed. synchronized (incompleteFileManager) { out.writeObject(incompleteFileManager); } out.flush(); return true; } catch (IOException e) { return false; } finally { if (out != null) try {out.close();}catch(IOException ignored){} } } /** Reads the downloaders serialized in DOWNLOAD_SNAPSHOT_FILE and adds them * to this, queued. The queued downloads will restart immediately if slots * are available. Returns false iff the file could not be read for any * reason. THIS METHOD SHOULD BE CALLED BEFORE ANY GUI ACTION. * It is public for testing purposes only! * @param file the downloads.dat snapshot file */ public synchronized boolean readSnapshot(File file) { //Read downloaders from disk. List buf=null; try { ObjectInputStream in = new ConverterObjectInputStream( new BufferedInputStream( new FileInputStream(file))); //This does not try to maintain backwards compatibility with older //versions of LimeWire, which only wrote the list of downloaders. //Note that there is a minor race condition here; if the user has //started some downloads before this method is called, the new and //old downloads will use different IncompleteFileManager instances. //This doesn't really cause an errors, however. buf=(List)in.readObject(); incompleteFileManager=(IncompleteFileManager)in.readObject(); } catch(Throwable t) { LOG.error("Unable to read download file", t); return false; } // Pump the downloaders through a set, to remove duplicate values. // This is necessary in case LimeWire got into a state where a // downloader was written to disk twice. buf = new LinkedList(new HashSet(buf)); //Initialize and start downloaders. Must catch ClassCastException since //the data could be corrupt. This code is a little tricky. It is //important that instruction (3) follow (1) and (2), because we must not //pass an uninitialized Downloader to the GUI. (The call to getFileName //will throw NullPointerException.) I believe the relative order of (1) //and (2) does not matter since this' monitor is held. (The download //thread must obtain the monitor to acquire a queue slot.) try { for (Iterator iter=buf.iterator(); iter.hasNext(); ) { ManagedDownloader downloader=(ManagedDownloader)iter.next(); DownloadCallback dc = callback; // ignore RequeryDownloaders -- they're legacy if(downloader instanceof RequeryDownloader) continue; waiting.add(downloader); //1 downloader.initialize(this, this.fileManager, callback(downloader)); //2 callback(downloader).addDownload(downloader); //3 } return true; } catch (ClassCastException e) { return false; } finally { // Remove entries that are too old or no longer existent and not actively // downloaded. if (incompleteFileManager.initialPurge(getActiveDownloadFiles(buf))) writeSnapshot(); } } private static Collection getActiveDownloadFiles(List downloaders) { List ret = new ArrayList(downloaders.size()); for (Iterator iter = downloaders.iterator(); iter.hasNext();) { Downloader d = (Downloader) iter.next(); File f = d.getFile(); if (f != null) { try { ret.add(FileUtils.getCanonicalFile(f)); } catch (IOException iox) { ret.add(f.getAbsoluteFile()); } } } return ret; } ////////////////////////// Main Public Interface /////////////////////// /** * Tries to "smart download" any of the given files.<p> * * If any of the files already being downloaded (or queued for downloaded) * has the same temporary name as any of the files in 'files', throws * AlreadyDownloadingException. Note, however, that this doesn't guarantee * that a successfully downloaded file can be moved to the library.<p> * * If overwrite==false, then if any of the files already exists in the * download directory, FileExistsException is thrown and no files are * modified. If overwrite==true, the files may be overwritten.<p> * * Otherwise returns a Downloader that allows you to stop and resume this * download. The DownloadCallback will also be notified of this download, * so the return value can usually be ignored. The download begins * immediately, unless it is queued. It stops after any of the files * succeeds. * * @param queryGUID the guid of the query that resulted in the RFDs being * downloaded. * @param saveDir can be null, then the default save directory is used * @param fileName can be null, then the first filename of one of element of * <code>files</code> is taken. * @throws SaveLocationException when there was an error setting the * location of the final download destination. * * @modifies this, disk */ public synchronized Downloader download(RemoteFileDesc[] files, List alts, GUID queryGUID, boolean overwrite, File saveDir, String fileName) throws SaveLocationException { String fName = getFileName(files, fileName); if (conflicts(files, fName)) { throw new SaveLocationException (SaveLocationException.FILE_ALREADY_DOWNLOADING, new File(fName != null ? fName : "")); } //Purge entries from incompleteFileManager that have no corresponding //file on disk. This protects against stupid users who delete their //temporary files while LimeWire is running, either through the command //prompt or the library. Note that you could optimize this by just //purging files corresponding to the current download, but it's not //worth it. incompleteFileManager.purge(); //Start download asynchronously. This automatically moves downloader to //active if it can. ManagedDownloader downloader = new ManagedDownloader(files, incompleteFileManager, queryGUID, saveDir, fileName, overwrite); initializeDownload(downloader); //Now that the download is started, add the sources w/o caching downloader.addDownload(alts,false); return downloader; } /** * Creates a new MAGNET downloader. Immediately tries to download from * <tt>defaultURL</tt>, if specified. If that fails, or if defaultURL does * not provide alternate locations, issues a requery with <tt>textQuery</tt> * and </tt>urn</tt>, as provided. (At least one must be non-null.) If * <tt>filename</tt> is specified, it will be used as the name of the * complete file; otherwise it will be taken from any search results or * guessed from <tt>defaultURLs</tt>. * * @param urn the hash of the file (exact topic), or null if unknown * @param textQuery requery keywords (keyword topic), or null if unknown * @param filename the final file name, or <code>null</code> if unknown * @param saveLocation can be null, then the default save location is used * @param defaultURLs the initial locations to try (exact source), or null * if unknown * * @exception IllegalArgumentException all urn, textQuery, filename are * null * @throws SaveLocationException */ public synchronized Downloader download(MagnetOptions magnet, boolean overwrite, File saveDir, String fileName) throws IllegalArgumentException, SaveLocationException { if (!magnet.isDownloadable()) throw new IllegalArgumentException("magnet not downloadable"); //remove entry from IFM if the incomplete file was deleted. incompleteFileManager.purge(); if (fileName == null) { fileName = magnet.getFileNameForSaving(); } if (conflicts(magnet.getSHA1Urn(), fileName, 0)) { throw new SaveLocationException (SaveLocationException.FILE_ALREADY_DOWNLOADING, new File(fileName)); } //Note: If the filename exists, it would be nice to check that we are //not already downloading the file by calling conflicts with the //filename...the problem is we cannot do this effectively without the //size of the file (atleast, not without being risky in assuming that //two files with the same name are the same file). So for now we will //just leave it and download the same file twice. //Instantiate downloader, validating incompleteFile first. MagnetDownloader downloader = new MagnetDownloader(incompleteFileManager, magnet, overwrite, saveDir, fileName); initializeDownload(downloader); return downloader; } /** * Starts a resume download for the given incomplete file. * @exception CantResumeException incompleteFile is not a valid * incomplete file * @throws SaveLocationException */ public synchronized Downloader download(File incompleteFile) throws CantResumeException, SaveLocationException { if (conflictsWithIncompleteFile(incompleteFile)) { throw new SaveLocationException (SaveLocationException.FILE_ALREADY_DOWNLOADING, incompleteFile); } //Check if file exists. TODO3: ideally we'd pass ALL conflicting files //to the GUI, so they know what they're overwriting. //if (! overwrite) { // try { // File downloadDir=SettingsManager.instance().getSaveDirectory(); // File completeFile=new File( // downloadDir, // incompleteFileManager.getCompletedName(incompleteFile)); // if (completeFile.exists()) // throw new FileExistsException(filename); // } catch (IllegalArgumentException e) { // throw new CantResumeException(incompleteFile.getName()); // } //} //Purge entries from incompleteFileManager that have no corresponding //file on disk. This protects against stupid users who delete their //temporary files while LimeWire is running, either through the command //prompt or the library. Note that you could optimize this by just //purging files corresponding to the current download, but it's not //worth it. incompleteFileManager.purge(); //Instantiate downloader, validating incompleteFile first. ResumeDownloader downloader=null; try { incompleteFile = FileUtils.getCanonicalFile(incompleteFile); String name=IncompleteFileManager.getCompletedName(incompleteFile); int size=ByteOrder.long2int( IncompleteFileManager.getCompletedSize(incompleteFile)); downloader = new ResumeDownloader(incompleteFileManager, incompleteFile, name, size); } catch (IllegalArgumentException e) { throw new CantResumeException(incompleteFile.getName()); } catch (IOException ioe) { throw new CantResumeException(incompleteFile.getName()); } initializeDownload(downloader); return downloader; } /** * Downloads an InNetwork update, using the info from the DownloadInformation. */ public synchronized Downloader download(DownloadInformation info, long now) throws SaveLocationException { File dir = FileManager.PREFERENCE_SHARE; dir.mkdirs(); File f = new File(dir, info.getUpdateFileName()); if(conflicts(info.getUpdateURN(), info.getUpdateFileName(), (int)info.getSize())) throw new SaveLocationException(SaveLocationException.FILE_ALREADY_DOWNLOADING, f); incompleteFileManager.purge(); ManagedDownloader d = new InNetworkDownloader(incompleteFileManager, info, dir, now); initializeDownload(d); return d; } /** * Performs common tasks for initializing the download. * 1) Initializes the downloader. * 2) Adds the download to the waiting list. * 3) Notifies the callback about the new downloader. * 4) Writes the new snapshot out to disk. */ private void initializeDownload(ManagedDownloader md) { md.initialize(this, fileManager, callback(md)); waiting.add(md); callback(md).addDownload(md); RouterService.schedule(new Runnable() { public void run() { writeSnapshot(); // Save state for crash recovery. } },0,0); } /** * Returns the callback that should be used for the given md. */ private DownloadCallback callback(ManagedDownloader md) { return (md instanceof InNetworkDownloader) ? innetworkCallback : callback; } /** * Returns true if there already exists a download for the same file. * <p> * Same file means: same urn, or as fallback same filename + same filesize * @param rfds * @return */ private boolean conflicts(RemoteFileDesc[] rfds, String fileName) { URN urn = null; for (int i = 0; i < rfds.length && urn == null; i++) { urn = rfds[0].getSHA1Urn(); } return conflicts(urn, fileName, rfds[0].getSize()); } /** * Returns <code>true</code> if there already is a download with the same urn. * @param urn may be <code>null</code>, then a check based on the fileName * and the fileSize is performed * @return */ public boolean conflicts(URN urn, String fileName, int fileSize) { if (urn == null && fileSize == 0) { return false; } synchronized (this) { return conflicts(active.iterator(), urn, fileName, fileSize) || conflicts(waiting.iterator(), urn, fileName, fileSize); } } private boolean conflicts(Iterator i, URN urn, String fileName, int fileSize) { while(i.hasNext()) { ManagedDownloader md = (ManagedDownloader)i.next(); if (md.conflicts(urn, fileName, fileSize)) { return true; } } return false; } /** * Returns <code>true</code> if there already is a download that is or * will be saving to this file location. * @param candidateFile the final file location. * @return */ public synchronized boolean isSaveLocationTaken(File candidateFile) { return isSaveLocationTaken(active.iterator(), candidateFile) || isSaveLocationTaken(waiting.iterator(), candidateFile); } private boolean isSaveLocationTaken(Iterator i, File candidateFile) { while(i.hasNext()) { ManagedDownloader md = (ManagedDownloader)i.next(); if (candidateFile.equals(md.getSaveFile())) { return true; } } return false; } private synchronized boolean conflictsWithIncompleteFile(File incompleteFile) { return conflictsWithIncompleteFile(active.iterator(), incompleteFile) || conflictsWithIncompleteFile(waiting.iterator(), incompleteFile); } private boolean conflictsWithIncompleteFile(Iterator i, File incompleteFile) { while(i.hasNext()) { ManagedDownloader md = (ManagedDownloader)i.next(); if (md.conflictsWithIncompleteFile(incompleteFile)) { return true; } } return false; } /** * Adds all responses (and alternates) in qr to any downloaders, if * appropriate. */ public void handleQueryReply(QueryReply qr) { // first check if the qr is of 'sufficient quality', if not just // short-circuit. if (qr.calculateQualityOfService( !RouterService.acceptedIncomingConnection()) < 1) return; List responses; HostData data; try { responses = qr.getResultsAsList(); data = qr.getHostData(); } catch(BadPacketException bpe) { return; // bad packet, do nothing. } addDownloadWithResponses(responses, data); } /** * Iterates through all responses seeing if they can be matched * up to any existing downloaders, adding them as possible * sources if they do. */ private void addDownloadWithResponses(List responses, HostData data) { if(responses == null) throw new NullPointerException("null responses"); if(data == null) throw new NullPointerException("null hostdata"); // need to synch because active and waiting are not thread safe List downloaders = new ArrayList(active.size() + waiting.size()); synchronized (this) { // add to all downloaders, even if they are waiting.... downloaders.addAll(active); downloaders.addAll(waiting); } // short-circuit. if(downloaders.isEmpty()) return; //For each response i, offer it to each downloader j. Give a response // to at most one downloader. // TODO: it's possible that downloader x could accept response[i] but //that would cause a conflict with downloader y. Check for this. for(Iterator i = responses.iterator(); i.hasNext(); ) { Response r = (Response)i.next(); // Don't bother with making XML from the EQHD. RemoteFileDesc rfd = r.toRemoteFileDesc(data); for(Iterator j = downloaders.iterator(); j.hasNext(); ) { ManagedDownloader currD = (ManagedDownloader)j.next(); // If we were able to add this specific rfd, // add any alternates that this response might have // also. if (currD.addDownload(rfd, true)) { Set alts = r.getLocations(); for(Iterator k = alts.iterator(); k.hasNext(); ) { Endpoint ep = (Endpoint)k.next(); // don't cache alts. currD.addDownload(new RemoteFileDesc(rfd, ep), false); } break; } } } } public void acceptConnection(String word, Socket sock) { HTTPStat.GIV_REQUESTS.incrementStat(); acceptDownload(sock); } /** * Accepts the given socket for a push download to this host. * If the GIV is for a file that was never requested or has already * been downloaded, this will deal with it appropriately. In any case * this eventually closes the socket. Non-blocking. * @modifies this * @requires "GIV " was just read from s */ public void acceptDownload(Socket socket) { String file = null; int index = 0; byte[] clientGUID = null; try { // 1. Read GIV line BEFORE acquiring lock, since this may block. GIVLine line = parseGIV(socket); file = line.file; index = line.index; clientGUID = line.clientGUID; } catch (IOException e) { IOUtils.close(socket); return; } // if the push was sent through udp, make sure we cancel the failover push. cancelUDPFailover(clientGUID); // 2. Attempt to give to an existing downloader. synchronized (this) { if (BrowseHostHandler.handlePush(index, new GUID(clientGUID), socket)) return; for (Iterator iter = active.iterator(); iter.hasNext();) { ManagedDownloader md = (ManagedDownloader) iter.next(); if (md.acceptDownload(file, socket, index, clientGUID)) return; } for (Iterator iter = waiting.iterator(); iter.hasNext();) { ManagedDownloader md = (ManagedDownloader) iter.next(); if (md.acceptDownload(file, socket, index, clientGUID)) return; } } // Will only get here if no matching push existed. // 3. We never requested the file or already got it. Kill it. IOUtils.close(socket); } // //////////// Callback Methods for ManagedDownloaders /////////////////// /** @requires this monitor' held by caller */ private boolean hasFreeSlot() { return active.size() - innetworkCount < DownloadSettings.MAX_SIM_DOWNLOAD.getValue(); } /** * Removes downloader entirely from the list of current downloads. * Notifies callback of the change in status. * If completed is true, finishes the download completely. Otherwise, * puts the download back in the waiting list to be finished later. * @modifies this, callback */ public synchronized void remove(ManagedDownloader downloader, boolean completed) { active.remove(downloader); if(downloader instanceof InNetworkDownloader) innetworkCount--; waiting.remove(downloader); if(completed) cleanupCompletedDownload(downloader, true); else waiting.add(downloader); } /** * Bumps the priority of an inactive download either up or down * by amt (if amt==0, bump to start/end of list). */ public synchronized void bumpPriority(Downloader downloader, boolean up, int amt) { int idx = waiting.indexOf(downloader); if(idx == -1) return; if(up && idx != 0) { waiting.remove(idx); if (amt > idx) amt = idx; if (amt != 0) waiting.add(idx - amt, downloader); else waiting.add(0, downloader); //move to top of list } else if(!up && idx != waiting.size() - 1) { waiting.remove(idx); if (amt != 0) { amt += idx; if (amt > waiting.size()) amt = waiting.size(); waiting.add(amt, downloader); } else { waiting.add(downloader); //move to bottom of list } } } /** * Cleans up the given ManagedDownloader after completion. * * If ser is true, also writes a snapshot to the disk. */ private void cleanupCompletedDownload(ManagedDownloader dl, boolean ser) { querySentMDs.remove(dl); dl.finish(); if (dl.getQueryGUID() != null) router.downloadFinished(dl.getQueryGUID()); callback(dl).removeDownload(dl); //Save this' state to disk for crash recovery. if(ser) writeSnapshot(); // Enable auto shutdown if(active.isEmpty() && waiting.isEmpty()) callback(dl).downloadsComplete(); } /** * Attempts to send the given requery to provide the given downloader with * more sources to download. May not actually send the requery if it doing * so would exceed the maximum requery rate. * * @param query the requery to send, which should have a marked GUID. * Queries are subjected to global rate limiting iff they have marked * requery GUIDs. * @param requerier the downloader requesting more sources. Needed to * ensure fair requery scheduling. This MUST be in the waiting list, * i.e., it MUST NOT have a download slot. * @return true iff the query was actually sent. If false is returned, * the downloader should attempt to send the query later. */ public synchronized boolean sendQuery(ManagedDownloader requerier, QueryRequest query) { //NOTE: this algorithm provides global but not local fairness. That is, //if two requeries x and y are competing for a slot, patterns like //xyxyxy or xyyxxy are allowed, though xxxxyx is not. if(LOG.isTraceEnabled()) LOG.trace("DM.sendQuery():" + query.getQuery()); Assert.that(waiting.contains(requerier), "Unknown or non-waiting MD trying to send requery."); //Disallow if global time limits exceeded. These limits don't apply to //queries that are requeries. boolean isRequery=GUID.isLimeRequeryGUID(query.getGUID()); long elapsed=System.currentTimeMillis()-lastRequeryTime; if (isRequery && elapsed<=TIME_BETWEEN_REQUERIES) { return false; } //Has everyone had a chance to send a query? If so, clear the slate. if (querySentMDs.size() >= waiting.size()) { LOG.trace("DM.sendQuery(): reseting query sent queue"); querySentMDs.clear(); } //If downloader has already sent a query, give someone else a turn. if (querySentMDs.contains(requerier)) { // nope, sorry, must lets others go first... if(LOG.isWarnEnabled()) LOG.warn("DM.sendQuery(): out of turn:" + query.getQuery()); return false; } if(LOG.isTraceEnabled()) LOG.trace("DM.sendQuery(): requery allowed:" + query.getQuery()); querySentMDs.add(requerier); lastRequeryTime = System.currentTimeMillis(); router.sendDynamicQuery(query); return true; } /** * Sends a push through multicast. * * Returns true only if the RemoteFileDesc was a reply to a multicast query * and we wanted to send through multicast. Otherwise, returns false, * as we shouldn't reply on the multicast network. */ private boolean sendPushMulticast(RemoteFileDesc file, byte []guid) { // Send as multicast if it's multicast. if( file.isReplyToMulticast() ) { byte[] addr = RouterService.getNonForcedAddress(); int port = RouterService.getNonForcedPort(); if( NetworkUtils.isValidAddress(addr) && NetworkUtils.isValidPort(port) ) { PushRequest pr = new PushRequest(guid, (byte)1, //ttl file.getClientGUID(), file.getIndex(), addr, port, Message.N_MULTICAST); router.sendMulticastPushRequest(pr); if (LOG.isInfoEnabled()) LOG.info("Sending push request through multicast " + pr); return true; } } return false; } /** * Sends a push through UDP. * * This always returns true, because a UDP push is always sent. */ private boolean sendPushUDP(RemoteFileDesc file, byte[] guid) { PushRequest pr = new PushRequest(guid, (byte)2, file.getClientGUID(), file.getIndex(), RouterService.getAddress(), RouterService.getPort(), Message.N_UDP); if (LOG.isInfoEnabled()) LOG.info("Sending push request through udp " + pr); UDPService udpService = UDPService.instance(); //and send the push to the node try { InetAddress address = InetAddress.getByName(file.getHost()); //don't bother sending direct push if the node reported invalid //address and port. if (NetworkUtils.isValidAddress(address) && NetworkUtils.isValidPort(file.getPort())) udpService.send(pr, address, file.getPort()); } catch(UnknownHostException notCritical) { //We can't send the push to a host we don't know //but we can still send it to the proxies. } finally { IPFilter filter = IPFilter.instance(); //make sure we send it to the proxies, if any Set proxies = file.getPushProxies(); for (Iterator iter = proxies.iterator();iter.hasNext();) { IpPort ppi = (IpPort)iter.next(); if (filter.allow(ppi.getAddress())) udpService.send(pr,ppi.getInetAddress(),ppi.getPort()); } } return true; } /** * Sends a push through TCP. * * Returns true if we have a valid push route, or if a push proxy * gave us a succesful sending notice. */ private boolean sendPushTCP(final RemoteFileDesc file, final byte[] guid) { // if this is a FW to FW transfer, we must consider special stuff final boolean shouldDoFWTransfer = file.supportsFWTransfer() && UDPService.instance().canDoFWT() && !RouterService.acceptedIncomingConnection(); // try sending to push proxies... if(sendPushThroughProxies(file, guid, shouldDoFWTransfer)) return true; // if push proxies failed, but we need a fw-fw transfer, give up. if(shouldDoFWTransfer && !RouterService.acceptedIncomingConnection()) return false; byte[] addr = RouterService.getAddress(); int port = RouterService.getPort(); if(!NetworkUtils.isValidAddressAndPort(addr, port)) return false; PushRequest pr = new PushRequest(guid, ConnectionSettings.TTL.getValue(), file.getClientGUID(), file.getIndex(), addr, port); if(LOG.isInfoEnabled()) LOG.info("Sending push request through Gnutella: " + pr); try { router.sendPushRequest(pr); } catch (IOException e) { // this will happen if we have no push route. return false; } return true; } /** * Sends a push through push proxies. * * Returns true if a push proxy gave us a succesful reply, * otherwise returns false is all push proxies tell us the sending failed. */ private boolean sendPushThroughProxies(final RemoteFileDesc file, final byte[] guid, boolean shouldDoFWTransfer) { Set proxies = file.getPushProxies(); if(proxies.isEmpty()) return false; byte[] externalAddr = RouterService.getExternalAddress(); // if a fw transfer is necessary, but our external address is invalid, // then exit immediately 'cause nothing will work. if (shouldDoFWTransfer && !NetworkUtils.isValidAddress(externalAddr)) return false; byte[] addr = RouterService.getAddress(); int port = RouterService.getPort(); //TODO: investigate not sending a HTTP request to a proxy //you are directly connected to. How much of a problem is this? //Probably not much of one at all. Classic example of code //complexity versus efficiency. It may be hard to actually //distinguish a PushProxy from one of your UP connections if the //connection was incoming since the port on the socket is ephemeral //and not necessarily the proxies listening port // we have proxy info - give them a try // set up the request string -- // if a fw-fw transfer is required, add the extra "file" parameter. final String request = "/gnutella/push-proxy?ServerID=" + Base32.encode(file.getClientGUID()) + (shouldDoFWTransfer ? ("&file=" + PushRequest.FW_TRANS_INDEX) : ""); final String nodeString = "X-Node"; final String nodeValue = NetworkUtils.ip2string(shouldDoFWTransfer ? externalAddr : addr) + ":" + port; IPFilter filter = IPFilter.instance(); // try to contact each proxy for(Iterator iter = proxies.iterator(); iter.hasNext(); ) { IpPort ppi = (IpPort)iter.next(); if (!filter.allow(ppi.getAddress())) continue; final String ppIp = ppi.getAddress(); final int ppPort = ppi.getPort(); String connectTo = "http://" + ppIp + ":" + ppPort + request; HttpClient client = HttpClientManager.getNewClient(); HeadMethod head = new HeadMethod(connectTo); head.addRequestHeader(nodeString, nodeValue); head.addRequestHeader("Cache-Control", "no-cache"); if(LOG.isTraceEnabled()) LOG.trace("Push Proxy Requesting with: " + connectTo); try { client.executeMethod(head); if(head.getStatusCode() == 202) { if(LOG.isInfoEnabled()) LOG.info("Succesful push proxy: " + connectTo); if (shouldDoFWTransfer) startFWIncomingThread(file); return true; // push proxy succeeded! } else { if(LOG.isWarnEnabled()) LOG.warn("Invalid push proxy: " + connectTo + ", response: " + head.getStatusCode()); } } catch (IOException ioe) { LOG.warn("PushProxy request exception", ioe); } finally { if( head != null ) head.releaseConnection(); } } // they all failed. return false; } /** * Starts a thread waiting for an incoming fw-fw transfer. */ private void startFWIncomingThread(final RemoteFileDesc file) { // we need to open up our NAT for incoming UDP, so // start the UDPConnection. The other side should // do it soon too so hopefully we can communicate. ThreadFactory.startThread(new Runnable() { public void run() { Socket fwTrans = null; try { fwTrans = new UDPConnection(file.getHost(), file.getPort()); DownloadStat.FW_FW_SUCCESS.incrementStat(); // TODO: put this out to Acceptor in // the future InputStream is = fwTrans.getInputStream(); String word = IOUtils.readWord(is, 4); if (word.equals("GIV")) acceptDownload(fwTrans); else fwTrans.close(); } catch (IOException crap) { LOG.debug("failed to establish UDP connection",crap); IOUtils.close(fwTrans); DownloadStat.FW_FW_FAILURE.incrementStat(); } } }, "FWIncoming"); } /** * Sends a push for the given file. */ public void sendPush(RemoteFileDesc file) { sendPush(file, null); } /** * Sends a push request for the given file. * * @param file the <tt>RemoteFileDesc</tt> constructed from the query * hit, containing data about the host we're pushing to * @param observer The ConnectObserver to notify of success or failure * @return <tt>true</tt> if the push was successfully sent, otherwise * <tt>false</tt> */ public void sendPush(final RemoteFileDesc file, final Shutdownable observer) { //Make sure we know our correct address/port. // If we don't, we can't send pushes yet. byte[] addr = RouterService.getAddress(); int port = RouterService.getPort(); if(!NetworkUtils.isValidAddress(addr) || !NetworkUtils.isValidPort(port)) { if(observer != null) observer.shutdown(); return; } final byte[] guid = GUID.makeGuid(); // If multicast worked, try nothing else. if (sendPushMulticast(file,guid)) return; // if we can't accept incoming connections, we can only try // using the TCP push proxy, which will do fw-fw transfers. if(!RouterService.acceptedIncomingConnection()) { // if we can do FWT, offload a TCP pusher. if(UDPService.instance().canDoFWT()) { ThreadFactory.startThread(new PushRequestor(file, guid, observer), "FWT PushRequestor"); } else if(observer != null) { observer.shutdown(); } return; } // remember that we are waiting a push from this host // for the specific file. // do not send tcp pushes to results from alternate locations. if (!file.isFromAlternateLocation()) { addUDPFailover(file); // schedule the failover tcp pusher, which will run // if we don't get a response from the UDP push // within the UDP_PUSH_FAILTIME timeframe RouterService.schedule(new Runnable(){ public void run() { // Add it to a ProcessingQueue, so the TCP connection // doesn't bog down RouterService's scheduler // The FailoverRequestor will thus run in another thread. FAILOVERS.add(new PushFailoverRequestor(file, guid, observer)); } }, UDP_PUSH_FAILTIME, 0); } sendPushUDP(file,guid); } /** * Adds the necessary data into UDP_FAILOVER so that a PushFailoverRequestor * knows if it should send a request. * @param file */ private void addUDPFailover(RemoteFileDesc file) { synchronized (UDP_FAILOVER) { byte[] key = file.getClientGUID(); IntWrapper requests = (IntWrapper)UDP_FAILOVER.get(key); if (requests == null) { requests = new IntWrapper(0); UDP_FAILOVER.put(key, requests); } requests.addInt(1); } } /** * Removes data from UDP_FAILOVER, indicating a push has used it. * * @param guid */ private void cancelUDPFailover(byte[] clientGUID) { synchronized (UDP_FAILOVER) { byte[] key = clientGUID; IntWrapper requests = (IntWrapper)UDP_FAILOVER.get(key); if (requests != null) { requests.addInt(-1); if (requests.getInt() <= 0) UDP_FAILOVER.remove(key); } } } // ///////////////// Internal Method to Parse GIV String /////////////////// private static final class GIVLine { final String file; final int index; final byte[] clientGUID; GIVLine(String file, int index, byte[] clientGUID) { this.file=file; this.index=index; this.clientGUID=clientGUID; } } /** * Returns the file, index, and client GUID from the GIV request from s. * The input stream of s is positioned just after the GIV request, * immediately before any HTTP. If s is closed or the line couldn't * be parsed, throws IOException. * @requires "GIV " just read from s * @modifies s's input stream. */ private static GIVLine parseGIV(Socket s) throws IOException { //1. Read "GIV 0:BC1F6870696111D4A74D0001031AE043/sample.txt\n\n" String command; try { //The try-catch below is a work-around for JDK bug 4091706. InputStream istream=null; try { istream = s.getInputStream(); } catch (Exception e) { throw new IOException(); } ByteReader br = new ByteReader(istream); command = br.readLine(); // read in the first line if (command==null) throw new IOException(); String next=br.readLine(); // read in empty line if (next==null || (! next.equals(""))) { throw new IOException(); } } catch (IOException e) { throw e; } //2. Parse and return the fields. try { //a) Extract file index. IndexOutOfBoundsException // or NumberFormatExceptions will be thrown here if there's // a problem. They're caught below. int i=command.indexOf(":"); int index=Integer.parseInt(command.substring(0,i)); //b) Extract clientID. This can throw // IndexOutOfBoundsException or // IllegalArgumentException, which is caught below. int j=command.indexOf("/", i); byte[] guid=GUID.fromHexString(command.substring(i+1,j)); //c). Extract file name. String filename=URLDecoder.decode(command.substring(j+1)); return new GIVLine(filename, index, guid); } catch (IndexOutOfBoundsException e) { throw new IOException(); } catch (NumberFormatException e) { throw new IOException(); } catch (IllegalArgumentException e) { throw new IOException(); } } /** Calls measureBandwidth on each uploader. */ public void measureBandwidth() { List activeCopy; synchronized(this) { activeCopy = new ArrayList(active); } float currentTotal = 0f; boolean c = false; for (Iterator iter = activeCopy.iterator(); iter.hasNext(); ) { BandwidthTracker bt = (BandwidthTracker)iter.next(); if (bt instanceof InNetworkDownloader) continue; c = true; bt.measureBandwidth(); currentTotal += bt.getAverageBandwidth(); } if ( c ) { synchronized(this) { averageBandwidth = ( (averageBandwidth * numMeasures) + currentTotal ) / ++numMeasures; } } } /** Returns the total upload throughput, i.e., the sum over all uploads. */ public float getMeasuredBandwidth() { List activeCopy; synchronized(this) { activeCopy = new ArrayList(active); } float sum=0; for (Iterator iter = activeCopy.iterator(); iter.hasNext(); ) { BandwidthTracker bt = (BandwidthTracker)iter.next(); if (bt instanceof InNetworkDownloader) continue; float curr = 0; try{ curr = bt.getMeasuredBandwidth(); } catch(InsufficientDataException ide) { curr = 0;//insufficient data? assume 0 } sum+=curr; } return sum; } /** * returns the summed average of the downloads */ public synchronized float getAverageBandwidth() { return averageBandwidth; } private String getFileName(RemoteFileDesc[] rfds, String fileName) { for (int i = 0; i < rfds.length && fileName == null; i++) { fileName = rfds[i].getFileName(); } return fileName; } /** * sends a tcp push if the udp push has failed. */ private class PushRequestor implements Runnable { final RemoteFileDesc _file; final byte [] _guid; final Shutdownable _observer; public PushRequestor(RemoteFileDesc file, byte[] guid, Shutdownable observer) { _file = file; _guid = guid; _observer = observer; } public void run() { if (shouldProceed()) { if (!sendPushTCP(_file, _guid)) { if(_observer != null) _observer.shutdown(); } } } protected boolean shouldProceed() { return true; } } private class PushFailoverRequestor extends PushRequestor { public PushFailoverRequestor(RemoteFileDesc file, byte[] guid, Shutdownable observer) { super(file, guid, observer); } protected boolean shouldProceed() { byte[] key =_file.getClientGUID(); synchronized(UDP_FAILOVER) { IntWrapper requests = (IntWrapper)UDP_FAILOVER.get(key); if (requests!=null && requests.getInt() > 0) { requests.addInt(-1); if (requests.getInt() == 0) UDP_FAILOVER.remove(key); return true; } } return false; } } /** * Once an in-network download finishes, the UpdateHandler is notified. */ private static class InNetworkCallback implements DownloadCallback { public void addDownload(Downloader d) {} public void removeDownload(Downloader d) { InNetworkDownloader downloader = (InNetworkDownloader)d; UpdateHandler.instance().inNetworkDownloadFinished(downloader.getSHA1Urn(), downloader.getState() == Downloader.COMPLETE); } public void downloadsComplete() {} public void showDownloads() {} // always discard corruption. public void promptAboutCorruptDownload(Downloader dloader) { dloader.discardCorruptDownload(true); } public String getHostValue(String key) { return null; } } }