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:
zzz
2012-06-18 22:09:45 +00:00
parent 273d7399a0
commit e62b76d2cc
39 changed files with 2836 additions and 1235 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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);
}
}
}
}

View File

@ -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) {}
}

View File

@ -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;
}
}

View File

@ -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, "");
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -1,7 +1,9 @@
package net.i2p.router.web;
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;
@ -9,6 +11,12 @@ 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;
@ -19,107 +27,55 @@ import net.i2p.util.SimpleScheduler;
import net.i2p.util.SimpleTimer;
import net.i2p.util.VersionComparator;
/**
* Download and install a plugin.
* 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.
* Unlike for router updates, we need not have the public key
* for the signature in advance.
*
* The zip file must have a standard directory layout, with
* a plugin.config file at the top level.
* The config file contains properties for the package name, version,
* signing public key, and other settings.
* The zip file will typically contain a webapps/ or lib/ dir,
* and a webapps.config and/or clients.config 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.
*
* @since 0.7.12
* @author zzz
* Moved from web/ and turned into an UpdateTask.
*
* @since 0.9.2 moved from PluginUpdateHandler
*/
public class PluginUpdateHandler extends UpdateHandler {
private static PluginUpdateRunner _pluginUpdateRunner;
private String _xpi2pURL;
private String _appStatus;
private volatile boolean _updated;
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 = "plugins";
public static final String PLUGIN_DIR = PluginStarter.PLUGIN_DIR;
private static PluginUpdateHandler _instance;
public static final synchronized PluginUpdateHandler getInstance(RouterContext ctx) {
if (_instance != null)
return _instance;
_instance = new PluginUpdateHandler(ctx);
return _instance;
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;
}
private PluginUpdateHandler(RouterContext ctx) {
super(ctx);
_appStatus = "";
}
public void update(String xpi2pURL) {
// don't block waiting for the other one to finish
if ("true".equals(System.getProperty(PROP_UPDATE_IN_PROGRESS))) {
_log.error("Update already running");
return;
}
synchronized (UpdateHandler.class) {
if (_pluginUpdateRunner == null)
_pluginUpdateRunner = new PluginUpdateRunner(_xpi2pURL);
if (_pluginUpdateRunner.isRunning())
return;
_xpi2pURL = xpi2pURL;
_updateFile = (new File(_context.getTempDir(), "tmp" + _context.random().nextInt() + XPI2P)).getAbsolutePath();
System.setProperty(PROP_UPDATE_IN_PROGRESS, "true");
I2PAppThread update = new I2PAppThread(_pluginUpdateRunner, "AppDownload");
update.start();
}
}
public String getAppStatus() {
return _appStatus;
}
public boolean isRunning() {
return _pluginUpdateRunner != null && _pluginUpdateRunner.isRunning();
@Override
public UpdateType getType() {
return _oldVersion.equals("") ? UpdateType.PLUGIN_INSTALL : UpdateType.PLUGIN;
}
@Override
public boolean isDone() {
// FIXME
return false;
}
/** @since 0.8.13 */
public boolean wasUpdateSuccessful() {
return _updated;
}
private void scheduleStatusClean(String msg) {
SimpleScheduler.getInstance().addEvent(new Cleaner(msg), 20*60*1000);
}
private class Cleaner implements SimpleTimer.TimedEvent {
private final String _msg;
public Cleaner(String msg) {
_msg = msg;
}
public void timeReached() {
if (_msg.equals(getStatus()))
updateStatus("");
}
}
public class PluginUpdateRunner extends UpdateRunner implements Runnable, EepGet.StatusListener {
public PluginUpdateRunner(String url) {
super();
}
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>");
@ -156,21 +112,6 @@ public class PluginUpdateHandler extends UpdateHandler {
}
}
@Override
public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {
StringBuilder buf = new StringBuilder(64);
buf.append("<b>").append(_("Downloading plugin")).append(' ');
double pct = ((double)alreadyTransferred + (double)currentWrite) /
((double)alreadyTransferred + (double)currentWrite + bytesRemaining);
synchronized (_pct) {
buf.append(_pct.format(pct));
}
buf.append(": ");
buf.append(_("{0}B transferred", DataHelper.formatSize2(currentWrite + alreadyTransferred)));
buf.append("</b>");
updateStatus(buf.toString());
}
@Override
public void transferComplete(long alreadyTransferred, long bytesTransferred, long bytesRemaining, String url, String outputFile, boolean notModified) {
boolean update = false;
@ -468,15 +409,7 @@ public class PluginUpdateHandler extends UpdateHandler {
private void statusDone(String msg) {
updateStatus(msg);
scheduleStatusClean(msg);
}
}
@Override
protected void updateStatus(String s) {
super.updateStatus(s);
_appStatus = s;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,7 @@
<html>
<body>
<p>
Classes to implement the update process.
</p>
</body>
</html>

View File

@ -56,7 +56,7 @@ public class CSSHelper extends HelperBase {
public void setNews(String val) {
// Protected with nonce in css.jsi
if (val != null)
NewsFetcher.getInstance(_context).showNews(val.equals("1"));
NewsHelper.showNews(_context, val.equals("1"));
}
/**

View File

@ -1,6 +1,8 @@
package net.i2p.router.web;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@ -12,6 +14,8 @@ import java.util.Set;
import net.i2p.router.client.ClientManagerFacadeImpl;
import net.i2p.router.startup.ClientAppConfig;
import net.i2p.router.startup.LoadClientAppsJob;
import net.i2p.router.update.ConsoleUpdateManager;
import static net.i2p.update.UpdateType.*;
import org.mortbay.jetty.handler.ContextHandlerCollection;
@ -333,7 +337,7 @@ public class ConfigClientsHandler extends FormHandler {
/** @since 0.8.13 */
private void updateAllPlugins() {
if ("true".equals(System.getProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS))) {
if (NewsHelper.isAnyUpdateInProgress()) {
addFormError(_("Plugin or update download already in progress."));
return;
}
@ -346,17 +350,26 @@ public class ConfigClientsHandler extends FormHandler {
}
private void installPlugin(String url) {
if ("true".equals(System.getProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS))) {
ConsoleUpdateManager mgr = (ConsoleUpdateManager) _context.updateManager();
if (mgr == null) {
addFormError("Update manager not registered, cannot install");
return;
}
if (mgr.isUpdateInProgress()) {
addFormError(_("Plugin or update download already in progress."));
return;
}
PluginUpdateHandler puh = PluginUpdateHandler.getInstance(_context);
if (puh.isRunning()) {
addFormError(_("Plugin or update download already in progress."));
URI uri;
try {
uri = new URI(url);
} catch (URISyntaxException use) {
addFormError(_("Bad URL {0}", url));
return;
}
puh.update(url);
addFormNotice(_("Downloading plugin from {0}", url));
if (mgr.installPlugin(uri))
addFormNotice(_("Downloading plugin from {0}", url));
else
addFormError("Cannot install, check logs");
// So that update() will post a status to the summary bar before we reload
try {
Thread.sleep(1000);
@ -364,16 +377,12 @@ public class ConfigClientsHandler extends FormHandler {
}
private void checkPlugin(String app) {
if ("true".equals(System.getProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS))) {
addFormError(_("Plugin or update download already in progress."));
ConsoleUpdateManager mgr = (ConsoleUpdateManager) _context.updateManager();
if (mgr == null) {
addFormError("Update manager not registered, cannot check");
return;
}
PluginUpdateChecker puc = PluginUpdateChecker.getInstance(_context);
if (puc.isRunning()) {
addFormError(_("Plugin or update download already in progress."));
return;
}
puc.update(app);
mgr.check(PLUGIN, app);
addFormNotice(_("Checking plugin {0} for updates", app));
// So that update() will post a status to the summary bar before we reload
try {

View File

@ -293,7 +293,7 @@ public class ConfigClientsHelper extends HelperBase {
* Like in DataHelper but doesn't convert null to ""
* There's a lot worse things a plugin could do but...
*/
static String stripHTML(Properties props, String key) {
public static String stripHTML(Properties props, String key) {
String orig = props.getProperty(key);
if (orig == null) return null;
String t1 = orig.replace('<', ' ');

View File

@ -6,6 +6,8 @@ import java.util.Map;
import net.i2p.I2PAppContext;
import net.i2p.crypto.TrustedUpdate;
import net.i2p.data.DataHelper;
import net.i2p.router.update.ConsoleUpdateManager;
import static net.i2p.update.UpdateType.*;
import net.i2p.util.FileUtil;
import net.i2p.util.PortMapper;
@ -84,7 +86,7 @@ public class ConfigUpdateHandler extends FormHandler {
* @return the configured value, else the registered HTTP proxy, else the default
* @since 0.8.13
*/
static int proxyPort(I2PAppContext ctx) {
public static int proxyPort(I2PAppContext ctx) {
return ctx.getProperty(PROP_PROXY_PORT,
ctx.portMapper().getPort(PortMapper.SVC_HTTP_PROXY, DEFAULT_PROXY_PORT_INT));
}
@ -94,11 +96,16 @@ public class ConfigUpdateHandler extends FormHandler {
if (_action == null)
return;
if (_action.equals(_("Check for updates"))) {
NewsFetcher fetcher = NewsFetcher.getInstance(_context);
fetcher.fetchNews();
if (fetcher.shouldFetchUnsigned())
fetcher.fetchUnsignedHead();
if (fetcher.updateAvailable() || fetcher.unsignedUpdateAvailable()) {
ConsoleUpdateManager mgr = (ConsoleUpdateManager) _context.updateManager();
if (mgr == null) {
addFormError("Update manager not registered, cannot check");
return;
}
boolean a1 = mgr.checkAvailable(NEWS, 60*1000) != null;
boolean a2 = false;
if ((!a1) && _updateUnsigned && _zipURL != null && _zipURL.length() > 0)
a2 = mgr.checkAvailable(ROUTER_UNSIGNED, 60*1000) != null;
if (a1 || a2) {
if ( (_updatePolicy == null) || (!_updatePolicy.equals("notify")) )
addFormNotice(_("Update available, attempting to download now"));
else
@ -118,7 +125,8 @@ public class ConfigUpdateHandler extends FormHandler {
String oldURL = ConfigUpdateHelper.getNewsURL(_context);
if ( (oldURL == null) || (!_newsURL.equals(oldURL)) ) {
changes.put(PROP_NEWS_URL, _newsURL);
NewsFetcher.getInstance(_context).invalidateNews();
// this invalidates the news
changes.put(NewsHelper.PROP_LAST_CHECKED, "0");
addFormNotice(_("Updating news URL to {0}", _newsURL));
}
}

View File

@ -14,7 +14,7 @@ public class ConfigUpdateHelper extends HelperBase {
@Override
public void setContextId(String contextId) {
super.setContextId(contextId);
_dontInstall = NewsFetcher.getInstance(_context).dontInstall();
_dontInstall = NewsHelper.dontInstall(_context);
}
public boolean canInstall() {
@ -160,6 +160,6 @@ public class ConfigUpdateHelper extends HelperBase {
}
public String getNewsStatus() {
return NewsFetcher.getInstance(_context).status();
return NewsHelper.status(_context);
}
}

View File

@ -60,7 +60,7 @@ public class FileDumpHelper extends HelperBase {
dumpDir(buf, dir, ".war");
// plugins
File pluginDir = new File(_context.getConfigDir(), PluginUpdateHandler.PLUGIN_DIR);
File pluginDir = new File(_context.getConfigDir(), PluginStarter.PLUGIN_DIR);
File[] files = pluginDir.listFiles();
if (files != null) {
Arrays.sort(files);

View File

@ -20,7 +20,7 @@ public class LogsHelper extends HelperBase {
}
/** @since 0.8.13 */
static String jettyVersion() {
public static String jettyVersion() {
return Server.getVersion();
}

View File

@ -1,416 +0,0 @@
package net.i2p.router.web;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
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.EepHead;
import net.i2p.util.FileUtil;
import net.i2p.util.Log;
/**
* Task to periodically look for updates to the news.xml, and to keep
* track of whether that has an announcement for a new version.
*/
public class NewsFetcher implements Runnable, EepGet.StatusListener {
private final RouterContext _context;
private final Log _log;
private boolean _updateAvailable;
private boolean _unsignedUpdateAvailable;
private long _lastFetch;
private long _lastUpdated;
private String _updateVersion;
private String _unsignedUpdateVersion;
private String _lastModified;
private boolean _invalidated;
private final File _newsFile;
private final File _tempFile;
private static NewsFetcher _instance;
private volatile boolean _isRunning;
//public static final synchronized NewsFetcher getInstance() { return _instance; }
public static final synchronized NewsFetcher getInstance(RouterContext ctx) {
if (_instance != null)
return _instance;
_instance = new NewsFetcher(ctx);
return _instance;
}
private static final String NEWS_FILE = "docs/news.xml";
private static final String TEMP_NEWS_FILE = "news.xml.temp";
/** @since 0.7.14 not configurable */
private static final String BACKUP_NEWS_URL = "http://www.i2p2.i2p/_static/news/news.xml";
private static final String PROP_LAST_CHECKED = "router.newsLastChecked";
/** @since 0.8.12 */
private static final String PROP_LAST_HIDDEN = "routerconsole.newsLastHidden";
private NewsFetcher(RouterContext ctx) {
_context = ctx;
_log = ctx.logManager().getLog(NewsFetcher.class);
_instance = this;
try {
String last = ctx.getProperty(PROP_LAST_CHECKED);
if (last != null)
_lastFetch = Long.parseLong(last);
} catch (NumberFormatException nfe) {}
_newsFile = new File(_context.getRouterDir(), NEWS_FILE);
_tempFile = new File(_context.getTempDir(), TEMP_NEWS_FILE);
updateLastFetched();
_updateVersion = "";
_isRunning = true;
}
/** @since 0.8.8 */
void shutdown() {
_isRunning = false;
}
private void updateLastFetched() {
if (_newsFile.exists()) {
if (_lastUpdated == 0)
_lastUpdated = _newsFile.lastModified();
if (_lastFetch == 0)
_lastFetch = _lastUpdated;
if (_lastModified == null)
_lastModified = RFC822Date.to822Date(_lastFetch);
} else {
_lastUpdated = 0;
_lastFetch = 0;
_lastModified = null;
}
}
public boolean updateAvailable() { return _updateAvailable; }
public String updateVersion() { return _updateVersion; }
public boolean unsignedUpdateAvailable() { return _unsignedUpdateAvailable; }
public String unsignedUpdateVersion() { return _unsignedUpdateVersion; }
/**
* Is the news newer than the last time it was hidden?
* @since 0.8.12
*/
public boolean shouldShowNews() {
if (_lastUpdated <= 0)
return true;
String h = _context.getProperty(PROP_LAST_HIDDEN);
if (h == null)
return true;
long last = 0;
try {
last = Long.parseLong(h);
} catch (NumberFormatException nfe) {}
return _lastUpdated > last;
}
/**
* Save config with the timestamp of the current news to hide, or 0 to show
* @since 0.8.12
*/
public void showNews(boolean yes) {
long stamp = yes ? 0 : _lastUpdated;
_context.router().saveConfig(PROP_LAST_HIDDEN, Long.toString(stamp));
}
/**
* @return HTML
*/
public String status() {
StringBuilder buf = new StringBuilder(128);
long now = _context.clock().now();
buf.append("<i>");
if (_lastUpdated > 0) {
buf.append(Messages.getString("News last updated {0} ago.",
DataHelper.formatDuration2(now - _lastUpdated),
_context))
.append('\n');
}
if (_lastFetch > _lastUpdated) {
buf.append(Messages.getString("News last checked {0} ago.",
DataHelper.formatDuration2(now - _lastFetch),
_context));
}
buf.append("</i>");
String consoleNonce = System.getProperty("router.consoleNonce");
if (_lastUpdated > 0 && consoleNonce != null) {
if (shouldShowNews()) {
buf.append(" <a href=\"/?news=0&amp;consoleNonce=").append(consoleNonce).append("\">")
.append(Messages.getString("Hide news", _context));
} else {
buf.append(" <a href=\"/?news=1&amp;consoleNonce=").append(consoleNonce).append("\">")
.append(Messages.getString("Show news", _context));
}
buf.append("</a>");
}
return buf.toString();
}
private static final long INITIAL_DELAY = 5*60*1000;
private static final long RUN_DELAY = 10*60*1000;
public void run() {
try { Thread.sleep(INITIAL_DELAY + _context.random().nextLong(INITIAL_DELAY)); } catch (InterruptedException ie) {}
while (_isRunning) {
if (!_updateAvailable) checkForUpdates();
if (shouldFetchNews()) {
fetchNews();
if (shouldFetchUnsigned())
fetchUnsignedHead();
}
try { Thread.sleep(RUN_DELAY); } catch (InterruptedException ie) {}
}
}
boolean dontInstall() {
File test = new File(_context.getBaseDir(), "history.txt");
boolean readonly = ((test.exists() && !test.canWrite()) || (!_context.getBaseDir().canWrite()));
boolean disabled = Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_UPDATE_DISABLED)).booleanValue();
return readonly || disabled;
}
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();
}
private boolean shouldFetchNews() {
if (_invalidated)
return true;
updateLastFetched();
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;
}
}
/**
* Call this when changing news URLs to force an update next time the timer fires.
* @since 0.8.7
*/
void invalidateNews() {
_lastModified = null;
_invalidated = true;
}
public void fetchNews() {
String newsURL = ConfigUpdateHelper.getNewsURL(_context);
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 (_tempFile.exists())
_tempFile.delete();
try {
EepGet get = null;
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()) {
_lastModified = get.getLastModified();
_invalidated = false;
} else {
// backup news location - always proxied
_tempFile.delete();
get = new EepGet(_context, true, proxyHost, proxyPort, 0, _tempFile.getAbsolutePath(), BACKUP_NEWS_URL, true, null, _lastModified);
get.addStatusListener(this);
if (get.fetch())
_lastModified = get.getLastModified();
}
} catch (Throwable t) {
_log.error("Error fetching the news", t);
}
}
public boolean shouldFetchUnsigned() {
String url = _context.getProperty(ConfigUpdateHandler.PROP_ZIP_URL);
return url != null && url.length() > 0 &&
Boolean.valueOf(_context.getProperty(ConfigUpdateHandler.PROP_UPDATE_UNSIGNED)).booleanValue() &&
!dontInstall();
}
/**
* 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.
*/
public void fetchUnsignedHead() {
String url = _context.getProperty(ConfigUpdateHandler.PROP_ZIP_URL);
if (url == null || url.length() <= 0)
return;
// 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;
String lastUpdate = _context.getProperty(UpdateHandler.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(UpdateHandler.PROP_LAST_UPDATE_TIME,
Long.toString(_context.clock().now()));
return;
}
long ms = 0;
try {
ms = Long.parseLong(lastUpdate);
} catch (NumberFormatException nfe) {}
if (ms <= 0) return;
if (modtime > ms) {
_unsignedUpdateAvailable = true;
// '07-Jul 21:09 UTC' with month name in the system locale
_unsignedUpdateVersion = (new SimpleDateFormat("dd-MMM HH:mm")).format(new Date(modtime)) + " UTC";
if (shouldInstall())
fetchUnsigned();
}
}
}
} catch (Throwable t) {
_log.error("Error fetching the unsigned update", t);
}
}
public void fetchUnsigned() {
String url = _context.getProperty(ConfigUpdateHandler.PROP_ZIP_URL);
if (url == null || url.length() <= 0)
return;
UpdateHandler handler = new UnsignedUpdateHandler(_context, url,
_unsignedUpdateVersion);
handler.update();
}
private static final String VERSION_STRING = "version=\"" + RouterVersion.VERSION + "\"";
private static final String VERSION_PREFIX = "version=\"";
private void checkForUpdates() {
_updateAvailable = false;
if ( (!_newsFile.exists()) || (_newsFile.length() <= 0) ) return;
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 == -1) {
// skip
} else {
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!");
_updateVersion = ver;
break;
} 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("Our version found, no need to update: " + buf.toString());
return;
} else {
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) {}
}
// could not find version="0.5.0.1", so there must be an update ;)
if (_log.shouldLog(Log.DEBUG))
_log.debug("Our version was NOT found (" + RouterVersion.VERSION + "), update needed");
_updateAvailable = !dontInstall();
if (shouldInstall()) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Policy requests update, so we update");
UpdateHandler handler = new UpdateHandler(_context);
handler.update();
} else {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Policy requests manual update, so we do nothing");
}
}
public void attemptFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt, int numRetries, Exception cause) {
// ignore
}
public void bytesTransferred(long alreadyTransferred, int currentWrite, long bytesTransferred, long bytesRemaining, String url) {
// ignore
}
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);
if (copied) {
_lastUpdated = now;
_tempFile.delete();
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");
}
_lastFetch = now;
_context.router().saveConfig(PROP_LAST_CHECKED, Long.toString(now));
}
public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
if (_log.shouldLog(Log.WARN))
_log.warn("Failed to fetch the news from " + url);
_tempFile.delete();
}
public void headerReceived(String url, int attemptNum, String key, String val) {}
public void attempting(String url) {}
}

View File

@ -2,6 +2,11 @@ package net.i2p.router.web;
import java.io.File;
import net.i2p.data.DataHelper;
import net.i2p.router.RouterContext;
import net.i2p.router.update.ConsoleUpdateManager;
import static net.i2p.update.UpdateType.*;
/**
* If news file does not exist, use file from the initialNews directory
* in $I2P
@ -10,6 +15,86 @@ import java.io.File;
*/
public class NewsHelper extends ContentHelper {
public static final String PROP_LAST_UPDATE_TIME = "router.updateLastDownloaded";
/** @since 0.8.12 */
private static final String PROP_LAST_HIDDEN = "routerconsole.newsLastHidden";
/** @since 0.9.2 */
public static final String PROP_LAST_CHECKED = "routerconsole.newsLastChecked";
/** @since 0.9.2 */
public static final String PROP_LAST_UPDATED = "routerconsole.newsLastUpdated";
public static final String NEWS_FILE = "docs/news.xml";
/**
* If ANY update is in progress.
* @since 0.9.2 was stored in system properties
*/
public static boolean isAnyUpdateInProgress() {
ConsoleUpdateManager mgr = ConsoleUpdateManager.getInstance();
if (mgr == null) return false;
return mgr.isUpdateInProgress();
}
/**
* If a signed or unsigned router update is in progress.
* Does NOT cover plugins, news, etc.
* @since 0.9.2 was stored in system properties
*/
public static boolean isUpdateInProgress() {
ConsoleUpdateManager mgr = ConsoleUpdateManager.getInstance();
if (mgr == null) return false;
return mgr.isUpdateInProgress(ROUTER_SIGNED) ||
mgr.isUpdateInProgress(ROUTER_UNSIGNED) ||
mgr.isUpdateInProgress(TYPE_DUMMY);
}
/**
* @since 0.9.2 moved from NewsFetcher
*/
public static boolean isUpdateAvailable() {
ConsoleUpdateManager mgr = ConsoleUpdateManager.getInstance();
if (mgr == null) return false;
return mgr.getUpdateAvailable(ROUTER_SIGNED) != null;
}
/**
* @return null if none
* @since 0.9.2 moved from NewsFetcher
*/
public static String updateVersion() {
ConsoleUpdateManager mgr = ConsoleUpdateManager.getInstance();
if (mgr == null) return null;
return mgr.getUpdateAvailable(ROUTER_SIGNED);
}
/**
* @since 0.9.2 moved from NewsFetcher
*/
public static boolean isUnsignedUpdateAvailable() {
ConsoleUpdateManager mgr = ConsoleUpdateManager.getInstance();
if (mgr == null) return false;
return mgr.getUpdateAvailable(ROUTER_UNSIGNED) != null;
}
/**
* @return null if none
* @since 0.9.2 moved from NewsFetcher
*/
public static String unsignedUpdateVersion() {
ConsoleUpdateManager mgr = ConsoleUpdateManager.getInstance();
if (mgr == null) return null;
return mgr.getUpdateAvailable(ROUTER_UNSIGNED);
}
/**
* @return "" if none
* @since 0.9.2 moved from UpdateHelper
*/
public static String getUpdateStatus() {
ConsoleUpdateManager mgr = ConsoleUpdateManager.getInstance();
if (mgr == null) return "";
return mgr.getStatus();
}
@Override
public String getContent() {
File news = new File(_page);
@ -18,8 +103,131 @@ public class NewsHelper extends ContentHelper {
return super.getContent();
}
/** @since 0.8.12 */
/**
* Is the news newer than the last time it was hidden?
* @since 0.8.12
*/
public boolean shouldShowNews() {
return NewsFetcher.getInstance(_context).shouldShowNews();
return shouldShowNews(_context);
}
/**
* @since 0.9.2
*/
public static boolean shouldShowNews(RouterContext ctx) {
long lastUpdated = lastUpdated(ctx);
if (lastUpdated <= 0)
return true;
String h = ctx.getProperty(PROP_LAST_HIDDEN);
if (h == null)
return true;
long last = 0;
try {
last = Long.parseLong(h);
} catch (NumberFormatException nfe) {}
return lastUpdated > last;
}
/**
* Save config with the timestamp of the current news to hide, or 0 to show
* @since 0.8.12
*/
public void showNews(boolean yes) {
showNews(_context, yes);
}
/**
* Save config with the timestamp of the current news to hide, or 0 to show
* @since 0.9.2
*/
public static void showNews(RouterContext ctx, boolean yes) {
long lastUpdated = 0;
/////// FIME from props, or from last mod time?
long stamp = yes ? 0 : lastUpdated;
ctx.router().saveConfig(PROP_LAST_HIDDEN, Long.toString(stamp));
}
/**
* @return HTML
* @since 0.9.2 moved from NewsFetcher
*/
public String status() {
return status(_context);
}
/**
* @return HTML
* @since 0.9.2 moved from NewsFetcher
*/
public static String status(RouterContext ctx) {
StringBuilder buf = new StringBuilder(128);
long now = ctx.clock().now();
buf.append("<i>");
long lastUpdated = lastUpdated(ctx);
long lastFetch = lastChecked(ctx);
if (lastUpdated > 0) {
buf.append(Messages.getString("News last updated {0} ago.",
DataHelper.formatDuration2(now - lastUpdated),
ctx))
.append('\n');
}
if (lastFetch > lastUpdated) {
buf.append(Messages.getString("News last checked {0} ago.",
DataHelper.formatDuration2(now - lastFetch),
ctx));
}
buf.append("</i>");
String consoleNonce = System.getProperty("router.consoleNonce");
if (lastUpdated > 0 && consoleNonce != null) {
if (shouldShowNews(ctx)) {
buf.append(" <a href=\"/?news=0&amp;consoleNonce=").append(consoleNonce).append("\">")
.append(Messages.getString("Hide news", ctx));
} else {
buf.append(" <a href=\"/?news=1&amp;consoleNonce=").append(consoleNonce).append("\">")
.append(Messages.getString("Show news", ctx));
}
buf.append("</a>");
}
return buf.toString();
}
/**
* @since 0.9.2 moved from NewsFetcher
*/
public static boolean dontInstall(RouterContext ctx) {
File test = new File(ctx.getBaseDir(), "history.txt");
boolean readonly = ((test.exists() && !test.canWrite()) || (!ctx.getBaseDir().canWrite()));
boolean disabled = ctx.getBooleanProperty(ConfigUpdateHandler.PROP_UPDATE_DISABLED);
return readonly || disabled;
}
/**
* @since 0.9.2
*/
public static long lastChecked(RouterContext ctx) {
String lc = ctx.getProperty(PROP_LAST_CHECKED);
if (lc == null) {
try {
return Long.parseLong(lc);
} catch (NumberFormatException nfe) {}
}
return 0;
}
/**
* When the news was last downloaded
* @since 0.9.2
*/
public static long lastUpdated(RouterContext ctx) {
String lc = ctx.getProperty(PROP_LAST_UPDATED);
if (lc == null) {
try {
return Long.parseLong(lc);
} catch (NumberFormatException nfe) {}
}
File newsFile = new File(ctx.getRouterDir(), NEWS_FILE);
long rv = newsFile.lastModified();
ctx.router().saveConfig(PROP_LAST_UPDATED, Long.toString(rv));
return rv;
}
}

View File

@ -25,6 +25,8 @@ import net.i2p.router.RouterContext;
import net.i2p.router.RouterVersion;
import net.i2p.router.startup.ClientAppConfig;
import net.i2p.router.startup.LoadClientAppsJob;
import net.i2p.router.update.ConsoleUpdateManager;
import static net.i2p.update.UpdateType.*;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.FileUtil;
import net.i2p.util.I2PAppThread;
@ -45,8 +47,9 @@ import org.mortbay.jetty.handler.ContextHandlerCollection;
*/
public class PluginStarter implements Runnable {
protected RouterContext _context;
static final String PREFIX = "plugin.";
static final String ENABLED = ".startOnLoad";
public static final String PREFIX = "plugin.";
public static final String ENABLED = ".startOnLoad";
public static final String PLUGIN_DIR = "plugins";
private static final String[] STANDARD_WEBAPPS = { "i2psnark", "i2ptunnel", "susidns",
"susimail", "addressbook", "routerconsole" };
private static final String[] STANDARD_THEMES = { "images", "light", "dark", "classic",
@ -66,7 +69,7 @@ public class PluginStarter implements Runnable {
public void run() {
if (_context.getBooleanPropertyDefaultTrue("plugins.autoUpdate") &&
(!Boolean.valueOf(System.getProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS)).booleanValue()) &&
(!NewsHelper.isUpdateInProgress()) &&
(!RouterVersion.VERSION.equals(_context.getProperty("router.previousVersion"))))
updateAll(_context, true);
startPlugins(_context);
@ -112,18 +115,24 @@ public class PluginStarter implements Runnable {
}
if (toUpdate.isEmpty())
return;
PluginUpdateChecker puc = PluginUpdateChecker.getInstance(ctx);
if (puc.isRunning())
ConsoleUpdateManager mgr = (ConsoleUpdateManager) ctx.updateManager();
if (mgr == null)
return;
if (mgr.isUpdateInProgress())
return;
if (delay) {
// wait for proxy
System.setProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS, "true");
puc.setAppStatus(Messages.getString("Checking for plugin updates", ctx));
try {
Thread.sleep(3*60*1000);
} catch (InterruptedException ie) {}
System.setProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS, "false");
mgr.update(TYPE_DUMMY, 3*60*1000);
mgr.notifyProgress(null, Messages.getString("Checking for plugin updates", ctx));
int loop = 0;
do {
try {
Thread.sleep(5*1000);
} catch (InterruptedException ie) {}
if (loop++ > 40) break;
} while (mgr.isUpdateInProgress(TYPE_DUMMY));
}
Log log = ctx.logManager().getLog(PluginStarter.class);
@ -132,34 +141,32 @@ public class PluginStarter implements Runnable {
String appName = entry.getKey();
if (log.shouldLog(Log.WARN))
log.warn("Checking for update plugin: " + appName);
puc.update(appName);
do {
try {
Thread.sleep(5*1000);
} catch (InterruptedException ie) {}
} while (puc.isRunning());
if (!puc.isNewerAvailable()) {
// blocking
if (mgr.checkAvailable(PLUGIN, appName, 60*1000) == null) {
if (log.shouldLog(Log.WARN))
log.warn("No update available for plugin: " + appName);
continue;
}
PluginUpdateHandler puh = PluginUpdateHandler.getInstance(ctx);
String url = entry.getValue();
if (log.shouldLog(Log.WARN))
log.warn("Updating plugin: " + appName);
puh.update(url);
mgr.update(PLUGIN, appName, 30*60*1000);
int loop = 0;
do {
try {
Thread.sleep(5*1000);
} catch (InterruptedException ie) {}
} while (puh.isRunning());
if (puh.wasUpdateSuccessful())
if (loop++ > 40) break;
} while (mgr.isUpdateInProgress(PLUGIN, appName));
if (mgr.getUpdateAvailable(PLUGIN, appName) != null)
updated++;
}
if (updated > 0)
puc.setDoneStatus(ngettext("1 plugin updated", "{0} plugins updated", updated, ctx));
mgr.notifyComplete(null, ngettext("1 plugin updated", "{0} plugins updated", updated, ctx));
else
puc.setDoneStatus(Messages.getString("Plugin update check complete", ctx));
mgr.notifyComplete(null, Messages.getString("Plugin update check complete", ctx));
}
/** this shouldn't throw anything */
@ -189,9 +196,9 @@ public class PluginStarter implements Runnable {
* @return true on success
* @throws just about anything, caller would be wise to catch Throwable
*/
static boolean startPlugin(RouterContext ctx, String appName) throws Exception {
public static boolean startPlugin(RouterContext ctx, String appName) throws Exception {
Log log = ctx.logManager().getLog(PluginStarter.class);
File pluginDir = new File(ctx.getConfigDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName);
File pluginDir = new File(ctx.getConfigDir(), PLUGIN_DIR + '/' + appName);
if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) {
log.error("Cannot start nonexistent plugin: " + appName);
disablePlugin(appName);
@ -199,7 +206,7 @@ public class PluginStarter implements Runnable {
}
// Do we need to extract an update?
File pluginUpdate = new File(ctx.getConfigDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName + "/app.xpi2p.zip" );
File pluginUpdate = new File(ctx.getConfigDir(), PLUGIN_DIR + '/' + appName + "/app.xpi2p.zip" );
if(pluginUpdate.exists()) {
// Compare the start time of the router with the plugin.
if(ctx.router().getWhenStarted() > pluginUpdate.lastModified()) {
@ -363,9 +370,9 @@ public class PluginStarter implements Runnable {
* @return true on success
* @throws just about anything, caller would be wise to catch Throwable
*/
static boolean stopPlugin(RouterContext ctx, String appName) throws Exception {
public static boolean stopPlugin(RouterContext ctx, String appName) throws Exception {
Log log = ctx.logManager().getLog(PluginStarter.class);
File pluginDir = new File(ctx.getConfigDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName);
File pluginDir = new File(ctx.getConfigDir(), PLUGIN_DIR + '/' + appName);
if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) {
log.error("Cannot stop nonexistent plugin: " + appName);
return false;
@ -424,7 +431,7 @@ public class PluginStarter implements Runnable {
/** @return true on success - caller should call stopPlugin() first */
static boolean deletePlugin(RouterContext ctx, String appName) throws Exception {
Log log = ctx.logManager().getLog(PluginStarter.class);
File pluginDir = new File(ctx.getConfigDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName);
File pluginDir = new File(ctx.getConfigDir(), PLUGIN_DIR + '/' + appName);
if ((!pluginDir.exists()) || (!pluginDir.isDirectory())) {
log.error("Cannot delete nonexistent plugin: " + appName);
return false;
@ -469,7 +476,7 @@ public class PluginStarter implements Runnable {
/** plugin.config */
public static Properties pluginProperties(I2PAppContext ctx, String appName) {
File cfgFile = new File(ctx.getConfigDir(), PluginUpdateHandler.PLUGIN_DIR + '/' + appName + '/' + "plugin.config");
File cfgFile = new File(ctx.getConfigDir(), PLUGIN_DIR + '/' + appName + '/' + "plugin.config");
Properties rv = new Properties();
try {
DataHelper.loadProps(rv, cfgFile);
@ -530,7 +537,7 @@ public class PluginStarter implements Runnable {
*/
public static List<String> getPlugins() {
List<String> rv = new ArrayList();
File pluginDir = new File(I2PAppContext.getGlobalContext().getConfigDir(), PluginUpdateHandler.PLUGIN_DIR);
File pluginDir = new File(I2PAppContext.getGlobalContext().getConfigDir(), PLUGIN_DIR);
File[] files = pluginDir.listFiles();
if (files == null)
return rv;

View File

@ -1,200 +0,0 @@
package net.i2p.router.web;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.util.List;
import java.util.Properties;
import net.i2p.crypto.TrustedUpdate;
import net.i2p.router.RouterContext;
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 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.
*
* @since 0.7.12
* @author zzz
*/
public class PluginUpdateChecker extends UpdateHandler {
private static PluginUpdateCheckerRunner _pluginUpdateCheckerRunner;
private String _appName;
private String _oldVersion;
private String _xpi2pURL;
private volatile boolean _isNewerAvailable;
private static PluginUpdateChecker _instance;
public static final synchronized PluginUpdateChecker getInstance(RouterContext ctx) {
if (_instance != null)
return _instance;
_instance = new PluginUpdateChecker(ctx);
return _instance;
}
private PluginUpdateChecker(RouterContext ctx) {
super(ctx);
}
/**
* check all plugins
* @deprecated not finished
*/
public void update() {
Thread t = new I2PAppThread(new AllCheckerRunner(), "AllAppChecker", true);
t.start();
}
/**
* check all plugins
* @deprecated not finished
*/
public class AllCheckerRunner implements Runnable {
public void run() {
List<String> plugins = PluginStarter.getPlugins();
// TODO
}
}
/** check a single plugin */
public void update(String appName) {
// don't block waiting for the other one to finish
if ("true".equals(System.getProperty(PROP_UPDATE_IN_PROGRESS))) {
_log.error("Update already running");
return;
}
synchronized (UpdateHandler.class) {
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;
}
if (_pluginUpdateCheckerRunner == null)
_pluginUpdateCheckerRunner = new PluginUpdateCheckerRunner();
if (_pluginUpdateCheckerRunner.isRunning())
return;
_xpi2pURL = xpi2pURL;
_appName = appName;
_oldVersion = oldVersion;
_isNewerAvailable = false;
System.setProperty(PROP_UPDATE_IN_PROGRESS, "true");
I2PAppThread update = new I2PAppThread(_pluginUpdateCheckerRunner, "AppChecker", true);
update.start();
}
}
/** @since 0.8.13 */
public void setAppStatus(String status) {
updateStatus(status);
}
/** @since 0.8.13 */
public void setDoneStatus(String status) {
updateStatus(status);
scheduleStatusClean(status);
}
public boolean isRunning() {
return _pluginUpdateCheckerRunner != null && _pluginUpdateCheckerRunner.isRunning();
}
@Override
public boolean isDone() {
// FIXME
return false;
}
/** @since 0.8.13 */
public boolean isNewerAvailable() {
return _isNewerAvailable;
}
private void scheduleStatusClean(String msg) {
SimpleScheduler.getInstance().addEvent(new Cleaner(msg), 20*60*1000);
}
private class Cleaner implements SimpleTimer.TimedEvent {
private final String _msg;
public Cleaner(String msg) {
_msg = msg;
}
public void timeReached() {
if (_msg.equals(getStatus()))
updateStatus("");
}
}
public class PluginUpdateCheckerRunner extends UpdateRunner implements Runnable, EepGet.StatusListener {
ByteArrayOutputStream _baos;
public PluginUpdateCheckerRunner() {
super();
_baos = new ByteArrayOutputStream(TrustedUpdate.HEADER_BYTES);
}
@Override
protected void update() {
_isNewerAvailable = false;
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, _xpi2pURL, TrustedUpdate.HEADER_BYTES);
_get.addStatusListener(PluginUpdateCheckerRunner.this);
_get.fetch();
} catch (Throwable t) {
_log.error("Error checking update for plugin", t);
}
}
public boolean isNewerAvailable() {
return _isNewerAvailable;
}
@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) {
String newVersion = TrustedUpdate.getVersionString(new ByteArrayInputStream(_baos.toByteArray()));
boolean newer = (new VersionComparator()).compare(newVersion, _oldVersion) > 0;
String msg;
if (newer) {
msg = "<b>" + _("New plugin version {0} is available", newVersion) + "</b>";
_isNewerAvailable = true;
} else {
msg = "<b>" + _("No new version is available for plugin {0}", _appName) + "</b>";
}
updateStatus(msg);
scheduleStatusClean(msg);
}
@Override
public void transferFailed(String url, long bytesTransferred, long bytesRemaining, int currentAttempt) {
File f = new File(_updateFile);
f.delete();
String msg = "<b>" + _("Update check failed for plugin {0}", _appName) + "</b>";
updateStatus(msg);
scheduleStatusClean(msg);
}
}
}

View File

@ -27,6 +27,7 @@ import net.i2p.apps.systray.SysTray;
import net.i2p.data.Base32;
import net.i2p.data.DataHelper;
import net.i2p.router.RouterContext;
import net.i2p.router.update.ConsoleUpdateManager;
import net.i2p.util.Addresses;
import net.i2p.util.FileUtil;
import net.i2p.util.I2PAppThread;
@ -538,10 +539,8 @@ public class RouterConsoleRunner {
if (contexts != null) {
RouterContext ctx = contexts.get(0);
NewsFetcher fetcher = NewsFetcher.getInstance(ctx);
Thread newsThread = new I2PAppThread(fetcher, "NewsFetcher", true);
newsThread.setPriority(Thread.NORM_PRIORITY - 1);
newsThread.start();
ConsoleUpdateManager um = new ConsoleUpdateManager(ctx);
um.start();
if (PluginStarter.pluginsEnabled(ctx)) {
t = new I2PAppThread(new PluginStarter(ctx), "PluginStarter", true);
@ -549,7 +548,6 @@ public class RouterConsoleRunner {
t.start();
ctx.addShutdownTask(new PluginStopper(ctx));
}
ctx.addShutdownTask(new NewsShutdown(fetcher, newsThread));
// stat summarizer registers its own hook
ctx.addShutdownTask(new ServerShutdown());
ConfigServiceHandler.registerSignalHandler(ctx);
@ -721,22 +719,6 @@ public class RouterConsoleRunner {
}
}
/** @since 0.8.8 */
private static class NewsShutdown implements Runnable {
private final NewsFetcher _fetcher;
private final Thread _newsThread;
public NewsShutdown(NewsFetcher fetcher, Thread t) {
_fetcher = fetcher;
_newsThread = t;
}
public void run() {
_fetcher.shutdown();
_newsThread.interrupt();
}
}
public static Properties webAppProperties() {
return webAppProperties(I2PAppContext.getGlobalContext().getConfigDir().getAbsolutePath());
}

View File

@ -594,19 +594,19 @@ public class SummaryHelper extends HelperBase {
********/
public boolean updateAvailable() {
return NewsFetcher.getInstance(_context).updateAvailable();
return NewsHelper.isUpdateAvailable();
}
public boolean unsignedUpdateAvailable() {
return NewsFetcher.getInstance(_context).unsignedUpdateAvailable();
return NewsHelper.isUnsignedUpdateAvailable();
}
public String getUpdateVersion() {
return NewsFetcher.getInstance(_context).updateVersion();
return NewsHelper.updateVersion();
}
public String getUnsignedUpdateVersion() {
return NewsFetcher.getInstance(_context).unsignedUpdateVersion();
return NewsHelper.unsignedUpdateVersion();
}
/**
@ -616,12 +616,12 @@ public class SummaryHelper extends HelperBase {
public String getUpdateStatus() {
StringBuilder buf = new StringBuilder(512);
// display all the time so we display the final failure message, and plugin update messages too
String status = UpdateHandler.getStatus();
String status = NewsHelper.getUpdateStatus();
if (status.length() > 0) {
buf.append("<h4>").append(status).append("</h4><hr>\n");
}
if (updateAvailable() || unsignedUpdateAvailable()) {
if ("true".equals(System.getProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS))) {
if (NewsHelper.isUpdateInProgress()) {
// nothing
} else if(
// isDone() is always false for now, see UpdateHandler

View File

@ -1,130 +0,0 @@
package net.i2p.router.web;
import java.io.File;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;
import net.i2p.router.util.RFC822Date;
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>
*/
public class UnsignedUpdateHandler extends UpdateHandler {
private static UnsignedUpdateRunner _unsignedUpdateRunner;
private String _zipURL;
private String _zipVersion;
public UnsignedUpdateHandler(RouterContext ctx, String zipURL, String version) {
super(ctx);
_zipURL = zipURL;
_zipVersion = version;
_updateFile = (new File(ctx.getTempDir(), "tmp" + ctx.random().nextInt() + Router.UPDATE_FILE)).getAbsolutePath();
}
@Override
public void update() {
// don't block waiting for the other one to finish
if ("true".equals(System.getProperty(PROP_UPDATE_IN_PROGRESS))) {
_log.error("Update already running");
return;
}
synchronized (UpdateHandler.class) {
if (_unsignedUpdateRunner == null) {
_unsignedUpdateRunner = new UnsignedUpdateRunner();
}
if (_unsignedUpdateRunner.isRunning()) {
return;
} else {
System.setProperty(PROP_UPDATE_IN_PROGRESS, "true");
I2PAppThread update = new I2PAppThread(_unsignedUpdateRunner, "UnsignedUpdate");
update.start();
}
}
}
/**
* Eepget the .zip file to the temp dir, then copy it over
*/
public class UnsignedUpdateRunner extends UpdateRunner implements Runnable, EepGet.StatusListener {
public UnsignedUpdateRunner() {
super();
}
/** Get the file */
@Override
protected void update() {
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) {
File updFile = new File(_updateFile);
if (FileUtil.verifyZip(updFile)) {
updateStatus("<b>" + _("Update downloaded") + "</b>");
} else {
updFile.delete();
updateStatus("<b>" + _("Unsigned update file from {0} is corrupt", url) + "</b>");
_log.log(Log.CRIT, "Corrupt zip file from " + url);
return;
}
File to = new File(_context.getRouterDir(), Router.UPDATE_FILE);
boolean copied = FileUtil.copy(updFile, to, true, false);
if (copied) {
updFile.delete();
String policy = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_POLICY);
this.done = true;
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 downloaded, restarting to install it");
updateStatus("<b>" + _("Update downloaded") + "</b><br>" + _("Restarting"));
restart();
} else {
_log.log(Log.CRIT, "Update was downloaded, will be installed at next restart");
StringBuilder buf = new StringBuilder(64);
buf.append("<b>").append(_("Update downloaded")).append("</b><br>");
if (_context.hasWrapper())
buf.append(_("Click Restart to install"));
else
buf.append(_("Click Shutdown and restart to install"));
buf.append(' ').append(_("Version {0}", _zipVersion));
updateStatus(buf.toString());
}
} else {
_log.log(Log.CRIT, "Failed copy to " + to);
updateStatus("<b>" + _("Failed copy to {0}", to.getAbsolutePath()) + "</b>");
}
}
}
}

View File

@ -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);
}
}

View File

@ -64,7 +64,7 @@ public class WebAppConfiguration implements Configuration {
File libDir = new File(i2pContext.getBaseDir(), "lib");
// FIXME this only works if war is the same name as the plugin
File pluginDir = new File(i2pContext.getConfigDir(),
PluginUpdateHandler.PLUGIN_DIR + ctxPath);
PluginStarter.PLUGIN_DIR + ctxPath);
File dir = libDir;
String cp;

View File

@ -26,6 +26,7 @@ import net.i2p.data.Base64;
import net.i2p.data.RoutingKeyGenerator;
import net.i2p.internal.InternalClientManager;
import net.i2p.stat.StatManager;
import net.i2p.update.UpdateManager;
import net.i2p.util.Clock;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.FileUtil;
@ -984,4 +985,13 @@ public class I2PAppContext {
_simpleTimer2Initialized = true;
}
}
/**
* The controller of router, plugin, and other updates.
* @return always null in I2PAppContext, the update manager if in RouterContext and it is registered
* @since 0.9.2
*/
public UpdateManager updateManager() {
return null;
}
}

View File

@ -0,0 +1,79 @@
package net.i2p.update;
import java.io.File;
import java.net.URI;
import java.util.List;;
/**
* The central resource coordinating updates.
* This must be registered with the context.
*
* The UpdateManager starts and stops all updates,
* and controls notification to the user.
*
* @since 0.9.2
*/
public interface UpdateManager {
/**
* Call multiple times for each type/method pair.
* The UpdateManager will then call start()
*/
public void register(Updater updater, UpdateType type, UpdateMethod method, int priority);
public void unregister(Updater updater, UpdateType type, UpdateMethod method);
public void start();
public void shutdown();
/**
* Called by the Updater, either after check() was called, or it found out on its own.
*
* @param newsSource who told us
* @param id plugin name for plugins, ignored otherwise
* @param method How to get the new version
* @param updateSourcew Where to get the new version
* @param newVersion The new version available
* @param minVersion The minimum installed version to be able to update to newVersion
* @return true if we didn't know already
*/
public boolean notifyVersionAvailable(UpdateTask task, URI newsSource,
UpdateType type, String id,
UpdateMethod method, List<URI> updateSources,
String newVersion, String minVersion);
/**
* Called by the Updater after check() was called and all notifyVersionAvailable() callbacks are finished
* @param newer notifyVersionAvailable was called
* @param success check succeeded (newer or not)
*/
public void notifyCheckComplete(UpdateTask task, boolean newer, boolean success);
public void notifyProgress(UpdateTask task, String status);
public void notifyProgress(UpdateTask task, String status, long downloaded, long totalSize);
/**
* Not necessarily the end if there are more URIs to try.
* @param t may be null
*/
public void notifyAttemptFailed(UpdateTask task, String reason, Throwable t);
/**
* The task has finished and failed.
* @param t may be null
*/
public void notifyTaskFailed(UpdateTask task, String reason, Throwable t);
/**
* An update has been downloaded but not verified.
* The manager will verify it.
* Caller should delete the file upon return, unless it will share it with others,
* e.g. on a torrent.
*
* @param actualVersion may be higher (or lower?) than the version requested
* @param file a valid format for the task's UpdateType
* @return true if valid, false if corrupt
*/
public boolean notifyComplete(UpdateTask task, String actualVersion, File file);
}

View File

@ -0,0 +1,15 @@
package net.i2p.update;
/**
* Transport mechanism for getting something.
*
* @since 0.9.2
*/
public enum UpdateMethod {
METHOD_DUMMY,
HTTP, // .i2p or via outproxy
HTTP_CLEARNET, // direct non-.i2p
TORRENT,
GNUTELLA, IMULE, TAHOE_LAFS,
DEBIAN
}

View File

@ -0,0 +1,30 @@
package net.i2p.update;
import java.net.URI;
/**
* A running check or download. Cannot be restarted.
*
* @since 0.9.2
*/
public interface UpdateTask {
public void shutdown();
public boolean isRunning();
public UpdateType getType();
public UpdateMethod getMethod();
/**
* The current URI being checked or downloaded from.
* Can change if there are multiple URIs to try.
*/
public URI getURI();
/**
* Valid for plugins
*/
public String getID();
}

View File

@ -0,0 +1,16 @@
package net.i2p.update;
/**
* What to update
*
* @since 0.9.2
*/
public enum UpdateType {
TYPE_DUMMY,
NEWS,
ROUTER_SIGNED,
ROUTER_SIGNED_PACK200, // unused, use ROUTER_SIGNED for both
ROUTER_UNSIGNED,
PLUGIN, PLUGIN_INSTALL,
GEOIP, BLOCKLIST, RESEED
}

View File

@ -0,0 +1,36 @@
package net.i2p.update;
import java.net.URI;
import java.util.List;
/**
* Controls one or more types of updates.
* This must be registered with the UpdateManager.
*
* @since 0.9.2
*/
public interface Updater {
/**
* Check for updates.
* Should not block.
* If any are found, call back to UpdateManager.notifyUpdateAvailable().
*
* @param id plugin name or ignored
* @param maxTime how long you have
* @return active task or null if unable to check
*/
public UpdateTask check(UpdateType type, UpdateMethod method,
String id, String currentVersion, long maxTime);
/**
* 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);
}

View File

@ -0,0 +1,8 @@
<html>
<body>
<p>
Interfaces for classes to assist in the update process without
needing the router context.
</p>
</body>
</html>

View File

@ -22,7 +22,7 @@ public class VersionComparator implements Comparator<String> {
while (lTokens.hasMoreTokens() && rTokens.hasMoreTokens()) {
String lNumber = lTokens.nextToken();
String rNumber = rTokens.nextToken();
int diff = intCompare(lNumber, rNumber);
int diff = longCompare(lNumber, rNumber);
if (diff != 0)
return diff;
}
@ -34,19 +34,24 @@ public class VersionComparator implements Comparator<String> {
return 0;
}
private static final int intCompare(String lop, String rop) {
int left, right;
private static final int longCompare(String lop, String rop) {
long left, right;
try {
left = Integer.parseInt(lop);
left = Long.parseLong(lop);
} catch (NumberFormatException nfe) {
return -1;
}
try {
right = Integer.parseInt(rop);
right = Long.parseLong(rop);
} catch (NumberFormatException nfe) {
return 1;
}
return left - right;
long diff = left - right;
if (diff < 0)
return -1;
if (diff > 0)
return 1;
return 0;
}
private static final String VALID_SEPARATOR_CHARS = ".-_";

View File

@ -22,6 +22,7 @@ import net.i2p.router.transport.FIFOBandwidthLimiter;
import net.i2p.router.transport.OutboundMessageRegistry;
import net.i2p.router.tunnel.TunnelDispatcher;
import net.i2p.router.tunnel.pool.TunnelPoolManager;
import net.i2p.update.UpdateManager;
import net.i2p.util.KeyRing;
import net.i2p.util.I2PProperties.I2PPropertyCallback;
@ -55,11 +56,12 @@ public class RouterContext extends I2PAppContext {
private Shitlist _shitlist;
private Blocklist _blocklist;
private MessageValidator _messageValidator;
private UpdateManager _updateManager;
//private MessageStateMonitor _messageStateMonitor;
private RouterThrottle _throttle;
private final Set<Runnable> _finalShutdownTasks;
// split up big lock on this to avoid deadlocks
private final Object _lock1 = new Object(), _lock2 = new Object();
private final Object _lock1 = new Object(), _lock2 = new Object(), _lock3 = new Object();
private static List<RouterContext> _contexts = new ArrayList(1);
@ -481,6 +483,7 @@ public class RouterContext extends I2PAppContext {
* @return true
* @since 0.7.9
*/
@Override
public boolean isRouterContext() {
return true;
}
@ -490,7 +493,44 @@ public class RouterContext extends I2PAppContext {
* @return the client manager
* @since 0.8.3
*/
@Override
public InternalClientManager internalClientManager() {
return _clientManagerFacade;
}
/**
* The controller of router, plugin, and other updates.
* @return The manager if it is registered, else null
* @since 0.9.2
*/
@Override
public UpdateManager updateManager() {
return _updateManager;
}
/**
* Register as the update manager.
* @throws IllegalStateException if one was already registered
* @since 0.9.2
*/
public void registerUpdateManager(UpdateManager mgr) {
synchronized(_lock3) {
if (_updateManager != null)
throw new IllegalStateException();
_updateManager = mgr;
}
}
/**
* Unregister the update manager.
* @throws IllegalStateException if it was not registered
* @since 0.9.2
*/
public void unregisterUpdateManager(UpdateManager mgr) {
synchronized(_lock3) {
if (_updateManager != mgr)
throw new IllegalStateException();
_updateManager = null;
}
}
}