package com.limegroup.gnutella.version; import java.io.File; import java.io.IOException; import java.util.LinkedList; import java.util.Random; import java.util.List; import java.util.Iterator; import java.util.HashSet; import java.util.Collections; import java.util.Set; import com.limegroup.gnutella.Assert; import com.limegroup.gnutella.Downloader; import com.limegroup.gnutella.SaveLocationException; import com.limegroup.gnutella.ManagedConnection; import com.limegroup.gnutella.FileDesc; import com.limegroup.gnutella.FileManager; import com.limegroup.gnutella.ReplyHandler; import com.limegroup.gnutella.URN; import com.limegroup.gnutella.downloader.InNetworkDownloader; import com.limegroup.gnutella.downloader.ManagedDownloader; import com.limegroup.gnutella.DownloadManager; import com.limegroup.gnutella.RemoteFileDesc; import com.limegroup.gnutella.RouterService; import com.limegroup.gnutella.util.CommonUtils; import com.limegroup.gnutella.util.FileUtils; import com.limegroup.gnutella.util.ProcessingQueue; import com.limegroup.gnutella.util.StringUtils; import com.limegroup.gnutella.security.SignatureVerifier; import com.limegroup.gnutella.settings.ApplicationSettings; import com.limegroup.gnutella.settings.UpdateSettings; import com.limegroup.gnutella.messages.vendor.CapabilitiesVM; import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.Log; /** * Manager for version updates. * * Handles queueing new data for parsing and keeping track of which current * version is stored in memory & on disk. */ public class UpdateHandler { private static final Log LOG = LogFactory.getLog(UpdateHandler.class); private static final long THREE_DAYS = 3 * 24 * 60 * 60 * 1000; /** * The filename on disk where data is stored. */ private static final String FILENAME = "version.xml"; /** * The filename on disk where the public key is stored. */ private static final String KEY = "version.key"; /** * init the random generator on class load time */ private static final Random RANDOM = new Random(); /** * means to override the current time for tests */ private static Clock clock = new Clock(); private static final UpdateHandler INSTANCE = new UpdateHandler(); private UpdateHandler() { initialize(); } public static UpdateHandler instance() { return INSTANCE; } /** * The queue that handles all incoming data. */ private final ProcessingQueue QUEUE = new ProcessingQueue("UpdateHandler"); /** * The most recent update info for this machine. */ private volatile UpdateInformation _updateInfo; /** * A collection of UpdateInformation's that we need to retrieve * an update for. */ private volatile List _updatesToDownload; /** * The most recent id of the update info. */ private volatile int _lastId; /** * The bytes to send on the wire. * * TODO: Don't store in memory. */ private volatile byte[] _lastBytes; /** * The timestamp of the latest update. */ private long _lastTimestamp; /** * The next time we can make an attempt to download a pushed file. */ private long _nextDownloadTime; private boolean _killingObsoleteNecessary; /** * The time we'll notify the gui about an update with URL */ /** * Initializes data as read from disk. */ private void initialize() { LOG.trace("Initializing UpdateHandler"); QUEUE.add(new Runnable() { public void run() { handleDataInternal(FileUtils.readFileFully(getStoredFile()), true); } }); // Try to update ourselves (re-use hosts for downloading, etc..) // at a specified interval. RouterService.schedule(new Runnable() { public void run() { QUEUE.add(new Poller()); } }, UpdateSettings.UPDATE_RETRY_DELAY.getValue(), 0); } /** * Sparks off an attempt to download any pending updates. */ public void tryToDownloadUpdates() { QUEUE.add(new Runnable() { public void run() { UpdateInformation updateInfo = _updateInfo; if (updateInfo != null && updateInfo.getUpdateURN() != null && isMyUpdateDownloaded(updateInfo)) RouterService.getCallback().updateAvailable(updateInfo); downloadUpdates(_updatesToDownload, null); } }); } /** * Notification that a ReplyHandler has received a VM containing an update. */ public void handleUpdateAvailable(final ReplyHandler rh, final int version) { if(version == _lastId) { QUEUE.add(new Runnable() { public void run() { addSourceIfIdMatches(rh, version); } }); } else if(LOG.isDebugEnabled()) LOG.debug("Another version from rh: " + rh + ", them: " + version + ", me: " + _lastId); } /** * Notification that a new message has arrived. * * (The actual processing is passed of to be run in a different thread. * All notifications are processed in the same thread, sequentially.) */ public void handleNewData(final byte[] data) { if(data != null) { QUEUE.add(new Runnable() { public void run() { LOG.trace("Parsing new data..."); handleDataInternal(data, false); } }); } } /** * Retrieves the latest id available. */ public int getLatestId() { return _lastId; } /** * Gets the bytes to send on the wire. */ public byte[] getLatestBytes() { return _lastBytes; } /** * Handles processing a newly arrived message. * * (Processes the data immediately.) */ private void handleDataInternal(byte[] data, boolean fromDisk) { if(data != null) { String xml = SignatureVerifier.getVerifiedData(data, getKeyFile(), "DSA", "SHA1"); if(xml != null) { UpdateCollection uc = UpdateCollection.create(xml); if(uc.getId() > _lastId) storeAndUpdate(data, uc, fromDisk); } else { LOG.warn("Couldn't verify signature on data."); } } else { LOG.warn("No data to handle."); } } /** * Stores the given data to disk & posts an update to neighboring connections. * Starts the download of any updates */ private void storeAndUpdate(byte[] data, UpdateCollection uc, boolean fromDisk) { LOG.trace("Retrieved new data, storing & updating."); _lastId = uc.getId(); _lastTimestamp = uc.getTimestamp(); long delay = UpdateSettings.UPDATE_DOWNLOAD_DELAY.getValue(); long random = Math.abs(RANDOM.nextLong() % delay); _nextDownloadTime = _lastTimestamp + random; _lastBytes = data; if(!fromDisk) { FileUtils.verySafeSave(CommonUtils.getUserSettingsDir(), FILENAME, data); CapabilitiesVM.reconstructInstance(); RouterService.getConnectionManager().sendUpdatedCapabilities(); } Version limeV; try { limeV = new Version(CommonUtils.getLimeWireVersion()); } catch(VersionFormatException vfe) { LOG.warn("Invalid LimeWire version", vfe); return; } Version javaV = null; try { javaV = new Version(CommonUtils.getJavaVersion()); } catch(VersionFormatException vfe) { LOG.warn("Invalid java version", vfe); } // don't allow someone to set the style to be above major. int style = Math.min(UpdateInformation.STYLE_MAJOR, UpdateSettings.UPDATE_STYLE.getValue()); UpdateData updateInfo = uc.getUpdateDataFor(limeV, ApplicationSettings.getLanguage(), CommonUtils.isPro(), style, javaV); List updatesToDownload = uc.getUpdatesWithDownloadInformation(); _killingObsoleteNecessary = true; // if we have an update for our machine, prepare the command line // and move our update to the front of the list of updates if (updateInfo != null && updateInfo.getUpdateURN() != null) { prepareUpdateCommand(updateInfo); updatesToDownload = new LinkedList(updatesToDownload); updatesToDownload.add(0,updateInfo); } _updateInfo = updateInfo; _updatesToDownload = updatesToDownload; downloadUpdates(updatesToDownload, null); if(updateInfo == null) { LOG.warn("No relevant update info to notify about."); return; } else if (updateInfo.getUpdateURN() == null || isHopeless(updateInfo)) { if (LOG.isDebugEnabled()) LOG.debug("we have an update, but it doesn't need a download. " + "or all our updates are hopeles. Scheduling URL notification..."); updateInfo.setUpdateCommand(null); RouterService.schedule(new NotificationFailover(_lastId), delay(clock.now(), uc.getTimestamp()), 0); } else if (isMyUpdateDownloaded(updateInfo)) { LOG.debug("there is an update for me, but I happen to have it on disk"); RouterService.getCallback().updateAvailable(updateInfo); } else LOG.debug("we have an update, it needs a download. Rely on callbacks"); } /** * replaces tokens in the update command with info about the specific system * i.e. <PATH> -> C:\Documents And Settings.... */ private static void prepareUpdateCommand(UpdateData info) { if (info == null || info.getUpdateCommand() == null) return; File path = FileManager.PREFERENCE_SHARE.getAbsoluteFile(); String name = info.getUpdateFileName(); try { path = FileUtils.getCanonicalFile(path); }catch (IOException bad) {} String command = info.getUpdateCommand(); command = StringUtils.replace(command,"$",path.getPath()+File.separator); command = StringUtils.replace(command,"%",name); info.setUpdateCommand(command); } /** * @return if the given update is considered hopeless */ private static boolean isHopeless(DownloadInformation info) { return UpdateSettings.FAILED_UPDATES.contains( info.getUpdateURN().httpStringValue()); } /** * Notification that a given ReplyHandler may have an update we can use. */ private void addSourceIfIdMatches(ReplyHandler rh, int version) { if(version == _lastId) downloadUpdates(_updatesToDownload, rh); else if (LOG.isDebugEnabled()) LOG.debug("Another version? Me: " + version + ", here: " + _lastId); } /** * Tries to download updates. * @return whether we had any non-hopeless updates. */ private void downloadUpdates(List toDownload, ReplyHandler source) { if (toDownload == null) toDownload = Collections.EMPTY_LIST; killObsoleteUpdates(toDownload); for(Iterator i = toDownload.iterator(); i.hasNext(); ) { DownloadInformation next = (DownloadInformation)i.next(); if (isHopeless(next)) continue; DownloadManager dm = RouterService.getDownloadManager(); FileManager fm = RouterService.getFileManager(); if(dm.isGUIInitd() && fm.isLoadFinished()) { FileDesc shared = fm.getFileDescForUrn(next.getUpdateURN()); ManagedDownloader md = (ManagedDownloader)dm.getDownloaderForURN(next.getUpdateURN()); if(LOG.isDebugEnabled()) LOG.debug("Looking for: " + next + ", got: " + shared); if(shared != null && shared.getClass() == FileDesc.class) { // if it's already shared, stop any existing download. if(md != null) md.stop(); continue; } // If we don't have an existing download ... // and there's no existing InNetwork downloads & // we're allowed to start a new one. if(md == null && !dm.hasInNetworkDownload() && canStartDownload()) { LOG.debug("Starting a new InNetwork Download"); try { md = (ManagedDownloader)dm.download(next, clock.now()); } catch(SaveLocationException sle) { LOG.error("Unable to construct download", sle); } } if(md != null) { if(source != null) md.addDownload(rfd(source, next), false); else addCurrentDownloadSources(md, next); } } } } /** * kills all in-network downloaders whose URNs are not listed in the list of updates. * Deletes any files in the folder that are not listed in the update message. */ private void killObsoleteUpdates(List toDownload) { DownloadManager dm = RouterService.getDownloadManager(); FileManager fm = RouterService.getFileManager(); if (!dm.isGUIInitd() || !fm.isLoadFinished()) return; if (_killingObsoleteNecessary) { _killingObsoleteNecessary = false; dm.killDownloadersNotListed(toDownload); Set urns = new HashSet(toDownload.size()); for (Iterator iter = toDownload.iterator(); iter.hasNext();) { UpdateData data = (UpdateData) iter.next(); urns.add(data.getUpdateURN()); } FileDesc [] shared = fm.getSharedFileDescriptors(FileManager.PREFERENCE_SHARE); for (int i = 0; i < shared.length; i++) { if (shared[i].getSHA1Urn() != null && !urns.contains(shared[i].getSHA1Urn())) { fm.removeFileIfShared(shared[i].getFile()); shared[i].getFile().delete(); } } } } /** * Adds all current connections that have the right update ID as a source for this download. */ private void addCurrentDownloadSources(ManagedDownloader md, DownloadInformation info) { List connections = RouterService.getConnectionManager().getConnections(); for(Iterator i = connections.iterator(); i.hasNext(); ) { ManagedConnection mc = (ManagedConnection)i.next(); if(mc.getRemoteHostUpdateVersion() == _lastId) { LOG.debug("Adding source: " + mc); md.addDownload(rfd(mc, info), false); } else LOG.debug("Not adding source because bad id: " + mc.getRemoteHostUpdateVersion() + ", us: " + _lastId); } } /** * Constructs an RFD out of the given information & connection. */ private RemoteFileDesc rfd(ReplyHandler rh, DownloadInformation info) { HashSet urns = new HashSet(1); urns.add(info.getUpdateURN()); return new RemoteFileDesc(rh.getAddress(), // address rh.getPort(), // port Integer.MAX_VALUE, // index (unknown) info.getUpdateFileName(), // filename (int)info.getSize(), // filesize rh.getClientGUID(), // client GUID 0, // speed false, // chat capable 2, // quality false, // browse hostable null, // xml doc urns, // urns false, // reply to MCast false, // is firewalled "LIME", // vendor System.currentTimeMillis(), // timestamp Collections.EMPTY_SET, // push proxies 0, // creation time 0); // firewalled transfer } /** * Determines if we're far enough past the timestamp to start a new * in network download. */ private boolean canStartDownload() { long now = clock.now(); if (LOG.isDebugEnabled()) LOG.debug("now is "+now+ " next time is "+_nextDownloadTime); return now > _nextDownloadTime; } /** * Determines if we should notify about there being new information. */ private void notifyAboutInfo(int id) { if (id != _lastId) return; UpdateInformation update = _updateInfo; Assert.that(update != null); RouterService.getCallback().updateAvailable(update); } /** * @return calculates a random delay after the timestamp, unless the timestamp * is more than 3 days in the future. */ private static long delay(long now, long timestamp) { if (timestamp - now > THREE_DAYS) return 0; long delay = UpdateSettings.UPDATE_DELAY.getValue(); long random = Math.abs(new Random().nextLong() % delay); long then = timestamp + random; if(LOG.isInfoEnabled()) { LOG.info("Delaying Update." + "\nNow : " + now + "\nStamp : " + timestamp + "\nDelay : " + delay + "\nRandom : " + random + "\nThen : " + then + "\nDiff : " + (then-now)); } return Math.max(0,then - now); } /** * Notifies this that an update with the given URN has finished downloading. * * If this was our update, we notify the gui. Its ok if the user restarts * as the rest of the updates will be downloaded the next session. */ public void inNetworkDownloadFinished(final URN urn, final boolean good) { Runnable r = new Runnable() { public void run() { // add it to the list of failed urns if (!good) UpdateSettings.FAILED_UPDATES.add(urn.httpStringValue()); UpdateData updateInfo = (UpdateData) _updateInfo; if (updateInfo != null && updateInfo.getUpdateURN() != null && updateInfo.getUpdateURN().equals(urn)) { if (!good) { // register a notification to the user later on. updateInfo.setUpdateCommand(null); long delay = delay(clock.now(),_lastTimestamp); RouterService.schedule(new NotificationFailover(_lastId),delay,0); } else RouterService.getCallback().updateAvailable(updateInfo); } } }; QUEUE.add(r); } /** * @return whether we killed any hopeless update downloads */ private static void killHopelessUpdates(List updates) { if (updates == null) return; DownloadManager dm = RouterService.getDownloadManager(); if (!dm.hasInNetworkDownload()) return; long now = clock.now(); for (Iterator iter = updates.iterator(); iter.hasNext();) { DownloadInformation info = (DownloadInformation) iter.next(); Downloader downloader = dm.getDownloaderForURN(info.getUpdateURN()); if (downloader != null && downloader instanceof InNetworkDownloader) { InNetworkDownloader iDownloader = (InNetworkDownloader)downloader; if (isHopeless(iDownloader, now)) iDownloader.stop(); } } } /** * @param now what time is it now * @return whether the in-network downloader is considered hopeless */ private static boolean isHopeless(InNetworkDownloader downloader, long now) { if (now - downloader.getStartTime() < UpdateSettings.UPDATE_GIVEUP_FACTOR.getValue() * UpdateSettings.UPDATE_DOWNLOAD_DELAY.getValue()) return false; if (downloader.getNumAttempts() < UpdateSettings.UPDATE_MIN_ATTEMPTS.getValue()) return false; return true; } /** * @return true if the update for our specific machine is downloaded or * there was nothing to download */ private static boolean isMyUpdateDownloaded(UpdateInformation myInfo) { FileManager fm = RouterService.getFileManager(); if (!fm.isLoadFinished()) return false; URN myUrn = myInfo.getUpdateURN(); if (myUrn == null) return true; FileDesc desc = fm.getFileDescForUrn(myUrn); if (desc == null) return false; return desc.getClass() == FileDesc.class; } /** * Simple accessor for the stored file. */ private File getStoredFile() { return new File(CommonUtils.getUserSettingsDir(), FILENAME); } /** * Simple accessor for the key file. */ private File getKeyFile() { return new File(CommonUtils.getUserSettingsDir(), KEY); } /** * a functor that repeatedly tries to download updates at a variable * interval. */ private class Poller implements Runnable { public void run() { downloadUpdates(_updatesToDownload, null); killHopelessUpdates(_updatesToDownload); RouterService.schedule( new Runnable() { public void run() { QUEUE.add(new Poller()); } },UpdateSettings.UPDATE_RETRY_DELAY.getValue(),0); } } private class NotificationFailover implements Runnable { private final int id; private boolean shown; NotificationFailover(int id) { this.id = id; } public void run() { if (shown) return; shown = true; notifyAboutInfo(id); } } } /** * to be overriden in tests */ class Clock { public long now() { return System.currentTimeMillis(); } }