package com.limegroup.gnutella.version;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.DefaultedHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.limewire.core.api.download.DownloadException;
import org.limewire.core.api.updates.UpdateStyle;
import org.limewire.core.settings.ApplicationSettings;
import org.limewire.core.settings.UpdateSettings;
import org.limewire.i18n.I18nMarker;
import org.limewire.inject.EagerSingleton;
import org.limewire.io.Connectable;
import org.limewire.io.ConnectableImpl;
import org.limewire.io.IOUtils;
import org.limewire.lifecycle.Service;
import org.limewire.lifecycle.ServiceRegistry;
import org.limewire.listener.EventListener;
import org.limewire.listener.EventListenerList;
import org.limewire.listener.ListenerSupport;
import org.limewire.util.Base32;
import org.limewire.util.Clock;
import org.limewire.util.CommonUtils;
import org.limewire.util.FileUtils;
import org.limewire.util.StringUtils;
import org.limewire.util.Version;
import org.limewire.util.VersionFormatException;
import org.limewire.util.VersionUtils;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
import com.limegroup.gnutella.ApplicationServices;
import com.limegroup.gnutella.ConnectionManager;
import com.limegroup.gnutella.ConnectionServices;
import com.limegroup.gnutella.DownloadManager;
import com.limegroup.gnutella.Downloader;
import com.limegroup.gnutella.NetworkUpdateSanityChecker;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.ReplyHandler;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.UrnSet;
import com.limegroup.gnutella.NetworkUpdateSanityChecker.RequestType;
import com.limegroup.gnutella.connection.RoutedConnection;
import com.limegroup.gnutella.downloader.InNetworkDownloader;
import com.limegroup.gnutella.downloader.ManagedDownloader;
import com.limegroup.gnutella.downloader.RemoteFileDescFactory;
import com.limegroup.gnutella.http.HTTPHeaderName;
import com.limegroup.gnutella.http.HttpClientListener;
import com.limegroup.gnutella.http.HttpExecutor;
import com.limegroup.gnutella.library.FileDesc;
import com.limegroup.gnutella.library.FileView;
import com.limegroup.gnutella.library.GnutellaFiles;
import com.limegroup.gnutella.library.IncompleteFileDesc;
import com.limegroup.gnutella.library.Library;
import com.limegroup.gnutella.library.LibraryStatusEvent;
import com.limegroup.gnutella.library.LibraryUtils;
import com.limegroup.gnutella.messages.vendor.CapabilitiesVMFactory;
import com.limegroup.gnutella.util.LimeWireUtils;
/**
* Manager for version updates.
* <p>
* Handles queueing new data for parsing and keeping track of which current
* version is stored in memory & on disk.
*/
@EagerSingleton
public class UpdateHandlerImpl implements UpdateHandler, EventListener<LibraryStatusEvent>, Service {
private static final Log LOG = LogFactory.getLog(UpdateHandlerImpl.class);
private static final long THREE_DAYS = 3 * 24 * 60 * 60 * 1000;
/** If we haven't had new updates in this long, schedule HTTP failover */
private static final long ONE_MONTH = 10L * THREE_DAYS;
/**
* The filename on disk where data is stored.
*/
private static final String FILENAME = "version.xml";
// Package access for testing
protected static final int IGNORE_ID = Integer.MAX_VALUE;
// Package access for testing
protected static enum UpdateType {
FROM_NETWORK, FROM_DISK, FROM_HTTP;
}
/**
* init the random generator on class load time
*/
private static final Random RANDOM = new Random();
/**
* means to override the current time for tests
*/
private final Clock clock;
/**
* The most recent update info for this machine.
*/
private volatile UpdateInformation _updateInfo;
/**
* A collection of DownloadInformation's that we need to retrieve an update for.
*/
private volatile List<DownloadInformation> _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;
/** If an HTTP failover update is in progress */
private final HttpRequestControl httpRequestControl = new HttpRequestControl();
private final ScheduledExecutorService backgroundExecutor;
private final ConnectionServices connectionServices;
private final Provider<HttpExecutor> httpExecutor;
private final Provider<HttpParams> defaultParams;
private final Provider<NetworkUpdateSanityChecker> networkUpdateSanityChecker;
private final CapabilitiesVMFactory capabilitiesVMFactory;
private final Provider<ConnectionManager> connectionManager;
private final Provider<DownloadManager> downloadManager;
private final Library library;
private final FileView gnutellaFileView;
private final ApplicationServices applicationServices;
private final UpdateCollectionFactory updateCollectionFactory;
private final UpdateMessageVerifier updateMessageVerifier;
private final RemoteFileDescFactory remoteFileDescFactory;
private final EventListenerList<UpdateEvent> listeners;
private volatile String timeoutUpdateLocation = "http://update0.limewire.com/v2/update.def";
private volatile List<String> maxedUpdateList = Arrays.asList("http://update1.limewire.com/v2/update.def",
"http://update2.limewire.com/v2/update.def",
"http://update3.limewire.com/v2/update.def",
"http://update4.limewire.com/v2/update.def",
"http://update5.limewire.com/v2/update.def",
"http://update6.limewire.com/v2/update.def",
"http://update7.limewire.com/v2/update.def",
"http://update8.limewire.com/v2/update.def",
"http://update9.limewire.com/v2/update.def",
"http://update10.limewire.com/v2/update.def");
private volatile int minMaxHttpRequestDelay = 1000 * 60;
private volatile int maxMaxHttpRequestDelay = 1000 * 60 * 30;
private volatile int silentPeriodForMaxHttpRequest = 1000 * 60 * 5;
private volatile UpdateCollection updateCollection;
@Inject
UpdateHandlerImpl(@Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor,
ConnectionServices connectionServices,
Provider<HttpExecutor> httpExecutor,
@Named("defaults") Provider<HttpParams> defaultParams,
Provider<NetworkUpdateSanityChecker> networkUpdateSanityChecker,
CapabilitiesVMFactory capabilitiesVMFactory,
Provider<ConnectionManager> connectionManager,
Provider<DownloadManager> downloadManager,
ApplicationServices applicationServices,
UpdateCollectionFactory updateCollectionFactory,
Clock clock,
UpdateMessageVerifier updateMessageVerifier,
RemoteFileDescFactory remoteFileDescFactory,
@GnutellaFiles FileView gnutellaFileView,
Library library) {
this.backgroundExecutor = backgroundExecutor;
this.connectionServices = connectionServices;
this.httpExecutor = httpExecutor;
this.defaultParams = defaultParams;
this.networkUpdateSanityChecker = networkUpdateSanityChecker;
this.capabilitiesVMFactory = capabilitiesVMFactory;
this.connectionManager = connectionManager;
this.downloadManager = downloadManager;
this.library = library;
this.applicationServices = applicationServices;
this.updateCollectionFactory = updateCollectionFactory;
this.clock = clock;
this.updateMessageVerifier = updateMessageVerifier;
this.remoteFileDescFactory = remoteFileDescFactory;
this.gnutellaFileView = gnutellaFileView;
this.listeners = new EventListenerList<UpdateEvent>();
}
@Inject
void register(ListenerSupport<LibraryStatusEvent> listener) {
listener.addListener(this);
}
String getTimeoutUrl() {
return timeoutUpdateLocation;
}
List<String> getMaxUrls() {
return maxedUpdateList;
}
void setMaxUrls(List<String> urls) {
this.maxedUpdateList = urls;
}
void setSilentPeriodForMaxHttpRequest(int silentPeriodForMaxHttpRequest) {
this.silentPeriodForMaxHttpRequest = silentPeriodForMaxHttpRequest;
}
/**
* Initializes data as read from disk.
*/
public void start() {
LOG.trace("Initializing UpdateHandler");
backgroundExecutor.execute(new Runnable() {
public void run() {
handleDataInternal(FileUtils.readFileFully(getStoredFile()), UpdateType.FROM_DISK, null);
}
});
// Try to update ourselves (re-use hosts for downloading, etc..)
// at a specified interval.
backgroundExecutor.schedule(new Poller(), UpdateSettings.UPDATE_RETRY_DELAY.getValue(), TimeUnit.MILLISECONDS);
}
/**
* Sparks off an attempt to down load any pending updates.
*/
private void tryToDownloadUpdates() {
backgroundExecutor.execute(new Runnable() {
public void run() {
UpdateInformation updateInfo = _updateInfo;
if (updateInfo != null &&
updateInfo.getUpdateURN() != null &&
isMyUpdateDownloaded(updateInfo)) {
fireUpdate(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) {
backgroundExecutor.execute(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.
* <p>
* (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, final ReplyHandler handler) {
if(data != null) {
backgroundExecutor.execute(new Runnable() {
public void run() {
LOG.trace("Parsing new data...");
handleDataInternal(data, UpdateType.FROM_NETWORK, handler);
}
});
}
}
/**
* 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. Package access for testing.
* <p>
* (Processes the data immediately.)
*/
protected void handleDataInternal(byte[] data, UpdateType updateType, ReplyHandler handler) {
if (data == null) {
if (updateType == UpdateType.FROM_NETWORK && handler != null)
networkUpdateSanityChecker.get()
.handleInvalidResponse(handler, RequestType.VERSION);
LOG.warn("No data to handle.");
return;
}
String xml = updateMessageVerifier.getVerifiedData(data);
if (xml == null) {
if (updateType == UpdateType.FROM_NETWORK && handler != null)
networkUpdateSanityChecker.get()
.handleInvalidResponse(handler, RequestType.VERSION);
LOG.warn("Couldn't verify signature on data.");
return;
}
if (updateType == UpdateType.FROM_NETWORK && handler != null)
networkUpdateSanityChecker.get().handleValidResponse(handler, RequestType.VERSION);
UpdateCollection uc = updateCollectionFactory.createUpdateCollection(xml);
updateCollection = uc;
if (LOG.isDebugEnabled())
LOG.debug("Got a collection with id: " + uc.getId() + ", from " + updateType + ". Current id is: " + _lastId);
switch (updateType) {
case FROM_NETWORK:
// the common case:
// a) if max && no max already, do failover.
// b) if not max && <= last, check stale.
// c) if not max && > last, update
if (uc.getId() == IGNORE_ID) {
if (_lastId != IGNORE_ID)
doHttpMaxFailover(uc);
} else if (uc.getId() <= _lastId) {
checkForStaleUpdateAndMaybeDoHttpFailover();
addSourceIfIdMatches(handler, uc.getId());
} else {// is greater
storeAndUpdate(data, uc, updateType);
}
break;
case FROM_DISK:
// on first load:
// a) always check for stale
// b) update if we didn't get an update before this ran.
checkForStaleUpdateAndMaybeDoHttpFailover();
if (uc.getId() > _lastId)
storeAndUpdate(data, uc, updateType);
break;
case FROM_HTTP:
// on HTTP response:
// a) update if >= stored.
// (note this is >=, different than >, which is from
// network)
if (uc.getId() >= _lastId)
storeAndUpdate(data, uc, updateType);
break;
}
}
/**
* 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, UpdateType updateType) {
if(LOG.isTraceEnabled())
LOG.trace("Retrieved new data from: " + updateType + ", storing & updating.");
if(uc.getId() == IGNORE_ID && updateType == UpdateType.FROM_NETWORK)
throw new IllegalStateException("shouldn't be here!");
// If an http max request is pending, don't even bother with this stuff.
// We want to get it straight from the source...
if (updateType == UpdateType.FROM_NETWORK && httpRequestControl.isRequestPending()
&& httpRequestControl.getRequestReason() == HttpRequestControl.RequestReason.MAX)
return;
_lastId = uc.getId();
_lastTimestamp = uc.getTimestamp();
UpdateSettings.LAST_UPDATE_TIMESTAMP.setValue(_lastTimestamp);
long delay = UpdateSettings.UPDATE_DOWNLOAD_DELAY.getValue();
long random = Math.abs(RANDOM.nextLong() % delay);
_nextDownloadTime = _lastTimestamp + random;
_lastBytes = data;
if(updateType != UpdateType.FROM_DISK) {
// cancel any http and pretend we just updated.
if(httpRequestControl.getRequestReason() == HttpRequestControl.RequestReason.TIMEOUT)
httpRequestControl.cancelRequest();
UpdateSettings.LAST_HTTP_FAILOVER.setValue(clock.now());
FileUtils.verySafeSave(CommonUtils.getUserSettingsDir(), FILENAME, data);
capabilitiesVMFactory.updateCapabilities();
connectionManager.get().sendUpdatedCapabilities();
}
Version limeV;
try {
limeV = new Version(LimeWireUtils.getLimeWireVersion());
} catch(VersionFormatException vfe) {
LOG.warn("Invalid LimeWire version", vfe);
return;
}
Version javaV = null;
try {
javaV = new Version(VersionUtils.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(UpdateStyle.STYLE_MAJOR,
UpdateSettings.UPDATE_STYLE.getValue());
UpdateData updateInfo = uc.getUpdateDataFor(limeV,
ApplicationSettings.getLanguage(),
LimeWireUtils.isPro(),
style,
javaV);
List<DownloadInformation> 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<DownloadInformation>(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);
backgroundExecutor.schedule(new NotificationFailover(_lastId),
delay(clock.now(), uc.getTimestamp()),
TimeUnit.MILLISECONDS);
} else if (isMyUpdateDownloaded(updateInfo)) {
LOG.debug("there is an update for me, but I happen to have it on disk");
fireUpdate(updateInfo);
} else
LOG.debug("we have an update, it needs a download. Rely on callbacks");
}
/**
* Begins an HTTP failover.
*/
private void checkForStaleUpdateAndMaybeDoHttpFailover() {
LOG.debug("checking for timeout http failover");
long monthAgo = clock.now() - ONE_MONTH;
if (UpdateSettings.LAST_UPDATE_TIMESTAMP.getValue() < monthAgo && // more than a month ago
UpdateSettings.LAST_HTTP_FAILOVER.getValue() < monthAgo && // and last failover too
!httpRequestControl.requestQueued(HttpRequestControl.RequestReason.TIMEOUT)) { // and we're not already doing a failover
long when = (connectionServices.isConnected() ? 1 : 5 ) * 60 * 1000;
if (LOG.isDebugEnabled())
LOG.debug("scheduling http failover in "+when);
backgroundExecutor.schedule(new Runnable() {
public void run() {
try {
launchHTTPUpdate(timeoutUpdateLocation);
} catch (URISyntaxException e) {
httpRequestControl.requestFinished();
httpRequestControl.cancelRequest();
LOG.warn(e.toString(), e);
}
}
}, when, TimeUnit.MILLISECONDS);
}
}
private void doHttpMaxFailover(UpdateCollection updateCollection) {
long maxTimeAgo = clock.now() - silentPeriodForMaxHttpRequest;
if(!httpRequestControl.requestQueued(HttpRequestControl.RequestReason.MAX) &&
UpdateSettings.LAST_HTTP_FAILOVER.getValue() < maxTimeAgo) {
LOG.debug("Scheduling http max failover...");
backgroundExecutor.schedule(new Runnable() {
public void run() {
String url = maxedUpdateList.get(RANDOM.nextInt(maxedUpdateList.size()));
try {
launchHTTPUpdate(url);
} catch (URISyntaxException e) {
httpRequestControl.requestFinished();
httpRequestControl.cancelRequest();
LOG.warn(e.toString(), e);
}
}
}, RANDOM.nextInt(maxMaxHttpRequestDelay) + minMaxHttpRequestDelay, TimeUnit.MILLISECONDS);
} else {
LOG.debug("Ignoring http max failover.");
}
}
/**
* Launches an HTTP update to the failover url.
*/
private void launchHTTPUpdate(String url) throws URISyntaxException {
if (!httpRequestControl.isRequestPending())
return;
LOG.debug("about to issue http request method");
HttpGet get = new HttpGet(LimeWireUtils.addLWInfoToUrl(url, applicationServices.getMyGUID()));
get.addHeader("User-Agent", LimeWireUtils.getHttpServer());
get.addHeader(HTTPHeaderName.CONNECTION.httpStringValue(),"close");
httpRequestControl.requestActive();
HttpParams params = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(params, 10000);
HttpConnectionParams.setSoTimeout(params, 10000);
params = new DefaultedHttpParams(params, defaultParams.get());
httpExecutor.get().execute(get, params, new RequestHandler());
}
/**
* 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 = LibraryUtils.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<? extends DownloadInformation> toDownload, ReplyHandler source) {
if (toDownload == null)
toDownload = Collections.emptyList();
killObsoleteUpdates(toDownload);
for(DownloadInformation next : toDownload) {
if (isHopeless(next))
continue;
if(downloadManager.get().isSavedDownloadsLoaded() && library.isLoadFinished()) {
//TODO: remove the cast
ManagedDownloader md = (ManagedDownloader)downloadManager.get().getDownloaderForURN(next.getUpdateURN());
// Skip to the next one since we already have a complete file.
if(hasCompleteFile(next.getUpdateURN())) {
if(md != null) {
md.stop();
}
continue;
}
// If we don't have an existing download ...
// and there's no existing InNetwork downloads &
// no existing Store downloads &
// we're allowed to start a new one.
if(md == null && !downloadManager.get().hasInNetworkDownload() && canStartDownload()) {
LOG.debug("Starting a new InNetwork Download");
try {
md = (ManagedDownloader)downloadManager.get().download(next, clock.now());
} catch(DownloadException e) {
LOG.error("Unable to construct download", e);
}
}
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<? extends DownloadInformation> toDownload) {
if (!downloadManager.get().isSavedDownloadsLoaded() || !library.isLoadFinished())
return;
if (_killingObsoleteNecessary) {
_killingObsoleteNecessary = false;
downloadManager.get().killDownloadersNotListed(toDownload);
Set<URN> urns = new HashSet<URN>(toDownload.size());
for(DownloadInformation data : toDownload)
urns.add(data.getUpdateURN());
List<FileDesc> shared = gnutellaFileView.getFilesInDirectory(LibraryUtils.PREFERENCE_SHARE);
for (FileDesc fd : shared) {
if (fd.getSHA1Urn() != null && !urns.contains(fd.getSHA1Urn())) {
library.remove(fd.getFile());
fd.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) {
for(RoutedConnection mc : connectionManager.get().getConnections()) {
if(mc.getConnectionCapabilities().getRemoteHostUpdateVersion() == _lastId) {
LOG.debug("Adding source: " + mc);
md.addDownload(rfd(mc, info), false);
} else
LOG.debug("Not adding source because bad id: " + mc.getConnectionCapabilities().getRemoteHostUpdateVersion() + ", us: " + _lastId);
}
}
/**
* Constructs an RFD out of the given information & connection.
*/
private RemoteFileDesc rfd(ReplyHandler rh, DownloadInformation info) {
Set<URN> urns = new UrnSet(info.getUpdateURN());
return remoteFileDescFactory.createRemoteFileDesc(new ConnectableImpl(rh.getInetSocketAddress(), rh instanceof Connectable ? ((Connectable)rh).isTLSCapable() : false), Integer.MAX_VALUE,
info.getUpdateFileName(), info.getSize(), rh.getClientGUID(), 0, 2, false, null, urns, false,
"LIME", -1);
}
/**
* 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(update != null);
fireUpdate(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 + " (" + new Date(now) + ")" +
"\nStamp : " + timestamp + " (" + new Date(timestamp) + ")" +
"\nDelay : " + delay + " (" + CommonUtils.seconds2time(delay/1000) + ")" +
"\nRandom : " + random + " (" + CommonUtils.seconds2time(random/1000) + ")" +
"\nThen : " + then + " (" + new Date(then) + ")" +
"\nDiff : " + (then-now) + " (" + CommonUtils.seconds2time((then-now)/1000) + ")");
}
return Math.max(0,then - now);
}
/**
* Notifies this that an update with the given URN has finished downloading.
* <p>
* If this was our update, we notify the GUI. It's 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);
backgroundExecutor.schedule(new NotificationFailover(_lastId),delay,TimeUnit.MILLISECONDS);
} else {
fireUpdate(updateInfo);
connectionManager.get().sendUpdatedCapabilities();
}
}
}
};
backgroundExecutor.execute(r);
}
/**
* @return whether we killed any hopeless update downloads
*/
private void killHopelessUpdates(List<? extends DownloadInformation> updates) {
if (updates == null)
return;
if (!downloadManager.get().hasInNetworkDownload())
return;
long now = clock.now();
for(DownloadInformation info : updates) {
Downloader downloader = downloadManager.get().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 boolean isHopeless(InNetworkDownloader downloader, long now) {
if (now - downloader.getStartTime() <
UpdateSettings.UPDATE_GIVEUP_FACTOR.getValue() *
UpdateSettings.UPDATE_DOWNLOAD_DELAY.getValue())
return false;
if (downloader.getDownloadAttempts() < 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 boolean isMyUpdateDownloaded(UpdateInformation myInfo) {
if (!library.isLoadFinished())
return false;
URN myUrn = myInfo.getUpdateURN();
if (myUrn == null)
return true;
return hasCompleteFile(myUrn);
}
private boolean hasCompleteFile(URN urn) {
List<FileDesc> fds = library.getFileDescsMatching(urn);
for(FileDesc fd : fds) {
if(!(fd instanceof IncompleteFileDesc)) {
return true;
}
}
return false;
}
/**
* Simple accessor for the stored file.
*/
private File getStoredFile() {
return new File(CommonUtils.getUserSettingsDir(), FILENAME);
}
/**
* 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);
}
}
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);
}
}
private class RequestHandler implements HttpClientListener {
public boolean requestComplete(HttpUriRequest request, HttpResponse response) {
LOG.debug("http request method succeeded");
// remember we made an attempt even if it didn't succeed
UpdateSettings.LAST_HTTP_FAILOVER.setValue(clock.now());
final byte[] inflated;
try {
if (response.getStatusLine().getStatusCode() < 200 || response.getStatusLine().getStatusCode() >= 300)
throw new IOException("bad code "+response.getStatusLine().getStatusCode());
byte [] resp = null;
if(response.getEntity() != null) {
resp = IOUtils.readFully(response.getEntity().getContent());
}
if (resp == null || resp.length == 0)
throw new IOException("bad body");
// inflate the response and process.
inflated = updateMessageVerifier.inflateNetworkData(resp);
} catch (IOException failed) {
httpRequestControl.requestFinished();
LOG.warn("couldn't fetch data ",failed);
return false;
} finally {
httpExecutor.get().releaseResources(response);
}
// Handle the data in the background thread.
backgroundExecutor.execute(new Runnable() {
public void run() {
httpRequestControl.requestFinished();
LOG.trace("Parsing new data...");
handleDataInternal(inflated, UpdateType.FROM_HTTP, null);
}
});
return false; // no more requests
}
public boolean requestFailed(HttpUriRequest request, HttpResponse response, IOException exc) {
LOG.warn("http failover failed",exc);
httpRequestControl.requestFinished();
UpdateSettings.LAST_HTTP_FAILOVER.setValue(clock.now());
httpExecutor.get().releaseResources(response);
// nothing we can do.
return false;
}
@Override
public boolean allowRequest(HttpUriRequest request) {
return true;
}
}
/**
* A simple control to let the flow of HTTP requests happen differently
* depending on why it was requested.
*/
private static class HttpRequestControl {
private static enum RequestReason { TIMEOUT, MAX };
private final AtomicBoolean requestQueued = new AtomicBoolean(false);
private final AtomicBoolean requestActive = new AtomicBoolean(false);
private volatile RequestReason requestReason;
/** Returns true if a request is queued or active. */
boolean isRequestPending() {
return requestActive.get() || requestQueued.get();
}
/** Sets a queued request and returns true if a request is pending or active. */
boolean requestQueued(RequestReason reason) {
boolean prior = requestQueued.getAndSet(true);
if(!prior || reason == RequestReason.MAX) // upgrade reason
requestReason = reason;
return prior || requestActive.get();
}
/** Sets a request to be active. */
void requestActive() {
requestActive.set(true);
requestQueued.set(false);
}
/** Returns the reason the last request was queueud. */
RequestReason getRequestReason() {
return requestReason;
}
void cancelRequest() {
requestQueued.set(false);
}
void requestFinished() {
requestActive.set(false);
}
}
public byte[] getOldUpdateResponse() {
return Base32.decode("I5AVOQKFIZCE4Q2RKFATKVBWKBKVEWSOJRFU6WS2JVIUCR2QGRJESNBVIE3UESKDINIUCSSWIZKE2WCHGZKFMMS2GJAVSTKCGQ2TINSLIRFEQWCRG5KEITL4PQ6HK4DEMF2GKIDJMQ6SEMRRGQ3TIOBTGY2DOIRAORUW2ZLTORQW24B5EIYSEPQKEAQCAPDNONTSAZTSN5WT2IRXGYXDONZOG42SEIDGN5ZD2IRYGYXDQOBOHA2SEIDUN46SEOBWFY4DSLRYGURCA5LSNQ6SE2DUORYDULZPO53XOLTMNFWWK53JOJSS4Y3PNUXXK4DEMF2GKIRAON2HS3DFHURDAIRAN5ZT2ISXNFXGI33XOMRCA5LSNY6SE5LSNY5GE2LUOBZGS3TUHJIEYUCSKRIEET2BKJBE6U2BJNIECTKHKZJTEU2MGU3VGM2HIRGFCLRXIZIEGR2NG43VGSCPKFGVAUSQJU2UGNKMJ5NEKT2EG43EGRK2IQ2E2USBIVGESIRAOVRW63LNMFXGIPJHEISCKIRAF5JSOIDVNZQW2ZJ5EJGGS3LFK5UXEZKXNFXDILRRGYXDMLTFPBSSEIDTNF5GKPJCGQ2TANRSGU3CEPQKEAQCAIBAEA6GYYLOM4QGSZB5E5SW4JZ6BIQCAIBAEAQCAIB4EFNUGRCBKRAVWNBOGE3C4NRAKVJE4XK5HYFCAIBAEAQCAPBPNRQW4ZZ6BIQCAIB4F5WXGZZ6BIQCAIBAEAQDY3LTM4QGM4TPNU6SENBOHAXDCIRAMZXXEPJCGQXDCNROGYRCA5LSNQ6SE2DUORYDULZPO53XOLTMNFWWK53JOJSS4Y3PNUXXK4DEMF2GKIRAMZZGKZJ5EJ2HE5LFEIQG64Z5EJLWS3TEN53XGIRAON2HS3DFHURDIIRAOVZG4PJCOVZG4OTCNF2HA4TJNZ2DUUCMKBJFIUCCJ5AVEQSPKNAUWUCBJVDVMUZSKNGDKN2TGNDUITCRFY3UMUCDI5GTON2TJBHVCTKQKJIE2NKDGVGE6WSFJ5CDONSDIVNEINCNKJAUKTCJEIQHKY3PNVWWC3TEHUTSEJBFEIQC6UZHEB2W4YLNMU6SETDJNVSVO2LSMVLWS3RUFYYTMLRWFZSXQZJCEBZWS6TFHURDINJQGYZDKNRCHYFCAIBAEAQCAPDMMFXGOIDJMQ6SOZLOE47AUIBAEAQCAIB4EFNUGRCBKRAVWCRAEAQCAIBAHR2GCYTMMUQGC3DJM5XD2Y3FNZ2GK4RAOZQWY2LHNY6WGZLOORSXEPR4ORZD4PDUMQ7AUPDDMVXHIZLSHY6GEPSVOJTWK3TUEBGGS3LFK5UXEZJAKNSWG5LSNF2HSICVOBSGC5DFEBAXMYLJNRQWE3DFFY6GE4R6BJIGYZLBONSSAVLQMRQXIZJAJFWW2ZLENFQXIZLMPEXDYYTSHY6GE4R6HQXWEPQKJFTCA5DIMUQHK4DEMF2GKIDEN5SXGIDON52CA53POJVSYIDWNFZWS5B4MJZD4CTIOR2HAORPF53XO5ZONRUW2ZLXNFZGKLTDN5WS6ZDPO5XGY33BMQ6GE4R6EBTG64RAORUGKIDMMF2GK43UEB3GK4TTNFXW4IDPMYQEY2LNMVLWS4TFFY6C6YR6HQXWGZLOORSXEPR4F52GIPR4F52HEPR4F52GCYTMMU7AUIBAEAQCAIC5LU7AUIBAEAQCAIB4F5WGC3THHYFCAIBAEAQCAPBPNVZWOPQKEAQCAIBAEAFCAIBAEAQCAPDNONTSAZTSN5WT2IRUFY4C4MJCEBTG64R5EI2C4MJWFY3CEIDVOJWD2ITIOR2HAORPF53XO5ZONRUW2ZLXNFZGKLTDN5WS65LQMRQXIZJCEBZXI6LMMU6SENBCHYFCAIBAEAQCAPDMMFXGOIDJMQ6SOZLOE47AUIBAEAQCAIB4EFNUGRCBKRAVWCRAEAQCAIBAHR2GCYTMMUQGC3DJM5XD2Y3FNZ2GK4RAOZQWY2LHNY6WGZLOORSXEPR4ORZD4PDUMQ7AUPDDMVXHIZLSHY6GEPSVOJTWK3TUEBGGS3LFK5UXEZJAKNSWG5LSNF2HSICVOBSGC5DFEBAXMYLJNRQWE3DFFY6GE4R6BJIGYZLBONSSAVLQMRQXIZJAJFWW2ZLENFQXIZLMPEXDYYTSHY6GE4R6HQXWEPQKJFTCA5DIMUQHK4DEMF2GKIDEN5SXGIDON52CA53POJVSYIDWNFZWS5B4MJZD4CTIOR2HAORPF53XO5ZONRUW2ZLXNFZGKLTDN5WS6ZDPO5XGY33BMQ6GE4R6EBTG64RAORUGKIDMMF2GK43UEB3GK4TTNFXW4IDPMYQEY2LNMVLWS4TFFY6C6YR6HQXWGZLOORSXEPR4F52GIPR4F52HEPR4F52GCYTMMU7AUIBAEAQCAIC5LU7AUIBAEAQCAIB4F5WGC3THHYFCAIBAEAQCAPBPNVZWOPQKHQXXK4DEMF2GKPQK");
}
public String getServiceName() {
return I18nMarker.marktr("Update Checks");
}
public void initialize() {
}
public void stop() {
}
@Inject
void register(ServiceRegistry registry) {
registry.register(this);
}
/**
* Listens for events from FileManager.
*/
@Override
public void handleEvent(LibraryStatusEvent evt) {
if(evt.getType() == LibraryStatusEvent.Type.LOAD_COMPLETE) {
tryToDownloadUpdates();
}
}
@Override
public UpdateCollection getUpdateCollection() {
return updateCollection;
}
private void fireUpdate(UpdateInformation update) {
listeners.broadcast(new UpdateEvent(update, UpdateEvent.Type.UPDATE));
}
@Override
public void addListener(EventListener<UpdateEvent> listener) {
listeners.addListener(listener);
}
@Override
public boolean removeListener(EventListener<UpdateEvent> listener) {
return listeners.removeListener(listener);
}
}