forked from I2P_Developers/i2p.i2p
Big refactor of the router console update subsystem, in preparation for
implementing out-of-console updaters like i2psnark. - Add new update interfaces in net.i2p.update - All update implementations moved to routerconsole update/ - Implement an UpdateManager that registers with the RouterContext - UpdateManager handles multiple types of things to update (router, plugins, news, ...) and methods of updating (HTTP, ...) - UpdateManager maintains list of installed, downloaded, and available versions of everything - Define Updaters that can check for a new version and/or download an item - Individual Updaters register with the UpdateManager obtained from I2PAppContext, identifying the type of update item and update method they can handle. - Updaters need only core libs, no router.jar or routerconsole access required. - All checks and updates are initiated via the UpdateManager. - All status on checks and updates in-progress or completed are obtained from the UpdateManager. No more use of System properties to broadcast update state. - All update and checker tasks are intantiated on demand and threaded; no more static references left over. - Split out the Runners and Checkers from the Handlers and make the inheritance more sane. - No more permanent NewsFetcher thread; run on the SimpleScheduler queue and thread a checker task only to fetch the news. - No more static NewsFetcher instance in routerconsole. All helper methods that are still required are moved to NewsHelper. The UpdateManager implements the policy for when to check and download. All requests go through the UpdateManager. For each update type, there's several parts: - The xxxUpdateHandler implements the Updater - The xxxUpdateChecker implements the UpdateTask for checking - The xxxUpdateRunner implements the UpdateTask for downloading New and moved classes: web/ update/ ---- ------- new ConsoleUpdateManager.java new PluginUpdateChecker.java from PluginUpdateChecker PluginUpdateChecker -> PluginUpdateHandler.java PluginUpdateHandler.java -> PluginUpdateRunner new UnsignedUpdateHandler.java UnsignedUpdateHandler -> UnsignedUpdateRunner.java new UnsignedUpdateChecker from NewsFetcher UpdateHandler.java remains new UpdateHandler.java new UpdateRunner.java from UpdateHandler move NewsHandler from NewsFetcher new NewsFetcher new NewsTimerTask new DummyHandler Initial checkin. Unfinished, untested, unpolished.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,68 @@
|
||||
package net.i2p.router.update;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.update.*;
|
||||
|
||||
/**
|
||||
* Dummy to lock up the updates for a period of time
|
||||
*
|
||||
* @since 0.9.2
|
||||
*/
|
||||
class DummyHandler implements Updater {
|
||||
private final RouterContext _context;
|
||||
|
||||
public DummyHandler(RouterContext ctx) {
|
||||
_context = ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spins off an UpdateTask that sleeps
|
||||
*/
|
||||
public UpdateTask check(UpdateType type, UpdateMethod method,
|
||||
String id, String currentVersion, long maxTime) {
|
||||
if (type != UpdateType.TYPE_DUMMY)
|
||||
return null;
|
||||
return new DummyRunner(_context, maxTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spins off an UpdateTask that sleeps
|
||||
*/
|
||||
public UpdateTask update(UpdateType type, UpdateMethod method, List<URI> updateSources,
|
||||
String id, String newVersion, long maxTime) {
|
||||
if (type != UpdateType.TYPE_DUMMY)
|
||||
return null;
|
||||
return new DummyRunner(_context, maxTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use for both check and update
|
||||
*/
|
||||
private static class DummyRunner extends UpdateRunner {
|
||||
private final long _delay;
|
||||
|
||||
public DummyRunner(RouterContext ctx, long maxTime) {
|
||||
super(ctx, Collections.EMPTY_LIST);
|
||||
_delay = maxTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateType getType() { return UpdateType.TYPE_DUMMY; }
|
||||
|
||||
@Override
|
||||
protected void update() {
|
||||
try {
|
||||
Thread.sleep(_delay);
|
||||
} catch (InterruptedException ie) {}
|
||||
UpdateManager mgr = _context.updateManager();
|
||||
if (mgr != null) {
|
||||
mgr.notifyCheckComplete(this, false, false);
|
||||
mgr.notifyTaskFailed(this, "dummy", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,198 @@
|
||||
package net.i2p.router.update;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import net.i2p.crypto.TrustedUpdate;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.router.Router;
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.router.RouterVersion;
|
||||
import net.i2p.router.util.RFC822Date;
|
||||
import net.i2p.router.web.ConfigUpdateHandler;
|
||||
import net.i2p.router.web.ConfigUpdateHelper;
|
||||
import net.i2p.router.web.NewsHelper;
|
||||
import net.i2p.update.*;
|
||||
import static net.i2p.update.UpdateType.*;
|
||||
import static net.i2p.update.UpdateMethod.*;
|
||||
import net.i2p.util.EepGet;
|
||||
import net.i2p.util.EepHead;
|
||||
import net.i2p.util.FileUtil;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Task to fetch updates to the news.xml, and to keep
|
||||
* track of whether that has an announcement for a new version.
|
||||
*
|
||||
* @since 0.9.2 moved from NewsFetcher and make an Updater
|
||||
*/
|
||||
class NewsFetcher extends UpdateRunner {
|
||||
private String _lastModified;
|
||||
private final File _newsFile;
|
||||
private final File _tempFile;
|
||||
|
||||
private static final String TEMP_NEWS_FILE = "news.xml.temp";
|
||||
|
||||
public NewsFetcher(RouterContext ctx, List<URI> uris) {
|
||||
super(ctx, uris);
|
||||
_newsFile = new File(ctx.getRouterDir(), NewsHelper.NEWS_FILE);
|
||||
_tempFile = new File(ctx.getTempDir(), "tmp-" + ctx.random().nextLong() + TEMP_NEWS_FILE);
|
||||
long lastMod = NewsHelper.lastChecked(ctx);
|
||||
if (lastMod > 0)
|
||||
_lastModified = RFC822Date.to822Date(lastMod);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateType getType() { return NEWS; }
|
||||
|
||||
private boolean dontInstall() {
|
||||
return NewsHelper.dontInstall(_context);
|
||||
}
|
||||
|
||||
private boolean shouldInstall() {
|
||||
String policy = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_POLICY);
|
||||
if ("notify".equals(policy) || dontInstall())
|
||||
return false;
|
||||
File zip = new File(_context.getRouterDir(), Router.UPDATE_FILE);
|
||||
return !zip.exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update() {
|
||||
fetchNews();
|
||||
}
|
||||
|
||||
public void fetchNews() {
|
||||
boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
|
||||
String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
|
||||
int proxyPort = ConfigUpdateHandler.proxyPort(_context);
|
||||
|
||||
for (URI uri : _urls) {
|
||||
_currentURI = uri;
|
||||
String newsURL = uri.toString();
|
||||
|
||||
if (_tempFile.exists())
|
||||
_tempFile.delete();
|
||||
|
||||
try {
|
||||
EepGet get;
|
||||
if (shouldProxy)
|
||||
get = new EepGet(_context, true, proxyHost, proxyPort, 0, _tempFile.getAbsolutePath(), newsURL, true, null, _lastModified);
|
||||
else
|
||||
get = new EepGet(_context, false, null, 0, 0, _tempFile.getAbsolutePath(), newsURL, true, null, _lastModified);
|
||||
get.addStatusListener(this);
|
||||
if (get.fetch()) {
|
||||
String lastMod = get.getLastModified();
|
||||
if (lastMod != null) {
|
||||
_lastModified = lastMod;
|
||||
long lm = RFC822Date.parse822Date(lastMod);
|
||||
if (lm > 0)
|
||||
_context.router().saveConfig(NewsHelper.PROP_LAST_CHECKED, Long.toString(lm));
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
_log.error("Error fetching the news", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final String VERSION_STRING = "version=\"" + RouterVersion.VERSION + "\"";
|
||||
private static final String VERSION_PREFIX = "version=\"";
|
||||
|
||||
///// move to UpdateManager?
|
||||
|
||||
/**
|
||||
* Parse the installed (not the temp) news file for the latest version.
|
||||
* TODO: Real XML parsing, different update methods,
|
||||
* URLs in the file, ...
|
||||
*/
|
||||
void checkForUpdates() {
|
||||
FileInputStream in = null;
|
||||
try {
|
||||
in = new FileInputStream(_newsFile);
|
||||
StringBuilder buf = new StringBuilder(128);
|
||||
while (DataHelper.readLine(in, buf)) {
|
||||
int index = buf.indexOf(VERSION_PREFIX);
|
||||
if (index >= 0) {
|
||||
int end = buf.indexOf("\"", index + VERSION_PREFIX.length());
|
||||
if (end > index) {
|
||||
String ver = buf.substring(index+VERSION_PREFIX.length(), end);
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Found version: [" + ver + "]");
|
||||
if (TrustedUpdate.needsUpdate(RouterVersion.VERSION, ver)) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Our version is out of date, update!");
|
||||
_mgr.notifyVersionAvailable(this, _currentURI,
|
||||
ROUTER_SIGNED, "", HTTP,
|
||||
_mgr.getUpdateURLs(ROUTER_SIGNED, "", HTTP),
|
||||
ver, "");
|
||||
} else {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Our version is current");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (buf.indexOf(VERSION_STRING) == -1) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("No match in " + buf.toString());
|
||||
}
|
||||
buf.setLength(0);
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Error checking the news for an update", ioe);
|
||||
return;
|
||||
} finally {
|
||||
if (in != null) try { in.close(); } catch (IOException ioe) {}
|
||||
}
|
||||
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("No version found in news.xml file");
|
||||
}
|
||||
|
||||
/** override to prevent status update */
|
||||
@Override
|
||||
public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {}
|
||||
|
||||
/**
|
||||
* Copies the file from temp dir to the news location,
|
||||
* calls checkForUpdates()
|
||||
*/
|
||||
@Override
|
||||
public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("News fetched from " + url + " with " + (alreadyTransferred+bytesTransferred));
|
||||
|
||||
long now = _context.clock().now();
|
||||
if (_tempFile.exists()) {
|
||||
boolean copied = FileUtil.copy(_tempFile, _newsFile, true, false);
|
||||
_tempFile.delete();
|
||||
if (copied) {
|
||||
String newVer = Long.toString(now);
|
||||
_context.router().saveConfig(NewsHelper.PROP_LAST_UPDATED, newVer);
|
||||
_mgr.notifyVersionAvailable(this, _currentURI, NEWS, "", HTTP,
|
||||
null, newVer, "");
|
||||
checkForUpdates();
|
||||
} else {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Failed to copy the news file!");
|
||||
}
|
||||
} else {
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Transfer complete, but no file? - probably 304 Not Modified");
|
||||
}
|
||||
}
|
||||
|
||||
/** override to prevent status update */
|
||||
@Override
|
||||
public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package net.i2p.router.update;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.router.web.ConfigUpdateHelper;
|
||||
import net.i2p.update.*;
|
||||
|
||||
/**
|
||||
* Task to periodically look for updates to the news.xml, and to keep
|
||||
* track of whether that has an announcement for a new version.
|
||||
*
|
||||
* @since 0.9.2 moved from NewsFetcher
|
||||
*/
|
||||
class NewsHandler extends UpdateHandler {
|
||||
|
||||
/** @since 0.7.14 not configurable */
|
||||
private static final String BACKUP_NEWS_URL = "http://www.i2p2.i2p/_static/news/news.xml";
|
||||
|
||||
public NewsHandler(RouterContext ctx) {
|
||||
super(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* This will check for news or router updates (it does the same thing).
|
||||
* Should not block.
|
||||
* @param currentVersion ignored, stored locally
|
||||
*/
|
||||
public UpdateTask check(UpdateType type, UpdateMethod method,
|
||||
String id, String currentVersion, long maxTime) {
|
||||
if ((type != UpdateType.ROUTER_SIGNED && type != UpdateType.ROUTER_SIGNED_PACK200 && type != UpdateType.NEWS) ||
|
||||
method != UpdateMethod.HTTP)
|
||||
return null;
|
||||
List<URI> updateSources = new ArrayList(2);
|
||||
try {
|
||||
updateSources.add(new URI(ConfigUpdateHelper.getNewsURL(_context)));
|
||||
} catch (URISyntaxException use) {}
|
||||
try {
|
||||
updateSources.add(new URI(BACKUP_NEWS_URL));
|
||||
} catch (URISyntaxException use) {}
|
||||
UpdateRunner update = new NewsFetcher(_context, updateSources);
|
||||
update.start();
|
||||
return update;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does nothing. check() also does update.
|
||||
*/
|
||||
public UpdateTask update(UpdateType type, UpdateMethod method, List<URI> updateSources,
|
||||
String id, String newVersion, long maxTime) {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
package net.i2p.router.update;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import net.i2p.crypto.TrustedUpdate;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.router.Router;
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.router.RouterVersion;
|
||||
import net.i2p.router.web.ConfigUpdateHandler;
|
||||
import net.i2p.router.web.ConfigUpdateHelper;
|
||||
import net.i2p.router.web.NewsHelper;
|
||||
import static net.i2p.update.UpdateType.*;
|
||||
import net.i2p.util.EepGet;
|
||||
import net.i2p.util.EepHead;
|
||||
import net.i2p.util.FileUtil;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.SimpleScheduler;
|
||||
import net.i2p.util.SimpleTimer;
|
||||
|
||||
/**
|
||||
* Task to periodically look for updates to the news.xml, and to keep
|
||||
* track of whether that has an announcement for a new version.
|
||||
* Also looks for unsigned updates.
|
||||
*
|
||||
* Runs forever on instantiation, can't be stopped.
|
||||
*
|
||||
* @since 0.9.2 moved from NewsFetcher
|
||||
*/
|
||||
class NewsTimerTask implements SimpleTimer.TimedEvent {
|
||||
private final RouterContext _context;
|
||||
private final Log _log;
|
||||
private final ConsoleUpdateManager _mgr;
|
||||
|
||||
private static final long INITIAL_DELAY = 5*60*1000;
|
||||
private static final long RUN_DELAY = 10*60*1000;
|
||||
|
||||
public NewsTimerTask(RouterContext ctx) {
|
||||
_context = ctx;
|
||||
_log = ctx.logManager().getLog(NewsTimerTask.class);
|
||||
_mgr = (ConsoleUpdateManager) _context.updateManager();
|
||||
ctx.simpleScheduler().addPeriodicEvent(this,
|
||||
INITIAL_DELAY + _context.random().nextLong(INITIAL_DELAY),
|
||||
RUN_DELAY);
|
||||
// UpdateManager calls NewsFetcher to check the existing news at startup
|
||||
}
|
||||
|
||||
public void timeReached() {
|
||||
if (shouldFetchNews()) {
|
||||
fetchNews();
|
||||
if (shouldFetchUnsigned())
|
||||
fetchUnsignedHead();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldFetchNews() {
|
||||
if (_context.router().gracefulShutdownInProgress())
|
||||
return false;
|
||||
if (NewsHelper.isUpdateInProgress())
|
||||
return false;
|
||||
long lastFetch = NewsHelper.lastChecked(_context);
|
||||
String freq = _context.getProperty(ConfigUpdateHandler.PROP_REFRESH_FREQUENCY,
|
||||
ConfigUpdateHandler.DEFAULT_REFRESH_FREQUENCY);
|
||||
try {
|
||||
long ms = Long.parseLong(freq);
|
||||
if (ms <= 0)
|
||||
return false;
|
||||
|
||||
if (lastFetch + ms < _context.clock().now()) {
|
||||
return true;
|
||||
} else {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Last fetched " + DataHelper.formatDuration(_context.clock().now() - lastFetch) + " ago");
|
||||
return false;
|
||||
}
|
||||
} catch (NumberFormatException nfe) {
|
||||
if (_log.shouldLog(Log.ERROR))
|
||||
_log.error("Invalid refresh frequency: " + freq);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchNews() {
|
||||
_mgr.check(NEWS, "");
|
||||
}
|
||||
|
||||
private boolean shouldFetchUnsigned() {
|
||||
String url = _context.getProperty(ConfigUpdateHandler.PROP_ZIP_URL);
|
||||
return url != null && url.length() > 0 &&
|
||||
_context.getBooleanProperty(ConfigUpdateHandler.PROP_UPDATE_UNSIGNED) &&
|
||||
!NewsHelper.dontInstall(_context);
|
||||
}
|
||||
|
||||
/**
|
||||
* HEAD the update url, and if the last-mod time is newer than the last update we
|
||||
* downloaded, as stored in the properties, then we download it using eepget.
|
||||
*/
|
||||
private void fetchUnsignedHead() {
|
||||
_mgr.check(ROUTER_UNSIGNED, "");
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
package net.i2p.router.update;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.i2p.crypto.TrustedUpdate;
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.router.web.ConfigUpdateHandler;
|
||||
import net.i2p.update.*;
|
||||
import net.i2p.util.EepGet;
|
||||
import net.i2p.util.I2PAppThread;
|
||||
import net.i2p.util.PartialEepGet;
|
||||
import net.i2p.util.VersionComparator;
|
||||
|
||||
/**
|
||||
* Check for an updated version of a plugin.
|
||||
* A plugin is a standard .sud file with a 40-byte signature,
|
||||
* a 16-byte version, and a .zip file.
|
||||
*
|
||||
* So we get the current version and update URL for the installed plugin,
|
||||
* then fetch the first 56 bytes of the URL, extract the version,
|
||||
* and compare.
|
||||
*
|
||||
* Moved from web/ and turned into an UpdateTask.
|
||||
*
|
||||
* @since 0.7.12
|
||||
*/
|
||||
class PluginUpdateChecker extends UpdateRunner {
|
||||
private final ByteArrayOutputStream _baos;
|
||||
private final String _appName;
|
||||
private final String _oldVersion;
|
||||
|
||||
public PluginUpdateChecker(RouterContext ctx, List<URI> uris, String appName, String oldVersion ) {
|
||||
super(ctx, uris);
|
||||
_baos = new ByteArrayOutputStream(TrustedUpdate.HEADER_BYTES);
|
||||
if (!uris.isEmpty())
|
||||
_currentURI = uris.get(0);
|
||||
_appName = appName;
|
||||
_oldVersion = oldVersion;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public UpdateType getType() { return UpdateType.PLUGIN; }
|
||||
|
||||
@Override
|
||||
protected void update() {
|
||||
// must be set for super
|
||||
_isPartial = true;
|
||||
updateStatus("<b>" + _("Checking for update of plugin {0}", _appName) + "</b>");
|
||||
// use the same settings as for updater
|
||||
// always proxy, or else FIXME
|
||||
//boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
|
||||
String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
|
||||
int proxyPort = ConfigUpdateHandler.proxyPort(_context);
|
||||
_baos.reset();
|
||||
try {
|
||||
_get = new PartialEepGet(_context, proxyHost, proxyPort, _baos, _currentURI.toString(), TrustedUpdate.HEADER_BYTES);
|
||||
_get.addStatusListener(this);
|
||||
_get.fetch();
|
||||
} catch (Throwable t) {
|
||||
_log.error("Error checking update for plugin", t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
|
||||
// super sets _newVersion if newer
|
||||
boolean newer = _newVersion != null;
|
||||
if (newer) {
|
||||
_mgr.notifyVersionAvailable(this, _currentURI, UpdateType.PLUGIN, _appName, UpdateMethod.HTTP,
|
||||
_urls, _newVersion, _oldVersion);
|
||||
}
|
||||
_mgr.notifyCheckComplete(this, newer, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
|
||||
File f = new File(_updateFile);
|
||||
f.delete();
|
||||
_mgr.notifyCheckComplete(this, false, false);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,92 @@
|
||||
package net.i2p.router.update;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.i2p.crypto.TrustedUpdate;
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.router.web.PluginStarter;
|
||||
import net.i2p.update.*;
|
||||
import net.i2p.util.EepGet;
|
||||
import net.i2p.util.I2PAppThread;
|
||||
import net.i2p.util.PartialEepGet;
|
||||
import net.i2p.util.SimpleScheduler;
|
||||
import net.i2p.util.SimpleTimer;
|
||||
import net.i2p.util.VersionComparator;
|
||||
|
||||
/**
|
||||
* Check for or download an updated version of a plugin.
|
||||
* A plugin is a standard .sud file with a 40-byte signature,
|
||||
* a 16-byte version, and a .zip file.
|
||||
*
|
||||
* So we get the current version and update URL for the installed plugin,
|
||||
* then fetch the first 56 bytes of the URL, extract the version,
|
||||
* and compare.
|
||||
*
|
||||
* Moved from web/ and turned into an Updater.
|
||||
*
|
||||
* @since 0.7.12
|
||||
* @author zzz
|
||||
*/
|
||||
class PluginUpdateHandler implements Updater {
|
||||
private final RouterContext _context;
|
||||
|
||||
public PluginUpdateHandler(RouterContext ctx) {
|
||||
_context = ctx;
|
||||
}
|
||||
|
||||
/** check a single plugin */
|
||||
@Override
|
||||
public UpdateTask check(UpdateType type, UpdateMethod method,
|
||||
String appName, String currentVersion, long maxTime) {
|
||||
if ((type != UpdateType.PLUGIN) ||
|
||||
method != UpdateMethod.HTTP || appName.length() <= 0)
|
||||
return null;
|
||||
|
||||
Properties props = PluginStarter.pluginProperties(_context, appName);
|
||||
String oldVersion = props.getProperty("version");
|
||||
String xpi2pURL = props.getProperty("updateURL");
|
||||
List<URI> updateSources = null;
|
||||
if (xpi2pURL != null) {
|
||||
try {
|
||||
updateSources = Collections.singletonList(new URI(xpi2pURL));
|
||||
} catch (URISyntaxException use) {}
|
||||
}
|
||||
|
||||
if (oldVersion == null || updateSources == null) {
|
||||
//updateStatus("<b>" + _("Cannot check, plugin {0} is not installed", appName) + "</b>");
|
||||
return null;
|
||||
}
|
||||
|
||||
UpdateRunner update = new PluginUpdateChecker(_context, updateSources, appName, oldVersion);
|
||||
update.start();
|
||||
return update;
|
||||
}
|
||||
|
||||
/** download a single plugin */
|
||||
@Override
|
||||
public UpdateTask update(UpdateType type, UpdateMethod method, List<URI> updateSources,
|
||||
String appName, String newVersion, long maxTime) {
|
||||
if ((type != UpdateType.PLUGIN && type != UpdateType.PLUGIN_INSTALL) ||
|
||||
method != UpdateMethod.HTTP || updateSources.isEmpty())
|
||||
return null;
|
||||
Properties props = PluginStarter.pluginProperties(_context, appName);
|
||||
String oldVersion = props.getProperty("version");
|
||||
String xpi2pURL = props.getProperty("updateURL");
|
||||
if (oldVersion == null || xpi2pURL == null) {
|
||||
//updateStatus("<b>" + _("Cannot check, plugin {0} is not installed", appName) + "</b>");
|
||||
return null;
|
||||
}
|
||||
|
||||
UpdateRunner update = new PluginUpdateRunner(_context, updateSources, appName, oldVersion);
|
||||
update.start();
|
||||
return update;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,415 @@
|
||||
package net.i2p.router.update;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import net.i2p.CoreVersion;
|
||||
import net.i2p.crypto.TrustedUpdate;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.router.web.ConfigClientsHelper;
|
||||
import net.i2p.router.web.ConfigUpdateHandler;
|
||||
import net.i2p.router.web.LogsHelper;
|
||||
import net.i2p.router.web.Messages;
|
||||
import net.i2p.router.web.PluginStarter;
|
||||
import net.i2p.update.*;
|
||||
import net.i2p.util.EepGet;
|
||||
import net.i2p.util.FileUtil;
|
||||
import net.i2p.util.I2PAppThread;
|
||||
import net.i2p.util.OrderedProperties;
|
||||
import net.i2p.util.SecureDirectory;
|
||||
import net.i2p.util.SecureFile;
|
||||
import net.i2p.util.SimpleScheduler;
|
||||
import net.i2p.util.SimpleTimer;
|
||||
import net.i2p.util.VersionComparator;
|
||||
|
||||
|
||||
/**
|
||||
* Check for an updated version of a plugin.
|
||||
* A plugin is a standard .sud file with a 40-byte signature,
|
||||
* a 16-byte version, and a .zip file.
|
||||
*
|
||||
* So we get the current version and update URL for the installed plugin,
|
||||
* then fetch the first 56 bytes of the URL, extract the version,
|
||||
* and compare.
|
||||
*
|
||||
* Moved from web/ and turned into an UpdateTask.
|
||||
*
|
||||
* @since 0.9.2 moved from PluginUpdateHandler
|
||||
*/
|
||||
class PluginUpdateRunner extends UpdateRunner {
|
||||
|
||||
private String _appName;
|
||||
private final String _oldVersion;
|
||||
private final URI _uri;
|
||||
private final String _xpi2pURL;
|
||||
private boolean _updated;
|
||||
|
||||
private static final String XPI2P = "app.xpi2p";
|
||||
private static final String ZIP = XPI2P + ".zip";
|
||||
public static final String PLUGIN_DIR = PluginStarter.PLUGIN_DIR;
|
||||
|
||||
public PluginUpdateRunner(RouterContext ctx, List<URI> uris, String appName, String oldVersion ) {
|
||||
super(ctx, uris);
|
||||
if (uris.isEmpty())
|
||||
_uri = null;
|
||||
else
|
||||
_uri = uris.get(0);
|
||||
_xpi2pURL = _uri.toString();
|
||||
_appName = appName;
|
||||
_oldVersion = oldVersion;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public UpdateType getType() {
|
||||
return _oldVersion.equals("") ? UpdateType.PLUGIN_INSTALL : UpdateType.PLUGIN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getURI() { return _uri; }
|
||||
|
||||
@Override
|
||||
protected void update() {
|
||||
|
||||
_updated = false;
|
||||
if(_xpi2pURL.startsWith("file://")) {
|
||||
updateStatus("<b>" + _("Attempting to install from file {0}", _xpi2pURL) + "</b>");
|
||||
// strip off "file://"
|
||||
String xpi2pfile = _xpi2pURL.substring(7);
|
||||
if(xpi2pfile.length() == 0) { // This is actually what String.isEmpty() does, so it should be safe.
|
||||
statusDone("<b>" + _("No file specified {0}", _xpi2pURL) + "</b>");
|
||||
} else {
|
||||
// copy the contents of from to _updateFile
|
||||
long alreadyTransferred = (new File(xpi2pfile)).getAbsoluteFile().length();
|
||||
if(FileUtil.copy((new File(xpi2pfile)).getAbsolutePath(), _updateFile, true, false)) {
|
||||
transferComplete(alreadyTransferred, alreadyTransferred, 0L, _xpi2pURL, _updateFile, false);
|
||||
} else {
|
||||
statusDone("<b>" + _("Failed to install from file {0}, copy failed.", _xpi2pURL) + "</b>");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateStatus("<b>" + _("Downloading plugin from {0}", _xpi2pURL) + "</b>");
|
||||
// use the same settings as for updater
|
||||
boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
|
||||
String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
|
||||
int proxyPort = ConfigUpdateHandler.proxyPort(_context);
|
||||
try {
|
||||
if (shouldProxy)
|
||||
// 10 retries!!
|
||||
_get = new EepGet(_context, proxyHost, proxyPort, 10, _updateFile, _xpi2pURL, false);
|
||||
else
|
||||
_get = new EepGet(_context, 1, _updateFile, _xpi2pURL, false);
|
||||
_get.addStatusListener(PluginUpdateRunner.this);
|
||||
_get.fetch();
|
||||
} catch (Throwable t) {
|
||||
_log.error("Error downloading plugin", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
|
||||
boolean update = false;
|
||||
updateStatus("<b>" + _("Plugin downloaded") + "</b>");
|
||||
File f = new File(_updateFile);
|
||||
File appDir = new SecureDirectory(_context.getConfigDir(), PLUGIN_DIR);
|
||||
if ((!appDir.exists()) && (!appDir.mkdir())) {
|
||||
f.delete();
|
||||
statusDone("<b>" + _("Cannot create plugin directory {0}", appDir.getAbsolutePath()) + "</b>");
|
||||
return;
|
||||
}
|
||||
|
||||
TrustedUpdate up = new TrustedUpdate(_context);
|
||||
File to = new File(_context.getTempDir(), "tmp" + _context.random().nextInt() + ZIP);
|
||||
// extract to a zip file whether the sig is good or not, so we can get the properties file
|
||||
String err = up.migrateFile(f, to);
|
||||
if (err != null) {
|
||||
statusDone("<b>" + err + ' ' + _("from {0}", url) + " </b>");
|
||||
f.delete();
|
||||
to.delete();
|
||||
return;
|
||||
}
|
||||
File tempDir = new File(_context.getTempDir(), "tmp" + _context.random().nextInt() + "-unzip");
|
||||
if (!FileUtil.extractZip(to, tempDir)) {
|
||||
f.delete();
|
||||
to.delete();
|
||||
FileUtil.rmdir(tempDir, false);
|
||||
statusDone("<b>" + _("Plugin from {0} is corrupt", url) + "</b>");
|
||||
return;
|
||||
}
|
||||
File installProps = new File(tempDir, "plugin.config");
|
||||
Properties props = new OrderedProperties();
|
||||
try {
|
||||
DataHelper.loadProps(props, installProps);
|
||||
} catch (IOException ioe) {
|
||||
f.delete();
|
||||
to.delete();
|
||||
FileUtil.rmdir(tempDir, false);
|
||||
statusDone("<b>" + _("Plugin from {0} does not contain the required configuration file", url) + "</b>");
|
||||
return;
|
||||
}
|
||||
// we don't need this anymore, we will unzip again
|
||||
FileUtil.rmdir(tempDir, false);
|
||||
|
||||
// ok, now we check sigs and deal with a bad sig
|
||||
String pubkey = props.getProperty("key");
|
||||
String signer = props.getProperty("signer");
|
||||
if (pubkey == null || signer == null || pubkey.length() != 172 || signer.length() <= 0) {
|
||||
f.delete();
|
||||
to.delete();
|
||||
//updateStatus("<b>" + "Plugin contains an invalid key" + ' ' + pubkey + ' ' + signer + "</b>");
|
||||
statusDone("<b>" + _("Plugin from {0} contains an invalid key", url) + "</b>");
|
||||
return;
|
||||
}
|
||||
|
||||
// add all existing plugin keys, so any conflicts with existing keys
|
||||
// will be discovered and rejected
|
||||
Map<String, String> existingKeys = PluginStarter.getPluginKeys(_context);
|
||||
for (Map.Entry<String, String> e : existingKeys.entrySet()) {
|
||||
// ignore dups/bad keys
|
||||
up.addKey(e.getKey(), e.getValue());
|
||||
}
|
||||
|
||||
if (up.haveKey(pubkey)) {
|
||||
// the key is already in the TrustedUpdate keyring
|
||||
// verify the sig and verify that it is signed by the signer in the plugin.config file
|
||||
// Allow "" as the previously-known signer
|
||||
String signingKeyName = up.verifyAndGetSigner(f);
|
||||
if (!(signer.equals(signingKeyName) || "".equals(signingKeyName))) {
|
||||
f.delete();
|
||||
to.delete();
|
||||
if (signingKeyName == null)
|
||||
_log.error("Failed to verify plugin signature, corrupt plugin or bad signature, signed by: " + signer);
|
||||
else
|
||||
_log.error("Plugin signer \"" + signer + "\" does not match existing signer in plugin.config file \"" + signingKeyName + "\"");
|
||||
statusDone("<b>" + _("Plugin signature verification of {0} failed", url) + "</b>");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// add to keyring...
|
||||
if(!up.addKey(pubkey, signer)) {
|
||||
// bad or duplicate key
|
||||
f.delete();
|
||||
to.delete();
|
||||
_log.error("Bad key or key mismatch - Failed to add plugin key \"" + pubkey + "\" for plugin signer \"" + signer + "\"");
|
||||
statusDone("<b>" + _("Plugin signature verification of {0} failed", url) + "</b>");
|
||||
return;
|
||||
}
|
||||
// ...and try the verify again
|
||||
// verify the sig and verify that it is signed by the signer in the plugin.config file
|
||||
String signingKeyName = up.verifyAndGetSigner(f);
|
||||
if (!signer.equals(signingKeyName)) {
|
||||
f.delete();
|
||||
to.delete();
|
||||
if (signingKeyName == null)
|
||||
_log.error("Failed to verify plugin signature, corrupt plugin or bad signature, signed by: " + signer);
|
||||
else
|
||||
// shouldn't happen
|
||||
_log.error("Plugin signer \"" + signer + "\" does not match new signer in plugin.config file \"" + signingKeyName + "\"");
|
||||
statusDone("<b>" + _("Plugin signature verification of {0} failed", url) + "</b>");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String sudVersion = TrustedUpdate.getVersionString(f);
|
||||
f.delete();
|
||||
|
||||
String appName = props.getProperty("name");
|
||||
String version = props.getProperty("version");
|
||||
if (appName == null || version == null || appName.length() <= 0 || version.length() <= 0 ||
|
||||
appName.indexOf("<") >= 0 || appName.indexOf(">") >= 0 ||
|
||||
version.indexOf("<") >= 0 || version.indexOf(">") >= 0 ||
|
||||
appName.startsWith(".") || appName.indexOf("/") >= 0 || appName.indexOf("\\") >= 0) {
|
||||
to.delete();
|
||||
statusDone("<b>" + _("Plugin from {0} has invalid name or version", url) + "</b>");
|
||||
return;
|
||||
}
|
||||
if (!version.equals(sudVersion)) {
|
||||
to.delete();
|
||||
statusDone("<b>" + _("Plugin {0} has mismatched versions", appName) + "</b>");
|
||||
return;
|
||||
}
|
||||
|
||||
String minVersion = ConfigClientsHelper.stripHTML(props, "min-i2p-version");
|
||||
if (minVersion != null &&
|
||||
(new VersionComparator()).compare(CoreVersion.VERSION, minVersion) < 0) {
|
||||
to.delete();
|
||||
statusDone("<b>" + _("This plugin requires I2P version {0} or higher", minVersion) + "</b>");
|
||||
return;
|
||||
}
|
||||
|
||||
minVersion = ConfigClientsHelper.stripHTML(props, "min-java-version");
|
||||
if (minVersion != null &&
|
||||
(new VersionComparator()).compare(System.getProperty("java.version"), minVersion) < 0) {
|
||||
to.delete();
|
||||
statusDone("<b>" + _("This plugin requires Java version {0} or higher", minVersion) + "</b>");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean wasRunning = false;
|
||||
File destDir = new SecureDirectory(appDir, appName);
|
||||
if (destDir.exists()) {
|
||||
if (Boolean.valueOf(props.getProperty("install-only")).booleanValue()) {
|
||||
to.delete();
|
||||
statusDone("<b>" + _("Downloaded plugin is for new installs only, but the plugin is already installed", url) + "</b>");
|
||||
return;
|
||||
}
|
||||
// compare previous version
|
||||
File oldPropFile = new File(destDir, "plugin.config");
|
||||
Properties oldProps = new OrderedProperties();
|
||||
try {
|
||||
DataHelper.loadProps(oldProps, oldPropFile);
|
||||
} catch (IOException ioe) {
|
||||
to.delete();
|
||||
FileUtil.rmdir(tempDir, false);
|
||||
statusDone("<b>" + _("Installed plugin does not contain the required configuration file", url) + "</b>");
|
||||
return;
|
||||
}
|
||||
String oldPubkey = oldProps.getProperty("key");
|
||||
String oldKeyName = oldProps.getProperty("signer");
|
||||
String oldAppName = oldProps.getProperty("name");
|
||||
if ((!pubkey.equals(oldPubkey)) || (!signer.equals(oldKeyName)) || (!appName.equals(oldAppName))) {
|
||||
to.delete();
|
||||
statusDone("<b>" + _("Signature of downloaded plugin does not match installed plugin") + "</b>");
|
||||
return;
|
||||
}
|
||||
String oldVersion = oldProps.getProperty("version");
|
||||
if (oldVersion == null ||
|
||||
(new VersionComparator()).compare(oldVersion, version) >= 0) {
|
||||
to.delete();
|
||||
statusDone("<b>" + _("Downloaded plugin version {0} is not newer than installed plugin", version) + "</b>");
|
||||
return;
|
||||
}
|
||||
minVersion = ConfigClientsHelper.stripHTML(props, "min-installed-version");
|
||||
if (minVersion != null &&
|
||||
(new VersionComparator()).compare(minVersion, oldVersion) > 0) {
|
||||
to.delete();
|
||||
statusDone("<b>" + _("Plugin update requires installed plugin version {0} or higher", minVersion) + "</b>");
|
||||
return;
|
||||
}
|
||||
String maxVersion = ConfigClientsHelper.stripHTML(props, "max-installed-version");
|
||||
if (maxVersion != null &&
|
||||
(new VersionComparator()).compare(maxVersion, oldVersion) < 0) {
|
||||
to.delete();
|
||||
statusDone("<b>" + _("Plugin update requires installed plugin version {0} or lower", maxVersion) + "</b>");
|
||||
return;
|
||||
}
|
||||
oldVersion = LogsHelper.jettyVersion();
|
||||
minVersion = ConfigClientsHelper.stripHTML(props, "min-jetty-version");
|
||||
if (minVersion != null &&
|
||||
(new VersionComparator()).compare(minVersion, oldVersion) > 0) {
|
||||
to.delete();
|
||||
statusDone("<b>" + _("Plugin requires Jetty version {0} or higher", minVersion) + "</b>");
|
||||
return;
|
||||
}
|
||||
maxVersion = ConfigClientsHelper.stripHTML(props, "max-jetty-version");
|
||||
if (maxVersion != null &&
|
||||
(new VersionComparator()).compare(maxVersion, oldVersion) < 0) {
|
||||
to.delete();
|
||||
statusDone("<b>" + _("Plugin requires Jetty version {0} or lower", maxVersion) + "</b>");
|
||||
return;
|
||||
}
|
||||
// do we defer extraction and installation?
|
||||
if (Boolean.valueOf(props.getProperty("router-restart-required")).booleanValue()) {
|
||||
// Yup!
|
||||
try {
|
||||
if(!FileUtil.copy(to, (new SecureFile( new SecureFile(appDir.getCanonicalPath() +"/" + appName +"/"+ ZIP).getCanonicalPath())) , true, true)) {
|
||||
to.delete();
|
||||
statusDone("<b>" + _("Cannot copy plugin to directory {0}", destDir.getAbsolutePath()) + "</b>");
|
||||
return;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
to.delete();
|
||||
_log.error("Error copying plugin {0}", t);
|
||||
return;
|
||||
}
|
||||
// we don't need the original file anymore.
|
||||
to.delete();
|
||||
statusDone("<b>" + _("Plugin will be installed on next restart.") + "</b>");
|
||||
return;
|
||||
}
|
||||
if (PluginStarter.isPluginRunning(appName, _context)) {
|
||||
wasRunning = true;
|
||||
try {
|
||||
if (!PluginStarter.stopPlugin(_context, appName)) {
|
||||
// failed, ignore
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
// no updateStatus() for this one
|
||||
_log.error("Error stopping plugin " + appName, e);
|
||||
}
|
||||
}
|
||||
update = true;
|
||||
} else {
|
||||
if (Boolean.valueOf(props.getProperty("update-only")).booleanValue()) {
|
||||
to.delete();
|
||||
statusDone("<b>" + _("Plugin is for upgrades only, but the plugin is not installed") + "</b>");
|
||||
return;
|
||||
}
|
||||
if (!destDir.mkdir()) {
|
||||
to.delete();
|
||||
statusDone("<b>" + _("Cannot create plugin directory {0}", destDir.getAbsolutePath()) + "</b>");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, extract the zip to the plugin directory
|
||||
if (!FileUtil.extractZip(to, destDir)) {
|
||||
to.delete();
|
||||
statusDone("<b>" + _("Failed to install plugin in {0}", destDir.getAbsolutePath()) + "</b>");
|
||||
return;
|
||||
}
|
||||
_updated = true;
|
||||
to.delete();
|
||||
// install != update. Changing the user's settings like this is probabbly a bad idea.
|
||||
if (Boolean.valueOf( props.getProperty("dont-start-at-install")).booleanValue()) {
|
||||
statusDone("<b>" + _("Plugin {0} installed", appName) + "</b>");
|
||||
if(!update) {
|
||||
Properties pluginProps = PluginStarter.pluginProperties();
|
||||
pluginProps.setProperty(PluginStarter.PREFIX + appName + PluginStarter.ENABLED, "false");
|
||||
PluginStarter.storePluginProperties(pluginProps);
|
||||
}
|
||||
} else if (wasRunning || PluginStarter.isPluginEnabled(appName)) {
|
||||
// start everything unless it was disabled and not running before
|
||||
try {
|
||||
if (PluginStarter.startPlugin(_context, appName)) {
|
||||
String linkName = ConfigClientsHelper.stripHTML(props, "consoleLinkName_" + Messages.getLanguage(_context));
|
||||
if (linkName == null)
|
||||
linkName = ConfigClientsHelper.stripHTML(props, "consoleLinkName");
|
||||
String linkURL = ConfigClientsHelper.stripHTML(props, "consoleLinkURL");
|
||||
String link;
|
||||
if (linkName != null && linkURL != null)
|
||||
link = "<a target=\"_blank\" href=\"" + linkURL + "\"/>" + linkName + "</a>";
|
||||
else
|
||||
link = appName;
|
||||
statusDone("<b>" + _("Plugin {0} installed and started", link) + "</b>");
|
||||
}
|
||||
else
|
||||
statusDone("<b>" + _("Plugin {0} installed but failed to start, check logs", appName) + "</b>");
|
||||
} catch (Throwable e) {
|
||||
statusDone("<b>" + _("Plugin {0} installed but failed to start", appName) + ": " + e + "</b>");
|
||||
_log.error("Error starting plugin " + appName, e);
|
||||
}
|
||||
} else {
|
||||
statusDone("<b>" + _("Plugin {0} installed", appName) + "</b>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
|
||||
File f = new File(_updateFile);
|
||||
f.delete();
|
||||
statusDone("<b>" + _("Failed to download plugin from {0}", url) + "</b>");
|
||||
}
|
||||
|
||||
private void statusDone(String msg) {
|
||||
updateStatus(msg);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,94 @@
|
||||
package net.i2p.router.update;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
import java.util.Date;
|
||||
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.router.util.RFC822Date;
|
||||
import net.i2p.router.web.ConfigUpdateHandler;
|
||||
import net.i2p.router.web.Messages;
|
||||
import net.i2p.update.*;
|
||||
import net.i2p.util.EepHead;
|
||||
import net.i2p.util.I2PAppThread;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* Does a simple EepHead to get the last-modified header.
|
||||
* Moved from NewsFetcher and turned into an UpdateTask.
|
||||
*
|
||||
* Overrides UpdateRunner for convenience, does not use super's Eepget StatusListener
|
||||
*
|
||||
* @since 0.9.2
|
||||
*/
|
||||
class UnsignedUpdateChecker extends UpdateRunner {
|
||||
private final long _ms;
|
||||
private boolean _unsignedUpdateAvailable;
|
||||
|
||||
protected static final String SIGNED_UPDATE_FILE = "i2pupdate.sud";
|
||||
|
||||
public UnsignedUpdateChecker(RouterContext ctx, List<URI> uris, long lastUpdateTime) {
|
||||
super(ctx, uris);
|
||||
_ms = lastUpdateTime;
|
||||
}
|
||||
|
||||
//////// begin UpdateTask methods
|
||||
|
||||
@Override
|
||||
public UpdateType getType() { return UpdateType.ROUTER_UNSIGNED; }
|
||||
|
||||
//////// end UpdateTask methods
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
_isRunning = true;
|
||||
boolean success = false;
|
||||
try {
|
||||
success = fetchUnsignedHead();
|
||||
} finally {
|
||||
_isRunning = false;
|
||||
}
|
||||
_mgr.notifyCheckComplete(this, _unsignedUpdateAvailable, success);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* HEAD the update url, and if the last-mod time is newer than the last update we
|
||||
* downloaded, as stored in the properties, then we download it using eepget.
|
||||
*/
|
||||
private boolean fetchUnsignedHead() {
|
||||
if (_urls.isEmpty())
|
||||
return false;
|
||||
_currentURI = _urls.get(0);
|
||||
String url = _currentURI.toString();
|
||||
// assume always proxied for now
|
||||
//boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
|
||||
String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
|
||||
int proxyPort = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_PORT, ConfigUpdateHandler.DEFAULT_PROXY_PORT_INT);
|
||||
|
||||
try {
|
||||
EepHead get = new EepHead(_context, proxyHost, proxyPort, 0, url);
|
||||
if (get.fetch()) {
|
||||
String lastmod = get.getLastModified();
|
||||
if (lastmod != null) {
|
||||
long modtime = RFC822Date.parse822Date(lastmod);
|
||||
if (modtime <= 0) return false;
|
||||
if (_ms <= 0) return false;
|
||||
if (modtime > _ms) {
|
||||
_unsignedUpdateAvailable = true;
|
||||
// '07-Jul 21:09 UTC' with month name in the system locale
|
||||
String unsignedUpdateVersion = (new SimpleDateFormat("dd-MMM HH:mm")).format(new Date(modtime)) + " UTC";
|
||||
_mgr.notifyVersionAvailable(this, _urls.get(0), getType(), "", getMethod(), _urls,
|
||||
unsignedUpdateVersion, "");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
_log.error("Error fetching the unsigned update", t);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package net.i2p.router.update;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import net.i2p.router.Router;
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.router.util.RFC822Date;
|
||||
import net.i2p.router.web.ConfigUpdateHandler;
|
||||
import net.i2p.router.web.NewsHelper;
|
||||
import net.i2p.update.*;
|
||||
import net.i2p.util.EepGet;
|
||||
import net.i2p.util.FileUtil;
|
||||
import net.i2p.util.I2PAppThread;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
/**
|
||||
* <p>Handles the request to update the router by firing off an
|
||||
* {@link net.i2p.util.EepGet} call to download the latest unsigned zip file
|
||||
* and displaying the status to anyone who asks.
|
||||
* </p>
|
||||
* <p>After the download completes the signed update file is copied to the
|
||||
* router directory, and if configured the router is restarted to complete
|
||||
* the update process.
|
||||
* </p>
|
||||
*/
|
||||
class UnsignedUpdateHandler implements Updater {
|
||||
private final RouterContext _context;
|
||||
|
||||
public UnsignedUpdateHandler(RouterContext ctx) {
|
||||
_context = ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param currentVersion ignored, we use time stored in a property
|
||||
*/
|
||||
@Override
|
||||
public UpdateTask check(UpdateType type, UpdateMethod method,
|
||||
String id, String currentVersion, long maxTime) {
|
||||
if (type != UpdateType.ROUTER_UNSIGNED || method != UpdateMethod.HTTP)
|
||||
return null;
|
||||
|
||||
String url = _context.getProperty(ConfigUpdateHandler.PROP_ZIP_URL);
|
||||
if (url == null)
|
||||
return null;
|
||||
|
||||
List<URI> updateSources;
|
||||
try {
|
||||
updateSources = Collections.singletonList(new URI(url));
|
||||
} catch (URISyntaxException use) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String lastUpdate = _context.getProperty(NewsHelper.PROP_LAST_UPDATE_TIME);
|
||||
if (lastUpdate == null) {
|
||||
// we don't know what version you have, so stamp it with the current time,
|
||||
// and we'll look for something newer next time around.
|
||||
_context.router().saveConfig(NewsHelper.PROP_LAST_UPDATE_TIME,
|
||||
Long.toString(_context.clock().now()));
|
||||
return null;
|
||||
}
|
||||
long ms = 0;
|
||||
try {
|
||||
ms = Long.parseLong(lastUpdate);
|
||||
} catch (NumberFormatException nfe) {}
|
||||
if (ms <= 0) {
|
||||
// we don't know what version you have, so stamp it with the current time,
|
||||
// and we'll look for something newer next time around.
|
||||
_context.router().saveConfig(NewsHelper.PROP_LAST_UPDATE_TIME,
|
||||
Long.toString(_context.clock().now()));
|
||||
return null;
|
||||
}
|
||||
|
||||
UpdateRunner update = new UnsignedUpdateChecker(_context, updateSources, ms);
|
||||
update.start();
|
||||
return update;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a download and return a handle to the download task.
|
||||
* Should not block.
|
||||
*
|
||||
* @param id plugin name or ignored
|
||||
* @param maxTime how long you have
|
||||
* @return active task or null if unable to download
|
||||
*/
|
||||
@Override
|
||||
public UpdateTask update(UpdateType type, UpdateMethod method, List<URI> updateSources,
|
||||
String id, String newVersion, long maxTime) {
|
||||
if (type != UpdateType.ROUTER_UNSIGNED || method != UpdateMethod.HTTP || updateSources.isEmpty())
|
||||
return null;
|
||||
UpdateRunner update = new UnsignedUpdateRunner(_context, updateSources);
|
||||
update.start();
|
||||
return update;
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package net.i2p.router.update;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
import net.i2p.router.Router;
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.router.util.RFC822Date;
|
||||
import net.i2p.router.web.ConfigUpdateHandler;
|
||||
import net.i2p.update.*;
|
||||
import net.i2p.util.EepGet;
|
||||
import net.i2p.util.FileUtil;
|
||||
import net.i2p.util.I2PAppThread;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
|
||||
/**
|
||||
* Eepget the .zip file to the temp dir, then notify.r
|
||||
* Moved from UnsignedUpdateHandler and turned into an UpdateTask.
|
||||
*
|
||||
* @since 0.9.2
|
||||
*/
|
||||
class UnsignedUpdateRunner extends UpdateRunner {
|
||||
|
||||
public UnsignedUpdateRunner(RouterContext ctx, List<URI> uris) {
|
||||
super(ctx, uris);
|
||||
if (!uris.isEmpty())
|
||||
_currentURI = uris.get(0);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public UpdateType getType() { return UpdateType.ROUTER_UNSIGNED; }
|
||||
|
||||
|
||||
/** Get the file */
|
||||
@Override
|
||||
protected void update() {
|
||||
String zipURL = _currentURI.toString();
|
||||
updateStatus("<b>" + _("Updating") + "</b>");
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Starting unsigned update URL: " + zipURL);
|
||||
// always proxy for now
|
||||
//boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
|
||||
String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
|
||||
int proxyPort = ConfigUpdateHandler.proxyPort(_context);
|
||||
try {
|
||||
// 40 retries!!
|
||||
_get = new EepGet(_context, proxyHost, proxyPort, 40, _updateFile, zipURL, false);
|
||||
_get.addStatusListener(UnsignedUpdateRunner.this);
|
||||
_get.fetch();
|
||||
} catch (Throwable t) {
|
||||
_log.error("Error updating", t);
|
||||
}
|
||||
}
|
||||
|
||||
/** eepget listener callback Overrides */
|
||||
@Override
|
||||
public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
|
||||
String lastmod = _get.getLastModified();
|
||||
File tmp = new File(_updateFile);
|
||||
/////// FIXME RFC822 or long?
|
||||
if (_mgr.notifyComplete(this, lastmod, tmp))
|
||||
this.done = true;
|
||||
else
|
||||
tmp.delete(); // corrupt
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package net.i2p.router.update;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import net.i2p.crypto.TrustedUpdate;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.router.Router;
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.router.RouterVersion;
|
||||
import net.i2p.router.util.RFC822Date;
|
||||
import net.i2p.update.*;
|
||||
import net.i2p.util.EepGet;
|
||||
import net.i2p.util.I2PAppThread;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.PartialEepGet;
|
||||
import net.i2p.util.VersionComparator;
|
||||
|
||||
/**
|
||||
* <p>Handles the request to update the router by firing one or more
|
||||
* {@link net.i2p.util.EepGet} calls to download the latest signed update file
|
||||
* and displaying the status to anyone who asks.
|
||||
* </p>
|
||||
* <p>After the download completes the signed update file is verified with
|
||||
* {@link net.i2p.crypto.TrustedUpdate}, and if it's authentic the payload
|
||||
* of the signed update file is unpacked and the router is restarted to complete
|
||||
* the update process.
|
||||
* </p>
|
||||
*
|
||||
* This does not do any checking, that is handled by the NewsFetcher.
|
||||
*/
|
||||
class UpdateHandler implements Updater {
|
||||
protected final RouterContext _context;
|
||||
|
||||
public UpdateHandler(RouterContext ctx) {
|
||||
_context = ctx;
|
||||
}
|
||||
|
||||
/** Can't check, the NewsHandler does that */
|
||||
public UpdateTask check(UpdateType type, UpdateMethod method,
|
||||
String id, String currentVersion, long maxTime) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a download and return a handle to the download task.
|
||||
* Should not block.
|
||||
*
|
||||
* @param id plugin name or ignored
|
||||
* @param maxTime how long you have
|
||||
* @return active task or null if unable to download
|
||||
*/
|
||||
public UpdateTask update(UpdateType type, UpdateMethod method, List<URI> updateSources,
|
||||
String id, String newVersion, long maxTime) {
|
||||
if ((type != UpdateType.ROUTER_SIGNED && type != UpdateType.ROUTER_SIGNED_PACK200) ||
|
||||
method != UpdateMethod.HTTP || updateSources.isEmpty())
|
||||
return null;
|
||||
UpdateRunner update = new UpdateRunner(_context, updateSources);
|
||||
update.start();
|
||||
return update;
|
||||
}
|
||||
}
|
@ -0,0 +1,231 @@
|
||||
package net.i2p.router.update;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
import net.i2p.crypto.TrustedUpdate;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.router.RouterVersion;
|
||||
import net.i2p.router.web.ConfigUpdateHandler;
|
||||
import net.i2p.router.web.Messages;
|
||||
import net.i2p.update.*;
|
||||
import net.i2p.util.EepGet;
|
||||
import net.i2p.util.I2PAppThread;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.PartialEepGet;
|
||||
import net.i2p.util.VersionComparator;
|
||||
|
||||
/**
|
||||
* The downloader for router signed updates,
|
||||
* and the base class for all the other Checkers and Runners.
|
||||
*
|
||||
* @since 0.9.2 moved from UpdateHandler
|
||||
*
|
||||
*/
|
||||
class UpdateRunner extends I2PAppThread implements UpdateTask, EepGet.StatusListener {
|
||||
protected final RouterContext _context;
|
||||
protected final Log _log;
|
||||
protected final ConsoleUpdateManager _mgr;
|
||||
protected final List<URI> _urls;
|
||||
protected final String _updateFile;
|
||||
protected volatile boolean _isRunning;
|
||||
protected boolean done;
|
||||
protected EepGet _get;
|
||||
/** tells the listeners what mode we are in - set to true in extending classes for checks */
|
||||
protected boolean _isPartial;
|
||||
/** set by the listeners on completion */
|
||||
protected String _newVersion;
|
||||
private ByteArrayOutputStream _baos;
|
||||
protected URI _currentURI;
|
||||
|
||||
private static final String SIGNED_UPDATE_FILE = "i2pupdate.sud";
|
||||
|
||||
public UpdateRunner(RouterContext ctx, List<URI> uris) {
|
||||
super("Update Runner");
|
||||
setDaemon(true);
|
||||
_context = ctx;
|
||||
_log = ctx.logManager().getLog(getClass());
|
||||
_mgr = (ConsoleUpdateManager) ctx.updateManager();
|
||||
_urls = uris;
|
||||
_updateFile = (new File(ctx.getTempDir(), "update" + ctx.random().nextInt() + ".tmp")).getAbsolutePath();
|
||||
}
|
||||
|
||||
//////// begin UpdateTask methods
|
||||
|
||||
public boolean isRunning() { return _isRunning; }
|
||||
|
||||
public void shutdown() {
|
||||
_isRunning = false;
|
||||
interrupt();
|
||||
}
|
||||
|
||||
public UpdateType getType() { return UpdateType.ROUTER_SIGNED; }
|
||||
|
||||
public UpdateMethod getMethod() { return UpdateMethod.HTTP; }
|
||||
|
||||
public URI getURI() { return _currentURI; }
|
||||
|
||||
public String getID() { return ""; }
|
||||
|
||||
//////// end UpdateTask methods
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
_isRunning = true;
|
||||
try {
|
||||
update();
|
||||
} finally {
|
||||
_isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop through the entire list of update URLs.
|
||||
* For each one, first get the version from the first 56 bytes and see if
|
||||
* it is newer than what we are running now.
|
||||
* If it is, get the whole thing.
|
||||
*/
|
||||
protected void update() {
|
||||
// Do a PartialEepGet on the selected URL, check for version we expect,
|
||||
// and loop if it isn't what we want.
|
||||
// This will allows us to do a release without waiting for the last host to install the update.
|
||||
// Alternative: In bytesTransferred(), Check the data in the output file after
|
||||
// we've received at least 56 bytes. Need a cancel() method in EepGet ?
|
||||
|
||||
boolean shouldProxy = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)).booleanValue();
|
||||
String proxyHost = _context.getProperty(ConfigUpdateHandler.PROP_PROXY_HOST, ConfigUpdateHandler.DEFAULT_PROXY_HOST);
|
||||
int proxyPort = ConfigUpdateHandler.proxyPort(_context);
|
||||
|
||||
if (_urls.isEmpty()) {
|
||||
// not likely, don't bother translating
|
||||
updateStatus("<b>Update source list is empty, cannot download update</b>");
|
||||
_log.log(Log.CRIT, "Update source list is empty - cannot download update");
|
||||
_mgr.notifyTaskFailed(this, "", null);
|
||||
return;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = null;
|
||||
if (shouldProxy)
|
||||
baos = new ByteArrayOutputStream(TrustedUpdate.HEADER_BYTES);
|
||||
for (URI uri : _urls) {
|
||||
_currentURI = uri;
|
||||
String updateURL = uri.toString();
|
||||
updateStatus("<b>" + _("Updating from {0}", linkify(updateURL)) + "</b>");
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Selected update URL: " + updateURL);
|
||||
|
||||
// Check the first 56 bytes for the version
|
||||
if (shouldProxy) {
|
||||
_isPartial = true;
|
||||
baos.reset();
|
||||
try {
|
||||
// no retries
|
||||
_get = new PartialEepGet(_context, proxyHost, proxyPort, baos, updateURL, TrustedUpdate.HEADER_BYTES);
|
||||
_get.addStatusListener(UpdateRunner.this);
|
||||
_get.fetch();
|
||||
} catch (Throwable t) {
|
||||
}
|
||||
_isPartial = false;
|
||||
if (_newVersion == null)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Now get the whole thing
|
||||
try {
|
||||
if (shouldProxy)
|
||||
// 40 retries!!
|
||||
_get = new EepGet(_context, proxyHost, proxyPort, 40, _updateFile, updateURL, false);
|
||||
else
|
||||
_get = new EepGet(_context, 1, _updateFile, updateURL, false);
|
||||
_get.addStatusListener(UpdateRunner.this);
|
||||
_get.fetch();
|
||||
} catch (Throwable t) {
|
||||
_log.error("Error updating", t);
|
||||
}
|
||||
if (this.done)
|
||||
break;
|
||||
}
|
||||
(new File(_updateFile)).delete();
|
||||
if (!this.done)
|
||||
_mgr.notifyTaskFailed(this, "", null);
|
||||
}
|
||||
|
||||
// EepGet Listeners below.
|
||||
// We use the same for both the partial and the full EepGet,
|
||||
// with a couple of adjustments depending on which mode.
|
||||
|
||||
public void attemptFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt, int numRetries, Exception cause) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Attempt failed on " + url, cause);
|
||||
// ignored
|
||||
}
|
||||
|
||||
/** subclasses should override */
|
||||
public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {
|
||||
if (_isPartial)
|
||||
return;
|
||||
long d = currentWrite + bytesTransferred;
|
||||
String status = "<b>" + _("Updating") + "</b>";
|
||||
_mgr.notifyProgress(this, status, d, d + bytesRemaining);
|
||||
}
|
||||
|
||||
/** subclasses should override */
|
||||
public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
|
||||
if (_isPartial) {
|
||||
// Compare version with what we have now
|
||||
String newVersion = TrustedUpdate.getVersionString(new ByteArrayInputStream(_baos.toByteArray()));
|
||||
boolean newer = (new VersionComparator()).compare(newVersion, RouterVersion.VERSION) > 0;
|
||||
if (newer) {
|
||||
_newVersion = newVersion;
|
||||
} else {
|
||||
updateStatus("<b>" + _("No new version found at {0}", linkify(url)) + "</b>");
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Found old version \"" + newVersion + "\" at " + url);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
File tmp = new File(_updateFile);
|
||||
if (_mgr.notifyComplete(this, _newVersion, tmp))
|
||||
this.done = true;
|
||||
else
|
||||
tmp.delete(); // corrupt
|
||||
}
|
||||
|
||||
/** subclasses should override */
|
||||
public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
|
||||
// don't display bytesTransferred as it is meaningless
|
||||
_log.error("Update from " + url + " did not download completely (" +
|
||||
bytesRemaining + " remaining after " + currentAttempt + " tries)");
|
||||
updateStatus("<b>" + _("Transfer failed from {0}", linkify(url)) + "</b>");
|
||||
}
|
||||
|
||||
public void headerReceived(String url, int attemptNum, String key, String val) {}
|
||||
|
||||
public void attempting(String url) {}
|
||||
|
||||
protected void updateStatus(String s) {
|
||||
_mgr.notifyProgress(this, s);
|
||||
}
|
||||
|
||||
protected static String linkify(String url) {
|
||||
return ConsoleUpdateManager.linkify(url);
|
||||
}
|
||||
|
||||
/** translate a string */
|
||||
protected String _(String s) {
|
||||
return Messages.getString(s, _context);
|
||||
}
|
||||
|
||||
/**
|
||||
* translate a string with a parameter
|
||||
*/
|
||||
protected String _(String s, Object o) {
|
||||
return Messages.getString(s, o, _context);
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>
|
||||
Classes to implement the update process.
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user