diff --git a/apps/routerconsole/java/build.xml b/apps/routerconsole/java/build.xml index 185c4976a54c4cb83782240207373fa37b91ccc4..8b97cc0f0f1bb3d453094482d0d354ffa86a6a9a 100644 --- a/apps/routerconsole/java/build.xml +++ b/apps/routerconsole/java/build.xml @@ -330,6 +330,118 @@ splitindex="true" windowtitle="Router Console" /> </target> + + <!-- scala paths --> + <target name="scala.init"> + <property name="scala-library.jar" value="${scalatest.libs}/scala-library.jar" /> + <property name="scalatest.jar" value="${scalatest.libs}/scalatest.jar" /> + <taskdef resource="scala/tools/ant/antlib.xml"> + <classpath> + <pathelement location="${scalatest.libs}/scala-compiler.jar" /> + <pathelement location="${scala-library.jar}" /> + </classpath> + </taskdef> + </target> + + <!-- unit tests --> + <target name="builddepscalatest"> + <ant dir="../../../router/java/" target="jar" /> + <ant dir="../../../router/java/" target="jarScalaTest" /> + </target> + <target name="scalatest.compileTest" depends="builddepscalatest, compile, scala.init"> + <mkdir dir="./build" /> + <mkdir dir="./build/obj_scala" /> + <scalac srcdir="./test/scalatest" destdir="./build/obj_scala" deprecation="on" > + <classpath> + <pathelement location="${classpath}" /> + <pathelement location="${scala-library.jar}" /> + <pathelement location="${scalatest.jar}" /> + <pathelement location="../../../core/java/build/i2pscalatest.jar" /> + <pathelement location="../../../router/java/build/routerscalatest.jar" /> + <pathelement location="./build/obj" /> + </classpath> + </scalac> + </target> + <!-- preparation of code coverage tool of choice --> + <target name="prepareClover" depends="compile" if="with.clover"> + <taskdef resource="clovertasks"/> + <mkdir dir="../../../reports/apps/routerconsole/clover" /> + <clover-setup initString="../../../reports/apps/routerconsole/clover/coverage.db"/> + </target> + <target name="prepareCobertura" depends="compile" if="with.cobertura"> + <taskdef classpath="${with.cobertura}" resource="tasks.properties" onerror="report" /> + <mkdir dir="./build/obj_cobertura" /> + <delete file="./cobertura.ser" /> + <cobertura-instrument todir="./build/obj_cobertura"> + <fileset dir="./build/obj"> + <include name="**/*.class"/> + <exclude name="**/*Test.class" /> + </fileset> + </cobertura-instrument> + </target> + <target name="prepareTest" depends="prepareClover, prepareCobertura" /> + <!-- end preparation of code coverage tool --> + <target name="scalatest.test" depends="clean, scalatest.compileTest, prepareTest"> + <mkdir dir="../../../reports/apps/routerconsole/scalatest/" /> + <delete> + <fileset dir="../../../reports/apps/routerconsole/scalatest"> + <include name="TEST-*.xml"/> + </fileset> + </delete> + <taskdef name="scalatest" classname="org.scalatest.tools.ScalaTestAntTask"> + <classpath> + <pathelement location="${classpath}" /> + <pathelement location="${scala-library.jar}" /> + <pathelement location="${scalatest.jar}" /> + <pathelement location="./build/obj_cobertura" /> + <pathelement location="./build/obj" /> + <pathelement location="${with.clover}" /> + <pathelement location="${with.cobertura}" /> + </classpath> + </taskdef> + <scalatest runpath="./build/obj_scala" fork="yes" maxmemory="384M"> + <tagsToExclude> + SlowTests + </tagsToExclude> + <reporter type="stdout" /> + <reporter type="junitxml" directory="../../../reports/apps/routerconsole/scalatest/" /> + </scalatest> + <!-- fetch the real hostname of this machine --> + <exec executable="hostname" outputproperty="host.name"/> + <!-- set if unset --> + <property name="host.fakename" value="i2ptester" /> + <!-- replace hostname that junit inserts into reports with fake one --> + <replace dir="../../../reports/apps/routerconsole/scalatest/" token="${host.name}" value="${host.fakename}"/> + </target> + <target name="test" depends="scalatest.test"/> + <!-- test reports --> + <target name="scalatest.report"> + <junitreport todir="../../../reports/apps/routerconsole/scalatest"> + <fileset dir="../../../reports/apps/routerconsole/scalatest"> + <include name="TEST-*.xml"/> + </fileset> + <report format="frames" todir="../../../reports/apps/routerconsole/html/scalatest"/> + </junitreport> + </target> + <target name="clover.report" depends="test" if="with.clover"> + <clover-report> + <current outfile="../../../reports/apps/routerconsole/html/clover"> + <format type="html"/> + </current> + </clover-report> + </target> + <target name="cobertura.report" depends="test" if="with.cobertura"> + <mkdir dir="../../../reports/apps/routerconsole/cobertura" /> + <cobertura-report format="xml" srcdir="./src" destdir="../../../reports/apps/routerconsole/cobertura" /> + <mkdir dir="../../../reports/apps/routerconsole/html/cobertura" /> + <cobertura-report format="html" srcdir="./src" destdir="../../../reports/apps/routerconsole/html/cobertura" /> + <delete file="./cobertura.ser" /> + </target> + <target name="test.report" depends="scalatest.report, clover.report, cobertura.report"/> + <!-- end test reports --> + <target name="fulltest" depends="cleandep, test, test.report" /> + <!-- end unit tests --> + <target name="clean"> <delete dir="./build" /> <delete dir="../jsp/WEB-INF/" /> diff --git a/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java b/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java new file mode 100644 index 0000000000000000000000000000000000000000..54e54c6fab65a15f9d7103167480ac7d5b9f1f91 --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/update/ConsoleUpdateManager.java @@ -0,0 +1,1056 @@ +package net.i2p.router.update; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.concurrent.ConcurrentHashMap; + +import net.i2p.I2PAppContext; +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.ConfigServiceHandler; +import net.i2p.router.web.ConfigUpdateHandler; +import net.i2p.router.web.Messages; +import net.i2p.router.web.NewsHelper; +import net.i2p.router.web.PluginStarter; +import net.i2p.update.*; +import static net.i2p.update.UpdateType.*; +import static net.i2p.update.UpdateMethod.*; +import net.i2p.util.ConcurrentHashSet; +import net.i2p.util.FileUtil; +import net.i2p.util.Log; +import net.i2p.util.SimpleScheduler; +import net.i2p.util.SimpleTimer; +import net.i2p.util.VersionComparator; + +/** + * The central resource coordinating updates. + * This must be registered with the context. + * + * The UpdateManager starts and stops all updates, + * prevents multiple updates as appropriate, + * and controls notification to the user. + * + * @since 0.9.2 + */ +public class ConsoleUpdateManager implements UpdateManager { + + private final RouterContext _context; + private final Log _log; + /** registered checkers / updaters */ + private final Collection<RegisteredUpdater> _registered; + /** active checking tasks */ + private final Collection<UpdateTask> _checkers; + /** active updating tasks, pointing to the next ones to try */ + private final Map<UpdateTask, List<RegisteredUpdater>> _downloaders; + /** as reported by checkers */ + private final Map<UpdateItem, VersionAvailable> _available; + /** downloaded but NOT installed */ + private final Map<UpdateItem, Version> _downloaded; + /** downloaded but NOT installed */ + private final Map<UpdateItem, Version> _installed; + private final DecimalFormat _pct = new DecimalFormat("0.0%"); + private static final VersionComparator _versionComparator = new VersionComparator(); + + private volatile String _status; + + public ConsoleUpdateManager(RouterContext ctx) { + _context = ctx; + _log = ctx.logManager().getLog(ConsoleUpdateManager.class); + _registered = new ConcurrentHashSet(); + _checkers = new ConcurrentHashSet(); + _downloaders = new ConcurrentHashMap(); + _available = new ConcurrentHashMap(); + _downloaded = new ConcurrentHashMap(); + _installed = new ConcurrentHashMap(); + _status = ""; + } + + public static ConsoleUpdateManager getInstance() { + return (ConsoleUpdateManager) I2PAppContext.getGlobalContext().updateManager(); + } + + public void start() { + notifyInstalled(NEWS, "", Long.toString(NewsHelper.lastUpdated(_context))); + notifyInstalled(ROUTER_SIGNED, "", RouterVersion.VERSION); + // hack to init from the current news file... do this before we register Updaters + (new NewsFetcher(_context, Collections.EMPTY_LIST)).checkForUpdates(); + for (String plugin : PluginStarter.getPlugins()) { + Properties props = PluginStarter.pluginProperties(_context, plugin); + String ver = props.getProperty("version"); + if (ver != null) + notifyInstalled(PLUGIN, plugin, ver); + } + + _context.registerUpdateManager(this); + Updater u = new DummyHandler(_context); + register(u, TYPE_DUMMY, HTTP, 0); + // register news before router, so we don't fire off an update + // right at instantiation if the news is already indicating a new version + u = new NewsHandler(_context); + register(u, NEWS, HTTP, 0); + register(u, ROUTER_SIGNED, HTTP, 0); // news is an update checker for the router + u = new UpdateHandler(_context); + register(u, ROUTER_SIGNED, HTTP, 0); + u = new UnsignedUpdateHandler(_context); + register(u, ROUTER_UNSIGNED, HTTP, 0); + u = new PluginUpdateHandler(_context); + register(u, PLUGIN, HTTP, 0); + register(u, PLUGIN_INSTALL, HTTP, 0); + new NewsTimerTask(_context); + } + + public void shutdown() { + _context.unregisterUpdateManager(this); + stopChecks(); + stopUpdates(); + _registered.clear(); + _available.clear(); + _downloaded.clear(); + _installed.clear(); + } + + /** + * The status on any update current or last finished. + * @return status or "" + */ + public String getStatus() { + return _status; + } + + public String checkAvailable(UpdateType type, long maxWait) { + return checkAvailable(type, "", maxWait); + } + + /** + * Is an update available? + * Blocking. + * @param maxWait max time to block + * @return new version or null if nothing newer is available + */ + public String checkAvailable(UpdateType type, String id, long maxWait) { +//// update too? + if (isCheckInProgress(type, id) || isUpdateInProgress(type, id)) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Check or update already in progress for: " + type + ' ' + id); + return null; + } + for (RegisteredUpdater r : _registered) { + if (r.type == type) { + UpdateTask t = r.updater.check(type, r.method, id, "FIXME", maxWait); + if (t != null) { + synchronized(t) { + try { + t.wait(maxWait); + } catch (InterruptedException ie) {} + } + return getUpdateAvailable(type, id); + } + } + } + return null; + } + + /** + * Fire off a checker task + * Non-blocking. + */ + public void check(UpdateType type, String id) { + if (isCheckInProgress(type, id)) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Check or update already in progress for: " + type + ' ' + id); + return; + } + for (RegisteredUpdater r : _registered) { + if (r.type == type) { +/// fixme "" will put an entry in _available for everything grrrrr???? + UpdateTask t = r.updater.check(type, r.method, id, "", 5*60*1000); + if (t != null) + break; + } + } + } + + /** + * Is an update available? + * Non-blocking, returns result of last check or notification from an Updater + * @return new version or null if nothing newer is available + */ + public String getUpdateAvailable(UpdateType type) { + return getUpdateAvailable(type, ""); + } + + /** + * Is an update available? + * Non-blocking, returns result of last check or notification from an Updater + * @return new version or null if nothing newer is available + */ + public String getUpdateAvailable(UpdateType type, String id) { + Version v = _available.get(new UpdateItem(type, id)); + if (v == null) + return null; + return v.version; + } + + /** + * Is an update downloaded? + * Non-blocking, returns result of last download + * @return new version or null if nothing was downloaded + */ + public String getUpdateDownloaded(UpdateType type) { + return getUpdateDownloaded(type, ""); + } + + /** + * Is an update downloaded? + * Non-blocking, returns result of last download + * @return new version or null if nothing was downloaded + */ + public String getUpdateDownloaded(UpdateType type, String id) { + Version v = _downloaded.get(new UpdateItem(type, id)); + if (v == null) + return null; + return v.version; + } + + /** + * Is any download in progress? + * Does not include checks. + */ + public boolean isUpdateInProgress() { + return !_downloaders.isEmpty(); + } + + /** + * Is a download in progress? + */ + public boolean isUpdateInProgress(UpdateType type) { + return isUpdateInProgress(type, ""); + } + + /** + * Is a download in progress? + */ + public boolean isUpdateInProgress(UpdateType type, String id) { + for (UpdateTask t : _downloaders.keySet()) { + if (t.getType() == type && id.equals(t.getID())) + return true; + } + return false; + } + + /** + * Stop all downloads in progress + */ + public void stopUpdates() { + for (UpdateTask t : _downloaders.keySet()) { + t.shutdown(); + } + _downloaders.clear(); + } + + /** + * Stop this download + */ + public void stopUpdate(UpdateType type) { + stopUpdate(type, ""); + } + + /** + * Stop this download + */ + public void stopUpdate(UpdateType type, String id) { + for (Iterator<UpdateTask> iter = _downloaders.keySet().iterator(); iter.hasNext(); ) { + UpdateTask t = iter.next(); + if (t.getType() == type && id.equals(t.getID())) { + iter.remove(); + t.shutdown(); + } + } + } + + /** + * Is any check in progress? + * Does not include updates. + */ + public boolean isCheckInProgress() { + return !_checkers.isEmpty(); + } + + /** + * Is a check in progress? + */ + public boolean isCheckInProgress(UpdateType type) { + return isCheckInProgress(type, ""); + } + + /** + * Is a check in progress? + */ + public boolean isCheckInProgress(UpdateType type, String id) { + for (UpdateTask t : _checkers) { + if (t.getType() == type && id.equals(t.getID())) + return true; + } + return false; + } + + /** + * Stop all checks in progress + */ + public void stopChecks() { + for (UpdateTask t : _checkers) { + t.shutdown(); + } + _checkers.clear(); + } + + /** + * Stop this check + */ + public void stopCheck(UpdateType type) { + stopCheck(type, ""); + } + + /** + * Stop this check + */ + public void stopCheck(UpdateType type, String id) { + for (Iterator<UpdateTask> iter = _checkers.iterator(); iter.hasNext(); ) { + UpdateTask t = iter.next(); + if (t.getType() == type && id.equals(t.getID())) { + iter.remove(); + t.shutdown(); + } + } + } + + /** + * Install a plugin. Non-blocking. + * If returns true, then call isUpdateInProgress() in a loop + * @return true if task started + */ + public boolean installPlugin(URI uri) { + String fakeName = Long.toString(_context.random().nextLong()); + List<URI> uris = Collections.singletonList(uri); + UpdateItem fake = new UpdateItem(PLUGIN_INSTALL, fakeName); + VersionAvailable va = new VersionAvailable("", "", HTTP, uris); + _available.put(fake, va); + return update(PLUGIN_INSTALL, fakeName); + } + + /** + * Non-blocking. Does not check. + * If returns true, then call isUpdateInProgress() in a loop + * Max time 3 hours by default but not honored by all Updaters + * @return true if task started + */ + public boolean update(UpdateType type) { + return update(type, "", 3*60*1000); + } + + /** + * Non-blocking. Does not check. + * Max time 3 hours by default but not honored by all Updaters + * If returns true, then call isUpdateInProgress() in a loop + * @return true if task started + */ + public boolean update(UpdateType type, String id) { + return update(type, id, 3*60*60*1000); + } + + /** + * Non-blocking. Does not check. + * If returns true, then call isUpdateInProgress() in a loop + * @param maxTime not honored by all Updaters + * @return true if task started + */ + public boolean update(UpdateType type, long maxTime) { + return update(type, "", maxTime); + } + + /** + * Non-blocking. Does not check. + * If returns true, then call isUpdateInProgress() in a loop + * @param maxTime not honored by all Updaters + * @return true if task started + */ + public boolean update(UpdateType type, String id, long maxTime) { + if (isCheckInProgress(type, id) || isUpdateInProgress(type, id)) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Check or update already in progress for: " + type + ' ' + id); + return false; + } + List<URI> updateSources = null; + UpdateItem ui = new UpdateItem(type, id); + VersionAvailable va = _available.get(ui); + if (va == null) + return false; + List<RegisteredUpdater> sorted = new ArrayList(_registered); + Collections.sort(sorted); + return retry(ui, va.sourceMap, sorted, maxTime) != null; + } + + private UpdateTask retry(UpdateItem ui, + Map<UpdateMethod, List<URI>> sourceMap, + List<RegisteredUpdater> toTry, long maxTime) { + for (Iterator<RegisteredUpdater> iter = toTry.iterator(); iter.hasNext(); ) { + RegisteredUpdater r = iter.next(); + iter.remove(); + // check in case unregistered later + if (!_registered.contains(r)) + continue; + for (Map.Entry<UpdateMethod, List<URI>> e : sourceMap.entrySet()) { + UpdateMethod meth = e.getKey(); + if (r.type == ui.type && r.method == meth) { + // fixme + UpdateTask t = r.updater.update(ui.type, meth, e.getValue(), ui.id, "", maxTime); + if (t != null) { + // race window here + // store the remaining ones for retrying + _downloaders.put(t, toTry); + return t; + } + } + } + } + return null; + } + + /////////// start UpdateManager interface + + /** + * Call multiple times, one for each type/method pair. + */ + public void register(Updater updater, UpdateType type, UpdateMethod method, int priority) { + RegisteredUpdater ru = new RegisteredUpdater(updater, type, method, priority); + if (_log.shouldLog(Log.INFO)) + _log.info("Registering " + ru); + _registered.add(ru); + } + + public void unregister(Updater updater, UpdateType type, UpdateMethod method) { + RegisteredUpdater ru = new RegisteredUpdater(updater, type, method, 0); + if (_log.shouldLog(Log.INFO)) + _log.info("Unregistering " + ru); + _registered.remove(ru); + } + + /** + * 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 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 it's newer + */ + public boolean notifyVersionAvailable(UpdateTask task, URI newsSource, + UpdateType type, String id, + UpdateMethod method, List<URI> updateSources, + String newVersion, String minVersion) { + if (type == NEWS) { + // shortcut + notifyInstalled(NEWS, "", newVersion); + return true; + } + UpdateItem ui = new UpdateItem(type, id); + VersionAvailable newVA = new VersionAvailable(newVersion, minVersion, method, updateSources); + Version old = _installed.get(ui); + if (_log.shouldLog(Log.INFO)) + _log.info("notifyVersionAvailable " + ui + ' ' + newVA + " old: " + old); + if (old != null && old.compareTo(newVA) >= 0) { + if (_log.shouldLog(Log.WARN)) + _log.warn(ui.toString() + ' ' + old + " already installed"); + return false; + } + old = _downloaded.get(ui); + if (old != null && old.compareTo(newVA) >= 0) { + if (_log.shouldLog(Log.WARN)) + _log.warn(ui.toString() + ' ' + old + " already downloaded"); + return false; + } + VersionAvailable oldVA = _available.get(ui); + if (oldVA != null) { + int comp = oldVA.compareTo(newVA); + if (comp > 0) { + if (_log.shouldLog(Log.WARN)) + _log.warn(ui.toString() + ' ' + oldVA + " already available"); + return false; + } + if (comp == 0) { + if (oldVA.sourceMap.putIfAbsent(method, updateSources) == null) { + if (_log.shouldLog(Log.WARN)) + _log.warn(ui.toString() + ' ' + oldVA + " updated with new source method"); + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn(ui.toString() + ' ' + oldVA + " already available"); + } + return false; + } + } + + if (_log.shouldLog(Log.INFO)) + _log.info(ui.toString() + ' ' + newVA + " now available"); + _available.put(ui, newVA); + + switch (type) { + case NEWS: + break; + + case ROUTER_SIGNED: + case ROUTER_SIGNED_PACK200: + if (shouldInstall()) { +//////////// + } + break; + + case ROUTER_UNSIGNED: + if (shouldInstall()) { +//////////// + } + break; + + case PLUGIN: + String msg = "<b>" + _("New plugin version {0} is available", newVersion) + "</b>"; + finishStatus(msg); + break; + + default: + break; + } + return true; +// TODO + } + + /** + * Called by the Updater after check() was called and all notifyVersionAvailable() callbacks are finished + */ + public void notifyCheckComplete(UpdateTask task, boolean newer, boolean success) { + if (_log.shouldLog(Log.INFO)) + _log.info(task.toString() + " complete"); + _checkers.remove(task); + switch (task.getType()) { + case NEWS: + // NewsFetcher will notify and spin off update tasks + break; + + case ROUTER_SIGNED: + case ROUTER_SIGNED_PACK200: + break; + + case ROUTER_UNSIGNED: + // if _mgr.getUpdateDownloaded(ROUTER_SIGNED) != null; + break; + + case PLUGIN: + String msg = null; + if (!success) + msg = "<b>" + _("Update check failed for plugin {0}", task.getID()) + "</b>"; + else if (!newer) + msg = "<b>" + _("No new version is available for plugin {0}", task.getID()) + "</b>"; + /// else success.... message for that? + + if (msg != null) + finishStatus(msg); + break; + + default: + break; + } + synchronized(task) { + task.notifyAll(); + } +// TODO + } + + public void notifyProgress(UpdateTask task, String status, long downloaded, long totalSize) { + StringBuilder buf = new StringBuilder(64); + buf.append(status).append(' '); + double pct = ((double)downloaded) / ((double)totalSize); + synchronized (_pct) { + buf.append(_pct.format(pct)); + } + buf.append("<br>\n"); + buf.append(_("{0}B transferred", DataHelper.formatSize2(downloaded))); + updateStatus(buf.toString()); + } + + /** + * @param task may be null + */ + public void notifyProgress(UpdateTask task, String status) { + updateStatus(status); + } + + /** + * An expiring status + * @param task may be null + */ + public void notifyComplete(UpdateTask task, String status) { + finishStatus(status); + } + + /** + * 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) { + _log.warn("Attempt failed " + task + ": " + reason, t); + } + + /** + * The task has finished and failed. + * @param t may be null + */ + public void notifyTaskFailed(UpdateTask task, String reason, Throwable t) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Failed " + task + ": " + reason, t); + List<RegisteredUpdater> toTry = _downloaders.get(task); + if (toTry != null) { + UpdateItem ui = new UpdateItem(task.getType(), task.getID()); + VersionAvailable va = _available.get(ui); + if (va != null) { + UpdateTask next = retry(ui, va.sourceMap, toTry, 3*60*1000); // fixme old maxtime lost + if (next != null) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Retrying with " + next); + } + } + } + _downloaders.remove(task); +///// for certain types only + finishStatus("<b>" + _("Transfer failed from {0}", linkify(task.getURI().toString())) + "</b>"); + } + + /** + * 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. + * If the return value is false, caller must call notifyTaskFailed() or notifyComplete() + * again. + * + * @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) { + if (_log.shouldLog(Log.INFO)) + _log.info(task.toString() + " complete"); + boolean rv = false; + switch (task.getType()) { + case TYPE_DUMMY: + case NEWS: + rv = true; + break; + + case ROUTER_SIGNED: + case ROUTER_SIGNED_PACK200: + rv = handleSudFile(task.getURI(), actualVersion, file); + if (rv) + notifyDownloaded(task.getType(), task.getID(), actualVersion); + break; + + case ROUTER_UNSIGNED: + rv = handleUnsignedFile(task.getURI(), actualVersion, file); +/////// FIXME RFC822 or long? + if (rv) + notifyDownloaded(task.getType(), task.getID(), actualVersion); + break; + + case PLUGIN: +/// FIXME probably handled in PluginUpdateRunner?????????? + rv = handlePluginFile(task.getURI(), actualVersion, file); + break; + + default: + break; + } + if (rv) + _downloaders.remove(task); + return rv; + } + + ///////// End UpdateManager interface + + /** + * Adds to installed, removes from downloaded and available + * @param version null to remove from installed + */ + private void notifyInstalled(UpdateType type, String id, String version) { + UpdateItem ui = new UpdateItem(type, id); + if (version == null) { + _installed.remove(ui); + if (_log.shouldLog(Log.INFO)) + _log.info(ui + " removed"); + return; + } + Version ver = new Version(version); + if (_log.shouldLog(Log.INFO)) + _log.info(ui + " " + ver + " installed"); + _installed.put(ui, ver); + Version old = _downloaded.get(ui); + if (old != null && old.compareTo(ver) <= 0) + _downloaded.remove(ui); + old = _available.get(ui); + if (old != null && old.compareTo(ver) <= 0) + _available.remove(ui); + } + + /** + * Adds to downloaded, removes from available + */ + private void notifyDownloaded(UpdateType type, String id, String version) { + UpdateItem ui = new UpdateItem(type, id); + Version ver = new Version(version); + if (_log.shouldLog(Log.INFO)) + _log.info(ui + " " + ver + " downloaded"); + _downloaded.put(ui, ver); + // one trumps the other + if (type == ROUTER_SIGNED) + _downloaded.remove(new UpdateItem(ROUTER_UNSIGNED, "")); + else if (type == ROUTER_UNSIGNED) + _downloaded.remove(new UpdateItem(ROUTER_SIGNED, "")); + Version old = _available.get(ui); + if (old != null && old.compareTo(ver) <= 0) + _available.remove(ui); + } + + /** from NewsFetcher */ + private boolean shouldInstall() { + String policy = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_POLICY); + if ("notify".equals(policy) || NewsHelper.dontInstall(_context)) + return false; +////////////////// + File zip = new File(_context.getRouterDir(), Router.UPDATE_FILE); + return !zip.exists(); + } + + /** + * Where to find various resources + * @return non-null may be empty + */ + public List<URI> getUpdateURLs(UpdateType type, String id, UpdateMethod method) { + VersionAvailable va = _available.get(new UpdateItem(type, id)); + if (va != null) { + List<URI> rv = va.sourceMap.get(method); + if (rv != null) + return rv; + } + + switch (type) { + case NEWS: + // handled in NewsHandler + break; + + case ROUTER_SIGNED: + case ROUTER_SIGNED_PACK200: + String URLs = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_URL, ConfigUpdateHandler.DEFAULT_UPDATE_URL); + StringTokenizer tok = new StringTokenizer(URLs, " ,\r\n"); + List<URI> rv = new ArrayList(); + while (tok.hasMoreTokens()) { + try { + rv.add(new URI(tok.nextToken().trim())); + } catch (URISyntaxException use) {} + } + Collections.shuffle(rv, _context.random()); + return rv; + + case ROUTER_UNSIGNED: + String url = _context.getProperty(ConfigUpdateHandler.PROP_ZIP_URL); + if (url != null) { + try { + return Collections.singletonList(new URI(url)); + } catch (URISyntaxException use) {} + } + break; + + case PLUGIN: + Properties props = PluginStarter.pluginProperties(_context, id); + String oldVersion = props.getProperty("version"); + String xpi2pURL = props.getProperty("updateURL"); + if (xpi2pURL != null) { + try { + return Collections.singletonList(new URI(xpi2pURL)); + } catch (URISyntaxException use) {} + } + break; + + default: + break; + } + return Collections.EMPTY_LIST; + } + + /** + * @return success + */ + private boolean handleSudFile(URI uri, String actualVersion, File f) { + String url = uri.toString(); + // Process the .sud/.su2 file + updateStatus("<b>" + _("Update downloaded") + "</b>"); + TrustedUpdate up = new TrustedUpdate(_context); + File to = new File(_context.getRouterDir(), Router.UPDATE_FILE); + String err = up.migrateVerified(RouterVersion.VERSION, f, to); +/////////// + // caller must delete now.. why? + //f.delete(); + if (err == null) { + String policy = _context.getProperty(ConfigUpdateHandler.PROP_UPDATE_POLICY); + // So unsigned update handler doesn't overwrite unless newer. +/// FIXME + //String lastmod = _get.getLastModified(); + String lastmod = null; + long modtime = 0; + if (lastmod != null) + modtime = RFC822Date.parse822Date(lastmod); + if (modtime <= 0) + modtime = _context.clock().now(); + _context.router().saveConfig(NewsHelper.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>"); + } + return err == null; + } + + /** + * @return success + */ + private boolean handleUnsignedFile(URI uri, String lastmod, File updFile) { + if (FileUtil.verifyZip(updFile)) { + updateStatus("<b>" + _("Update downloaded") + "</b>"); + } else { + updFile.delete(); + String url = uri.toString(); + updateStatus("<b>" + _("Unsigned update file from {0} is corrupt", url) + "</b>"); + _log.log(Log.CRIT, "Corrupt zip file from " + url); + return false; + } + 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); + long modtime = 0; + if (lastmod != null) + modtime = RFC822Date.parse822Date(lastmod); + if (modtime <= 0) + modtime = _context.clock().now(); + _context.router().saveConfig(NewsHelper.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")); +/// OK? + buf.append(' ').append(_("Version {0}", lastmod)); + updateStatus(buf.toString()); + } + } else { + _log.log(Log.CRIT, "Failed copy to " + to); + updateStatus("<b>" + _("Failed copy to {0}", to.getAbsolutePath()) + "</b>"); + } + return copied; + } + + /** + * @return success + */ + private boolean handlePluginFile(URI uri, String actualVersion, File sudFile) { + //////////////// handled elsewhere? + return false; + } + + private void restart() { + if (_context.hasWrapper()) + ConfigServiceHandler.registerWrapperNotifier(_context, Router.EXIT_GRACEFUL_RESTART, false); + _context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART); + } + + static String linkify(String url) { + return "<a target=\"_blank\" href=\"" + url + "\"/>" + url + "</a>"; + } + + /** translate a string */ + private String _(String s) { + return Messages.getString(s, _context); + } + + /** + * translate a string with a parameter + */ + private String _(String s, Object o) { + return Messages.getString(s, o, _context); + } + + private void updateStatus(String s) { + _status = s; + } + + private void finishStatus(String msg) { + updateStatus(msg); + _context.simpleScheduler().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(""); + } + } + + /** + * Equals on updater, type and method only + */ + private static class RegisteredUpdater implements Comparable<RegisteredUpdater> { + public final Updater updater; + public final UpdateType type; + public final UpdateMethod method; + public final int priority; + + public RegisteredUpdater(Updater u, UpdateType t, UpdateMethod m, int priority) { + updater = u; type = t; method = m; this.priority = priority; + } + + /** reverse, highest priority first, ensure different ones are different */ + public int compareTo(RegisteredUpdater r) { + int p = r.priority - priority; + if (p != 0) + return p; + return hashCode() - r.hashCode(); + } + + @Override + public int hashCode() { + return updater.hashCode() ^ type.hashCode() ^ method.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof RegisteredUpdater)) + return false; + RegisteredUpdater r = (RegisteredUpdater) o; + return type == r.type && method == r.method && + updater.equals(r.updater); + } + + @Override + public String toString() { + return "RegisteredUpdater " + updater + " for " + type + ' ' + method + " @pri " + priority; + } + } + + /** + * Equals on type and ID only + */ + private static class UpdateItem { + public final UpdateType type; + public final String id; + + public UpdateItem(UpdateType t, String id) { + type = t; + this.id = id; + } + + @Override + public int hashCode() { + return type.hashCode() ^ id.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof UpdateItem)) + return false; + UpdateItem r = (UpdateItem) o; + return type == r.type && id.equals(r.id); + } + + @Override + public String toString() { + return "UpdateItem " + type + ' ' + id; + } + } + + private static class Version implements Comparable<Version> { + public final String version; + + public Version(String version) { + this.version = version; + } + + public int compareTo(Version r) { + return _versionComparator.compare(version, r.version); + } + + @Override + public String toString() { + return "Version " + version; + } + } + + private static class VersionAvailable extends Version { + public final String minVersion; + public final ConcurrentHashMap<UpdateMethod, List<URI>> sourceMap; + + /** + * Puts the method and sources in the map. The map may be added to later. + */ + public VersionAvailable(String version, String min, UpdateMethod method, List<URI> updateSources) { + super(version); + minVersion = min; + sourceMap = new ConcurrentHashMap(4); + sourceMap.put(method, updateSources); + } + + @Override + public String toString() { + return "VersionAvailable " + version + ' ' + sourceMap; + } + } +} diff --git a/apps/routerconsole/java/src/net/i2p/router/update/DummyHandler.java b/apps/routerconsole/java/src/net/i2p/router/update/DummyHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..04b820ec418e5b395a940279b8a97f854aebbd3d --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/update/DummyHandler.java @@ -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); + } + } + } +} diff --git a/apps/routerconsole/java/src/net/i2p/router/update/NewsFetcher.java b/apps/routerconsole/java/src/net/i2p/router/update/NewsFetcher.java new file mode 100644 index 0000000000000000000000000000000000000000..5d74a8a4b862d539eccd4ea8629562c952487265 --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/update/NewsFetcher.java @@ -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) {} +} diff --git a/apps/routerconsole/java/src/net/i2p/router/update/NewsHandler.java b/apps/routerconsole/java/src/net/i2p/router/update/NewsHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..135c254334f45ee797cb86e11d17d06061c00267 --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/update/NewsHandler.java @@ -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; + } +} diff --git a/apps/routerconsole/java/src/net/i2p/router/update/NewsTimerTask.java b/apps/routerconsole/java/src/net/i2p/router/update/NewsTimerTask.java new file mode 100644 index 0000000000000000000000000000000000000000..1ad65b3fcb3560c8e9172bbe65845f3276515657 --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/update/NewsTimerTask.java @@ -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, ""); + } +} diff --git a/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateChecker.java b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateChecker.java new file mode 100644 index 0000000000000000000000000000000000000000..3ae282f5b5e39d7da2f94eba2b5d21ee75f713a1 --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateChecker.java @@ -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(CONNECT_TIMEOUT); + } 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); + } +} + diff --git a/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..fe394135b39b83f6a7a95b1609b3da7d1fd53871 --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateHandler.java @@ -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; + } +} + diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java similarity index 80% rename from apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java rename to apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java index 8e895702409bf193107f855d2dac3dc9332ad702..d75a8ef43d8795b77ad011eb0c8f33ec211b365e 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/update/PluginUpdateRunner.java @@ -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. + * + * Moved from web/ and turned into an UpdateTask. * - * @since 0.7.12 - * @author zzz + * @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"; - - private static PluginUpdateHandler _instance; - public static final synchronized PluginUpdateHandler getInstance(RouterContext ctx) { - if (_instance != null) - return _instance; - _instance = new PluginUpdateHandler(ctx); - return _instance; + public static final String PLUGIN_DIR = PluginStarter.PLUGIN_DIR; + + public PluginUpdateRunner(RouterContext ctx, List<URI> uris, String appName, String oldVersion ) { + super(ctx, uris); + if (uris.isEmpty()) + _uri = null; + else + _uri = uris.get(0); + _xpi2pURL = _uri.toString(); + _appName = appName; + _oldVersion = oldVersion; } - 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 boolean isDone() { - // FIXME - return false; - } - - /** @since 0.8.13 */ - public boolean wasUpdateSuccessful() { - return _updated; - } - - private void scheduleStatusClean(String msg) { - _context.simpleScheduler().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 UpdateType getType() { + return _oldVersion.equals("") ? UpdateType.PLUGIN_INSTALL : UpdateType.PLUGIN; } - public class PluginUpdateRunner extends UpdateRunner implements Runnable, EepGet.StatusListener { - - public PluginUpdateRunner(String url) { - super(); - } + @Override + public URI getURI() { return _uri; } @Override protected void update() { + _updated = false; if(_xpi2pURL.startsWith("file://")) { updateStatus("<b>" + _("Attempting to install from file {0}", _xpi2pURL) + "</b>"); @@ -139,7 +95,7 @@ public class PluginUpdateHandler extends UpdateHandler { } else { updateStatus("<b>" + _("Downloading plugin from {0}", _xpi2pURL) + "</b>"); // use the same settings as for updater - boolean shouldProxy = Boolean.parseBoolean(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)); + 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 { @@ -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; @@ -313,7 +254,7 @@ public class PluginUpdateHandler extends UpdateHandler { boolean wasRunning = false; File destDir = new SecureDirectory(appDir, appName); if (destDir.exists()) { - if (Boolean.parseBoolean(props.getProperty("install-only"))) { + if (Boolean.valueOf(props.getProperty("install-only")).booleanValue()) { to.delete(); statusDone("<b>" + _("Downloaded plugin is for new installs only, but the plugin is already installed", url) + "</b>"); return; @@ -374,7 +315,7 @@ public class PluginUpdateHandler extends UpdateHandler { return; } // do we defer extraction and installation? - if (Boolean.parseBoolean(props.getProperty("router-restart-required"))) { + if (Boolean.valueOf(props.getProperty("router-restart-required")).booleanValue()) { // Yup! try { if(!FileUtil.copy(to, (new SecureFile( new SecureFile(appDir.getCanonicalPath() +"/" + appName +"/"+ ZIP).getCanonicalPath())) , true, true)) { @@ -405,7 +346,7 @@ public class PluginUpdateHandler extends UpdateHandler { } update = true; } else { - if (Boolean.parseBoolean(props.getProperty("update-only"))) { + if (Boolean.valueOf(props.getProperty("update-only")).booleanValue()) { to.delete(); statusDone("<b>" + _("Plugin is for upgrades only, but the plugin is not installed") + "</b>"); return; @@ -426,7 +367,7 @@ public class PluginUpdateHandler extends UpdateHandler { _updated = true; to.delete(); // install != update. Changing the user's settings like this is probabbly a bad idea. - if (Boolean.parseBoolean( props.getProperty("dont-start-at-install"))) { + if (Boolean.valueOf( props.getProperty("dont-start-at-install")).booleanValue()) { statusDone("<b>" + _("Plugin {0} installed", appName) + "</b>"); if(!update) { Properties pluginProps = PluginStarter.pluginProperties(); @@ -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; - } } diff --git a/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateChecker.java b/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateChecker.java new file mode 100644 index 0000000000000000000000000000000000000000..f56979e36c7c9e2954b82eb467df6c090bb21cf9 --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateChecker.java @@ -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; + } +} diff --git a/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..7e5628684a9932f1dd631b4805d3b2ff01a9f853 --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateHandler.java @@ -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; + } +} diff --git a/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateRunner.java b/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateRunner.java new file mode 100644 index 0000000000000000000000000000000000000000..57495f63486e5e5ccbea142951b50967a7479942 --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/update/UnsignedUpdateRunner.java @@ -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(CONNECT_TIMEOUT, -1, INACTIVITY_TIMEOUT); + } 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 + } +} diff --git a/apps/routerconsole/java/src/net/i2p/router/update/UpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/update/UpdateHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..9c79dcb282db6d1ec013933c74f1a99576432796 --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/update/UpdateHandler.java @@ -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; + } +} diff --git a/apps/routerconsole/java/src/net/i2p/router/update/UpdateRunner.java b/apps/routerconsole/java/src/net/i2p/router/update/UpdateRunner.java new file mode 100644 index 0000000000000000000000000000000000000000..798b886cda97399caf64751e79744c17efd86fcf --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/update/UpdateRunner.java @@ -0,0 +1,235 @@ +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"; + + protected static final long CONNECT_TIMEOUT = 55*1000; + protected static final long INACTIVITY_TIMEOUT = 5*60*1000; + protected static final long NOPROXY_INACTIVITY_TIMEOUT = 60*1000; + + 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(CONNECT_TIMEOUT); + } 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(CONNECT_TIMEOUT, -1, shouldProxy ? INACTIVITY_TIMEOUT : NOPROXY_INACTIVITY_TIMEOUT); + } 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); + } +} diff --git a/apps/routerconsole/java/src/net/i2p/router/update/package.html b/apps/routerconsole/java/src/net/i2p/router/update/package.html new file mode 100644 index 0000000000000000000000000000000000000000..5b03299139e681d7b753ee37a4f8a488e8f7f030 --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/update/package.html @@ -0,0 +1,7 @@ +<html> +<body> +<p> +Classes to implement the update process. +</p> +</body> +</html> diff --git a/apps/routerconsole/java/src/net/i2p/router/web/CSSHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/CSSHelper.java index 15c2b2b2c27568d4b0aff374fbdb8c2a1184e507..785af69a3e3f980b395ddfe364d6312120eee32a 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/CSSHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/CSSHelper.java @@ -59,7 +59,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")); } /** diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java index 6c37db82909e9f40432ad67f445c652610cb4479..06f87e377f1efab152dda60d40423a6d7149b507 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java @@ -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))) { - addFormError(_("Plugin or update download already in progress.")); + ConsoleUpdateManager mgr = (ConsoleUpdateManager) _context.updateManager(); + if (mgr == null) { + addFormError("Update manager not registered, cannot install"); return; } - PluginUpdateHandler puh = PluginUpdateHandler.getInstance(_context); - if (puh.isRunning()) { + if (mgr.isUpdateInProgress()) { addFormError(_("Plugin or update download already in progress.")); return; } - puh.update(url); - addFormNotice(_("Downloading plugin from {0}", url)); + URI uri; + try { + uri = new URI(url); + } catch (URISyntaxException use) { + addFormError(_("Bad URL {0}", url)); + return; + } + 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.")); - return; - } - PluginUpdateChecker puc = PluginUpdateChecker.getInstance(_context); - if (puc.isRunning()) { - addFormError(_("Plugin or update download already in progress.")); + ConsoleUpdateManager mgr = (ConsoleUpdateManager) _context.updateManager(); + if (mgr == null) { + addFormError("Update manager not registered, cannot check"); 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 { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java index c3816570157cca83bff05c4b550e5be514f0866a..b4a1155ffcc4eee69439d2350f88a7674905af6b 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java @@ -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('<', ' '); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java index a3b30d337ad2b7ee08cb1a09da999ce9c7205772..0340dde3e7de185a7f0cb9a72488256fc8db8d4b 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHandler.java @@ -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)); } } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHelper.java index 07fad31975da21d62aec17baab9a651054686cf4..7fecfc85f57ff10ee1912054532d406970f90133 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigUpdateHelper.java @@ -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() { @@ -159,6 +159,6 @@ public class ConfigUpdateHelper extends HelperBase { } public String getNewsStatus() { - return NewsFetcher.getInstance(_context).status(); + return NewsHelper.status(_context); } } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/FileDumpHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/FileDumpHelper.java index 7bb1e99d05f2efe61713b419dbd7ccedb9c7a43d..41ffec627b5da02f5e6cb269a07710a098ee3877 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/FileDumpHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/FileDumpHelper.java @@ -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); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java index de9d427eeabe0c16803b3e5ee68ba228c33bccef..c2878b383c61b1f3106e6000b8df380e6ef16518 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java @@ -20,7 +20,7 @@ public class LogsHelper extends HelperBase { } /** @since 0.8.13 */ - static String jettyVersion() { + public static String jettyVersion() { return Server.getVersion(); } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java b/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java deleted file mode 100644 index 84b5a75ee56227815c51464b1dcd628206e8c12d..0000000000000000000000000000000000000000 --- a/apps/routerconsole/java/src/net/i2p/router/web/NewsFetcher.java +++ /dev/null @@ -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&consoleNonce=").append(consoleNonce).append("\">") - .append(Messages.getString("Hide news", _context)); - } else { - buf.append(" <a href=\"/?news=1&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 = _context.getBooleanProperty(ConfigUpdateHandler.PROP_UPDATE_DISABLED); - 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.parseBoolean(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)); - 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 && - _context.getBooleanProperty(ConfigUpdateHandler.PROP_UPDATE_UNSIGNED) && - !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.parseBoolean(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)); - 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) {} -} diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NewsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NewsHelper.java index d5d702e8ff01cbbf9313212e7910292ac63f7e11..f8a45b215c5b82242693668751919cfcdb67d2da 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/NewsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/NewsHelper.java @@ -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&consoleNonce=").append(consoleNonce).append("\">") + .append(Messages.getString("Hide news", ctx)); + } else { + buf.append(" <a href=\"/?news=1&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; } } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java index 60e9dd8c3a3f4f51a283f0c885f817e09a143f2d..6d70a9dbf030838e325d9edd2138c6feb70c5302 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/PluginStarter.java @@ -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.parseBoolean(System.getProperty(UpdateHandler.PROP_UPDATE_IN_PROGRESS))) && + (!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; diff --git a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java b/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java deleted file mode 100644 index 31dfb623f25007749a69c9e05be4f4e705d7fdf4..0000000000000000000000000000000000000000 --- a/apps/routerconsole/java/src/net/i2p/router/web/PluginUpdateChecker.java +++ /dev/null @@ -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) { - _context.simpleScheduler().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(CONNECT_TIMEOUT); - } 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); - } - } -} - diff --git a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java index 488c5b655d6a7c7593c3aa272baa4cec4b8c9166..2144fc8c1d1606ff809292b1b990bb7716426f1d 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java @@ -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; @@ -568,10 +569,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); @@ -579,7 +578,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); @@ -751,22 +749,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()); } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java index 99a64df949764cb84a6e2a544a1b0edaee240e05..abc52aa654915cc5414de583cadbde18ae782e36 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java @@ -628,19 +628,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(); } /** @@ -650,12 +650,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>\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 diff --git a/apps/routerconsole/java/src/net/i2p/router/web/UnsignedUpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/UnsignedUpdateHandler.java deleted file mode 100644 index 4725f62b167f7108a80201812bb15ae4d874093f..0000000000000000000000000000000000000000 --- a/apps/routerconsole/java/src/net/i2p/router/web/UnsignedUpdateHandler.java +++ /dev/null @@ -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(CONNECT_TIMEOUT, -1, INACTIVITY_TIMEOUT); - } 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>"); - } - } - } -} diff --git a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java index 2b87231bb6ccc2d39a20bb279e56c7292b49cc84..412a9072835574dbf55fe2d24325275435a6c888 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/UpdateHandler.java @@ -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,31 +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"; - - protected static final long CONNECT_TIMEOUT = 55*1000; - protected static final long INACTIVITY_TIMEOUT = 5*60*1000; - protected static final long NOPROXY_INACTIVITY_TIMEOUT = 60*1000; - 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(); } /** @@ -89,274 +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(); - } - } - } - - public static String getStatus() { - return _status; + mgr.update(type); } - - 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.parseBoolean(_context.getProperty(ConfigUpdateHandler.PROP_SHOULD_PROXY, ConfigUpdateHandler.DEFAULT_SHOULD_PROXY)); - 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(CONNECT_TIMEOUT); - } 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(CONNECT_TIMEOUT, -1, shouldProxy ? INACTIVITY_TIMEOUT : NOPROXY_INACTIVITY_TIMEOUT); - } 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); - double pct = ((double)alreadyTransferred + (double)currentWrite) / - ((double)alreadyTransferred + (double)currentWrite + bytesRemaining); - synchronized (_pct) { - buf.append(_("{0} downloaded", _pct.format(pct))); - } - buf.append("<br>\n"); - buf.append(DataHelper.formatSize2(currentWrite + alreadyTransferred)) - .append("B / ") - .append(DataHelper.formatSize2(currentWrite + alreadyTransferred + bytesRemaining)) - .append("B"); - 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); - } - } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java b/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java index 98ca9311337091847c65c08dba159ed4191880a2..5afa5d2db080a466fd2b7e7bf36a3d4da1e1399a 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/WebAppConfiguration.java @@ -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; diff --git a/core/java/build.xml b/core/java/build.xml index a415e7954fcf766ec6959007bd44d5d6cdf203c4..c52df80d319d580b1ff8d0fbb5dbd5130ea3aaf7 100644 --- a/core/java/build.xml +++ b/core/java/build.xml @@ -81,14 +81,15 @@ </target> <!-- unit tests --> - <target name="scalatest.compileTest" depends="jar, scala.init"> + <target name="scalatest.compileTest" depends="compile, scala.init"> <mkdir dir="./build" /> <mkdir dir="./build/obj_scala" /> <scalac srcdir="./test/scalatest" destdir="./build/obj_scala" deprecation="on" > <classpath> + <pathelement location="${classpath}" /> <pathelement location="${scala-library.jar}" /> <pathelement location="${scalatest.jar}" /> - <pathelement location="./build/i2p.jar" /> + <pathelement location="./build/obj" /> </classpath> </scalac> </target> @@ -102,6 +103,23 @@ <compilerarg line="${javac.compilerargs}" /> </javac> </target> + <!-- jars with tests --> + <target name="jarScalaTest" depends="scalatest.compileTest"> + <mkdir dir="./build/obj_scala_jar" /> + <copy todir="./build/obj_scala_jar"> + <fileset dir="./build/"> + <include name="obj/**/*.class"/> + </fileset> + <mapper type="glob" from="obj/*" to="*" /> + </copy> + <copy todir="./build/obj_scala_jar"> + <fileset dir="./build/"> + <include name="obj_scala/**/*.class"/> + </fileset> + <mapper type="glob" from="obj_scala/*" to="*" /> + </copy> + <jar destfile="./build/i2pscalatest.jar" basedir="./build/obj_scala_jar" includes="**/*.class" /> + </target> <target name="jarTest" depends="junit.compileTest"> <jar destfile="./build/i2ptest.jar" basedir="./build/obj" includes="**/*.class" /> </target> diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java index bd1085f180ab1e02de137ab6867f9468432a1a7e..05475eea001d66ed12d76a3bb5383ebadfcac4c8 100644 --- a/core/java/src/net/i2p/I2PAppContext.java +++ b/core/java/src/net/i2p/I2PAppContext.java @@ -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; @@ -991,4 +992,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; + } } diff --git a/core/java/src/net/i2p/update/UpdateManager.java b/core/java/src/net/i2p/update/UpdateManager.java new file mode 100644 index 0000000000000000000000000000000000000000..4099822bcc40633de37debc87254f014324cb0a0 --- /dev/null +++ b/core/java/src/net/i2p/update/UpdateManager.java @@ -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); +} diff --git a/core/java/src/net/i2p/update/UpdateMethod.java b/core/java/src/net/i2p/update/UpdateMethod.java new file mode 100644 index 0000000000000000000000000000000000000000..6d30bc99c87b3d3754bc36ca3b8059cbc7ae8aff --- /dev/null +++ b/core/java/src/net/i2p/update/UpdateMethod.java @@ -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 +} diff --git a/core/java/src/net/i2p/update/UpdateTask.java b/core/java/src/net/i2p/update/UpdateTask.java new file mode 100644 index 0000000000000000000000000000000000000000..b9237b3ee63af50ac58d38c4b3c27ecea6f0f3d1 --- /dev/null +++ b/core/java/src/net/i2p/update/UpdateTask.java @@ -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(); +} diff --git a/core/java/src/net/i2p/update/UpdateType.java b/core/java/src/net/i2p/update/UpdateType.java new file mode 100644 index 0000000000000000000000000000000000000000..f16eaad7d29b9e2554b9a7d690f1ab741f9fdf8e --- /dev/null +++ b/core/java/src/net/i2p/update/UpdateType.java @@ -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 +} diff --git a/core/java/src/net/i2p/update/Updater.java b/core/java/src/net/i2p/update/Updater.java new file mode 100644 index 0000000000000000000000000000000000000000..bea1a778dbd4417d3fc6895b4775700e08ab56f2 --- /dev/null +++ b/core/java/src/net/i2p/update/Updater.java @@ -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); +} diff --git a/core/java/src/net/i2p/update/package.html b/core/java/src/net/i2p/update/package.html new file mode 100644 index 0000000000000000000000000000000000000000..20e06658ea0a70cf46111552ec5da875c0c8ec8d --- /dev/null +++ b/core/java/src/net/i2p/update/package.html @@ -0,0 +1,8 @@ +<html> +<body> +<p> +Interfaces for classes to assist in the update process without +needing the router context. +</p> +</body> +</html> diff --git a/core/java/src/net/i2p/util/VersionComparator.java b/core/java/src/net/i2p/util/VersionComparator.java index 3b6e97f5b48a834a041a4fa2150c17c48e647470..8c72252fcb40f4a7a0fe49ac2804cee7bfab3232 100644 --- a/core/java/src/net/i2p/util/VersionComparator.java +++ b/core/java/src/net/i2p/util/VersionComparator.java @@ -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 = ".-_"; diff --git a/router/java/build.xml b/router/java/build.xml index 83f23fa4443b2faf173d37635c4c90b59f5e0a6d..6b49c9530cecd9b515006e1f5c6717d7d1d50e18 100644 --- a/router/java/build.xml +++ b/router/java/build.xml @@ -99,18 +99,23 @@ </target> <!-- unit tests --> + <target name="builddepscalatest"> + <ant dir="../../core/java/" target="jar" /> + <ant dir="../../core/java/" target="jarScalaTest" /> + </target> <target name="builddeptest"> <ant dir="../../core/java/" target="jarTest" /> </target> - <target name="scalatest.compileTest" depends="jar, scala.init"> + <target name="scalatest.compileTest" depends="builddepscalatest, compile, scala.init"> <mkdir dir="./build" /> <mkdir dir="./build/obj_scala" /> <scalac srcdir="./test/scalatest" destdir="./build/obj_scala" deprecation="on" > <classpath> + <pathelement location="${classpath}" /> <pathelement location="${scala-library.jar}" /> <pathelement location="${scalatest.jar}" /> - <pathelement location="../../core/java/build/i2p.jar" /> - <pathelement location="./build/router.jar" /> + <pathelement location="../../core/java/build/i2pscalatest.jar" /> + <pathelement location="./build/obj" /> </classpath> </scalac> </target> @@ -124,6 +129,23 @@ <compilerarg line="${javac.compilerargs}" /> </javac> </target> + <!-- jars with tests --> + <target name="jarScalaTest" depends="scalatest.compileTest"> + <mkdir dir="./build/obj_scala_jar" /> + <copy todir="./build/obj_scala_jar"> + <fileset dir="./build/"> + <include name="obj/**/*.class"/> + </fileset> + <mapper type="glob" from="obj/*" to="*" /> + </copy> + <copy todir="./build/obj_scala_jar"> + <fileset dir="./build/"> + <include name="obj_scala/**/*.class"/> + </fileset> + <mapper type="glob" from="obj_scala/*" to="*" /> + </copy> + <jar destfile="./build/routerscalatest.jar" basedir="./build/obj_scala_jar" includes="**/*.class" /> + </target> <target name="jarTest" depends="junit.compileTest"> <jar destfile="./build/routertest.jar" basedir="./build/obj" includes="**/*.class" /> </target> diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java index 421539ce3fe1e69e42bc380e2a5412065cd87e62..51af505543b5d0eec87b9b9862ef5898002e1907 100644 --- a/router/java/src/net/i2p/router/RouterContext.java +++ b/router/java/src/net/i2p/router/RouterContext.java @@ -23,6 +23,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; @@ -56,11 +57,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 final List<RouterContext> _contexts = new CopyOnWriteArrayList(); @@ -483,6 +485,7 @@ public class RouterContext extends I2PAppContext { * @return true * @since 0.7.9 */ + @Override public boolean isRouterContext() { return true; } @@ -492,7 +495,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; + } + } }