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:
@@ -1,25 +1,10 @@
|
||||
package net.i2p.router.web;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
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.util.EepGet;
|
||||
import net.i2p.util.I2PAppThread;
|
||||
import net.i2p.router.update.ConsoleUpdateManager;
|
||||
import net.i2p.update.UpdateType;
|
||||
import static net.i2p.update.UpdateType.*;
|
||||
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
|
||||
@@ -31,27 +16,22 @@ import net.i2p.util.VersionComparator;
|
||||
* of the signed update file is unpacked and the router is restarted to complete
|
||||
* the update process.
|
||||
* </p>
|
||||
*
|
||||
* This is like a FormHandler but we don't extend it, as we don't have the message area, etc.
|
||||
*/
|
||||
public class UpdateHandler {
|
||||
protected static UpdateRunner _updateRunner;
|
||||
protected RouterContext _context;
|
||||
protected Log _log;
|
||||
protected String _updateFile;
|
||||
private static String _status = "";
|
||||
private String _action;
|
||||
private String _nonce;
|
||||
|
||||
protected static final String SIGNED_UPDATE_FILE = "i2pupdate.sud";
|
||||
static final String PROP_UPDATE_IN_PROGRESS = "net.i2p.router.web.UpdateHandler.updateInProgress";
|
||||
protected static final String PROP_LAST_UPDATE_TIME = "router.updateLastDownloaded";
|
||||
|
||||
public UpdateHandler() {
|
||||
this(ContextHelper.getContext(null));
|
||||
}
|
||||
|
||||
public UpdateHandler(RouterContext ctx) {
|
||||
_context = ctx;
|
||||
_log = ctx.logManager().getLog(UpdateHandler.class);
|
||||
_updateFile = (new File(ctx.getRouterDir(), SIGNED_UPDATE_FILE)).getAbsolutePath();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,272 +65,21 @@ public class UpdateHandler {
|
||||
if (_nonce.equals(System.getProperty("net.i2p.router.web.UpdateHandler.nonce")) ||
|
||||
_nonce.equals(System.getProperty("net.i2p.router.web.UpdateHandler.noncePrev"))) {
|
||||
if (_action.contains("Unsigned")) {
|
||||
// Not us, have NewsFetcher instantiate the correct class.
|
||||
NewsFetcher fetcher = NewsFetcher.getInstance(_context);
|
||||
fetcher.fetchUnsigned();
|
||||
update(ROUTER_UNSIGNED);
|
||||
} else {
|
||||
update();
|
||||
update(ROUTER_SIGNED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void update() {
|
||||
// don't block waiting for the other one to finish
|
||||
if ("true".equals(System.getProperty(PROP_UPDATE_IN_PROGRESS))) {
|
||||
private void update(UpdateType type) {
|
||||
ConsoleUpdateManager mgr = (ConsoleUpdateManager) _context.updateManager();
|
||||
if (mgr == null)
|
||||
return;
|
||||
if (mgr.isUpdateInProgress(ROUTER_SIGNED) || mgr.isUpdateInProgress(ROUTER_UNSIGNED)) {
|
||||
_log.error("Update already running");
|
||||
return;
|
||||
}
|
||||
synchronized (UpdateHandler.class) {
|
||||
if (_updateRunner == null)
|
||||
_updateRunner = new UpdateRunner();
|
||||
if (_updateRunner.isRunning()) {
|
||||
return;
|
||||
} else {
|
||||
System.setProperty(PROP_UPDATE_IN_PROGRESS, "true");
|
||||
I2PAppThread update = new I2PAppThread(_updateRunner, "SignedUpdate");
|
||||
update.start();
|
||||
}
|
||||
}
|
||||
mgr.update(type);
|
||||
}
|
||||
|
||||
public static String getStatus() {
|
||||
return _status;
|
||||
}
|
||||
|
||||
public boolean isDone() {
|
||||
return false;
|
||||
// this needs to be fixed and tested
|
||||
//if(this._updateRunner == null)
|
||||
// return true;
|
||||
//return this._updateRunner.isDone();
|
||||
}
|
||||
|
||||
public class UpdateRunner implements Runnable, EepGet.StatusListener {
|
||||
protected volatile boolean _isRunning;
|
||||
protected boolean done;
|
||||
protected EepGet _get;
|
||||
protected final DecimalFormat _pct = new DecimalFormat("0.0%");
|
||||
/** tells the listeners what mode we are in */
|
||||
private boolean _isPartial;
|
||||
/** set by the listeners on completion */
|
||||
private boolean _isNewer;
|
||||
private ByteArrayOutputStream _baos;
|
||||
|
||||
public UpdateRunner() {
|
||||
_isRunning = false;
|
||||
this.done = false;
|
||||
updateStatus("<b>" + _("Updating") + "</b>");
|
||||
}
|
||||
public boolean isRunning() { return _isRunning; }
|
||||
public boolean isDone() {
|
||||
return this.done;
|
||||
}
|
||||
public void run() {
|
||||
_isRunning = true;
|
||||
update();
|
||||
System.setProperty(PROP_UPDATE_IN_PROGRESS, "false");
|
||||
_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);
|
||||
|
||||
List<String> urls = getUpdateURLs();
|
||||
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");
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldProxy)
|
||||
_baos = new ByteArrayOutputStream(TrustedUpdate.HEADER_BYTES);
|
||||
for (String updateURL : urls) {
|
||||
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;
|
||||
_isNewer = false;
|
||||
_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) {
|
||||
_isNewer = false;
|
||||
}
|
||||
_isPartial = false;
|
||||
if (!_isNewer)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
_isNewer = false;
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Attempt failed on " + url, cause);
|
||||
// ignored
|
||||
}
|
||||
public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {
|
||||
if (_isPartial)
|
||||
return;
|
||||
StringBuilder buf = new StringBuilder(64);
|
||||
buf.append("<b>").append(_("Updating")).append("</b> ");
|
||||
double pct = ((double)alreadyTransferred + (double)currentWrite) /
|
||||
((double)alreadyTransferred + (double)currentWrite + bytesRemaining);
|
||||
synchronized (_pct) {
|
||||
buf.append(_pct.format(pct));
|
||||
}
|
||||
buf.append(":<br>\n");
|
||||
buf.append(_("{0}B transferred", DataHelper.formatSize2(currentWrite + alreadyTransferred)));
|
||||
updateStatus(buf.toString());
|
||||
}
|
||||
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) {
|
||||
updateStatus("<b>" + _("No new version found at {0}", linkify(url)) + "</b>");
|
||||
if (_log.shouldLog(Log.WARN))
|
||||
_log.warn("Found old version \"" + newVersion + "\" at " + url);
|
||||
}
|
||||
_isNewer = newer;
|
||||
return;
|
||||
}
|
||||
// Process the .sud/.su2 file
|
||||
updateStatus("<b>" + _("Update downloaded") + "</b>");
|
||||
TrustedUpdate up = new TrustedUpdate(_context);
|
||||
File f = new File(_updateFile);
|
||||
File to = new File(_context.getRouterDir(), Router.UPDATE_FILE);
|
||||
String err = up.migrateVerified(RouterVersion.VERSION, f, to);
|
||||
f.delete();
|
||||
if (err == null) {
|
||||
String policy = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_POLICY);
|
||||
this.done = true;
|
||||
// So unsigned update handler doesn't overwrite unless newer.
|
||||
String lastmod = _get.getLastModified();
|
||||
long modtime = 0;
|
||||
if (lastmod != null)
|
||||
modtime = RFC822Date.parse822Date(lastmod);
|
||||
if (modtime <= 0)
|
||||
modtime = _context.clock().now();
|
||||
_context.router().saveConfig(PROP_LAST_UPDATE_TIME, "" + modtime);
|
||||
if ("install".equals(policy)) {
|
||||
_log.log(Log.CRIT, "Update was VERIFIED, restarting to install it");
|
||||
updateStatus("<b>" + _("Update verified") + "</b><br>" + _("Restarting"));
|
||||
restart();
|
||||
} else {
|
||||
_log.log(Log.CRIT, "Update was VERIFIED, will be installed at next restart");
|
||||
StringBuilder buf = new StringBuilder(64);
|
||||
buf.append("<b>").append(_("Update downloaded")).append("<br>");
|
||||
if (_context.hasWrapper())
|
||||
buf.append(_("Click Restart to install"));
|
||||
else
|
||||
buf.append(_("Click Shutdown and restart to install"));
|
||||
if (up.newVersion() != null)
|
||||
buf.append(' ').append(_("Version {0}", up.newVersion()));
|
||||
buf.append("</b>");
|
||||
updateStatus(buf.toString());
|
||||
}
|
||||
} else {
|
||||
_log.log(Log.CRIT, err + " from " + url);
|
||||
updateStatus("<b>" + err + ' ' + _("from {0}", linkify(url)) + " </b>");
|
||||
}
|
||||
}
|
||||
public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
|
||||
_isNewer = false;
|
||||
// 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 restart() {
|
||||
if (_context.hasWrapper())
|
||||
ConfigServiceHandler.registerWrapperNotifier(_context, Router.EXIT_GRACEFUL_RESTART, false);
|
||||
_context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART);
|
||||
}
|
||||
|
||||
private List<String> getUpdateURLs() {
|
||||
String URLs = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_URL, ConfigUpdateHandler.DEFAULT_UPDATE_URL);
|
||||
StringTokenizer tok = new StringTokenizer(URLs, " ,\r\n");
|
||||
List<String> URLList = new ArrayList();
|
||||
while (tok.hasMoreTokens())
|
||||
URLList.add(tok.nextToken().trim());
|
||||
Collections.shuffle(URLList, _context.random());
|
||||
return URLList;
|
||||
}
|
||||
|
||||
protected void updateStatus(String s) {
|
||||
_status = s;
|
||||
}
|
||||
|
||||
protected static String linkify(String url) {
|
||||
return "<a target=\"_blank\" href=\"" + url + "\"/>" + url + "</a>";
|
||||
}
|
||||
|
||||
/** translate a string */
|
||||
protected String _(String s) {
|
||||
return Messages.getString(s, _context);
|
||||
}
|
||||
|
||||
/**
|
||||
* translate a string with a parameter
|
||||
* This is a lot more expensive than _(s), so use sparingly.
|
||||
*
|
||||
* @param s string to be translated containing {0}
|
||||
* The {0} will be replaced by the parameter.
|
||||
* Single quotes must be doubled, i.e. ' -> '' in the string.
|
||||
* @param o parameter, not translated.
|
||||
* To tranlslate parameter also, use _("foo {0} bar", _("baz"))
|
||||
* Do not double the single quotes in the parameter.
|
||||
* Use autoboxing to call with ints, longs, floats, etc.
|
||||
*/
|
||||
protected String _(String s, Object o) {
|
||||
return Messages.getString(s, o, _context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user