UpdateRunner.java 10.91 KiB
package org.klomp.snark;
import java.io.File;
import java.net.URI;
import java.util.List;
import net.i2p.I2PAppContext;
import net.i2p.crypto.TrustedUpdate;
import net.i2p.data.DataHelper;
import net.i2p.update.*;
import net.i2p.util.Log;
import net.i2p.util.SimpleTimer2;
import org.klomp.snark.comments.CommentSet;
/**
* The downloader for router signed updates.
*
* @since 0.9.4
*/
class UpdateRunner implements UpdateTask, CompleteListener {
private final I2PAppContext _context;
private final Log _log;
private final UpdateManager _umgr;
private final SnarkManager _smgr;
private final UpdateType _type;
private final List<URI> _urls;
private volatile boolean _isRunning;
private volatile boolean _hasMetaInfo;
private volatile boolean _isComplete;
private final String _newVersion;
private URI _currentURI;
private Snark _snark;
private static final long MAX_LENGTH = 128*1024*1024;
private static final long METAINFO_TIMEOUT = 60*60*1000;
private static final long COMPLETE_TIMEOUT = 12*60*60*1000;
private static final long CHECK_INTERVAL = 3*60*1000;
public UpdateRunner(I2PAppContext ctx, UpdateManager umgr, SnarkManager smgr,
UpdateType type, List<URI> uris, String newVersion) {
_context = ctx;
_log = ctx.logManager().getLog(getClass());
_umgr = umgr;
_smgr = smgr;
_type = type;
_urls = uris;
_newVersion = newVersion;
}
//////// begin UpdateTask methods
public boolean isRunning() { return _isRunning; }
public void shutdown() {
_isRunning = false;
if (_snark != null) {
}
}
public UpdateType getType() { return _type; }
public UpdateMethod getMethod() { return UpdateMethod.TORRENT; }
public URI getURI() { return _currentURI; }
public String getID() { return ""; }
//////// end UpdateTask methods
public void start() {
_isRunning = true;
update();
}
/**
* 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.
*/
private void update() {
for (URI uri : _urls) {
_currentURI = uri;
String updateURL = uri.toString();
try {
MagnetURI magnet = new MagnetURI(_smgr.util(), updateURL);
byte[] ih = magnet.getInfoHash();
// do we already have it?
_snark = _smgr.getTorrentByInfoHash(ih);
if (_snark != null) {
if (_snark.getMetaInfo() != null) {
_hasMetaInfo = true;
Storage storage = _snark.getStorage();
if (storage != null && storage.complete())
processComplete(_snark);
}
if (!_isComplete) {
if (_snark.isStopped() && !_snark.isStarting())
_snark.startTorrent();
// we aren't a listener so we must poll
new Watcher();
}
break;
}
String name = magnet.getName();
String trackerURL = magnet.getTrackerURL();
if (trackerURL == null && !_smgr.util().shouldUseDHT() &&
!_smgr.util().shouldUseOpenTrackers()) {
// but won't we use OT as a failsafe even if disabled?
_umgr.notifyAttemptFailed(this, "No tracker, no DHT, no OT", null);
continue;
}
_snark = _smgr.addMagnet(name, ih, trackerURL, true, true, null, this);
if (_snark != null) {
updateStatus("<b>" + _smgr.util().getString("Updating from {0}", linkify(updateURL)) + "</b>");
new Timeout();
break;
}
} catch (IllegalArgumentException iae) {
_log.error("Invalid update URL", iae);
}
}
if (_snark == null)
fatal("No valid URLs");
}
/**
* This will run twice, once at the metainfo timeout and
* once at the complete timeout.
*/
private class Timeout extends SimpleTimer2.TimedEvent {
private final long _start = _context.clock().now();
public Timeout() {
super(_context.simpleTimer2(), METAINFO_TIMEOUT);
}
public void timeReached() {
if (_isComplete || !_isRunning)
return;
if (!_hasMetaInfo) {
fatal("Metainfo timeout");
return;
}
if (_context.clock().now() - _start >= COMPLETE_TIMEOUT) {
fatal("Complete timeout");
return;
}
reschedule(COMPLETE_TIMEOUT - METAINFO_TIMEOUT);
}
}
/**
* Rarely used - only if the user added the torrent, so
* we aren't a complete listener.
* This will periodically until the complete timeout.
*/
private class Watcher extends SimpleTimer2.TimedEvent {
private final long _start = _context.clock().now();
public Watcher() {
super(_context.simpleTimer2(), CHECK_INTERVAL);
}
public void timeReached() {
if (_hasMetaInfo && _snark.getRemainingLength() == 0 && !_isComplete)
processComplete(_snark);
if (_isComplete || !_isRunning)
return;
if (_context.clock().now() - _start >= METAINFO_TIMEOUT && !_hasMetaInfo) {
fatal("Metainfo timeout");
return;
}
if (_context.clock().now() - _start >= COMPLETE_TIMEOUT) {
fatal("Complete timeout");
return;
}
notifyProgress();
reschedule(CHECK_INTERVAL);
}
}
private void fatal(String error) {
if (_snark != null) {
if (_hasMetaInfo) {
// avoid loop stopTorrent() ... updateStatus() ... fatal() ...
if (!_snark.isStopped())
_smgr.stopTorrent(_snark, true);
String file = _snark.getName();
_smgr.removeTorrent(file);
// delete torrent file
File f = new File(_smgr.getDataDir(), file);
f.delete();
// delete data
file = _snark.getBaseName();
f = new File(_smgr.getDataDir(), file);
f.delete();
} else {
_smgr.deleteMagnet(_snark);
}
}
_umgr.notifyTaskFailed(this, error, null);
_log.error(error);
_isRunning = false;
// stop the tunnel if we were the only one running
if (_smgr.util().connected() && !_smgr.util().isConnecting()) {
for (Snark s : _smgr.getTorrents()) {
if (!s.isStopped())
return;
}
_smgr.util().disconnect();
}
}
private void processComplete(Snark snark) {
String dataFile = snark.getBaseName();
File f = new File(_smgr.getDataDir(), dataFile);
String sudVersion = TrustedUpdate.getVersionString(f);
if (_newVersion.equals(sudVersion))
_umgr.notifyComplete(this, _newVersion, f);
else
fatal("version mismatch");
_isComplete = true;
}
private void notifyProgress() {
if (_hasMetaInfo) {
long total = _snark.getTotalLength();
long remaining = _snark.getRemainingLength();
String status = "<b>" + _smgr.util().getString("Updating") + "</b>";
_umgr.notifyProgress(this, status, total - remaining, total);
}
}
//////// begin CompleteListener methods
//////// all pass through to SnarkManager
public void torrentComplete(Snark snark) {
processComplete(snark);
_smgr.torrentComplete(snark);
}
/**
* This is called by stopTorrent() among others
*/
public void updateStatus(Snark snark) {
if (snark.isStopped()) {
if (!_isComplete)
fatal("stopped by user");
}
_smgr.updateStatus(snark);
}
public String gotMetaInfo(Snark snark) {
MetaInfo info = snark.getMetaInfo();
if (info.getFiles() != null) {
fatal("more than 1 file");
return null;
}
if (info.isPrivate()) {
fatal("private torrent");
return null;
}
if (info.getTotalLength() > MAX_LENGTH) {
fatal("too big");
return null;
}
_hasMetaInfo = true;
notifyProgress();
snark.setAutoStoppable(true);
return _smgr.gotMetaInfo(snark);
}
public void fatal(Snark snark, String error) {
fatal(error);
_smgr.fatal(snark, error);
}
public void addMessage(Snark snark, String message) {
_smgr.addMessage(snark, message);
}
public void gotPiece(Snark snark) {
notifyProgress();
_smgr.gotPiece(snark);
}
public long getSavedTorrentTime(Snark snark) {
return _smgr.getSavedTorrentTime(snark);
}
public BitField getSavedTorrentBitField(Snark snark) {
return _smgr.getSavedTorrentBitField(snark);
}
public boolean getSavedPreserveNamesSetting(Snark snark) {
return _smgr.getSavedPreserveNamesSetting(snark);
}
public long getSavedUploaded(Snark snark) {
return _smgr.getSavedUploaded(snark);
}
/** @since 0.9.31 */
public CommentSet getSavedComments(Snark snark) {
return _smgr.getSavedComments(snark);
}
/** @since 0.9.31 */
public void locked_saveComments(Snark snark, CommentSet comments) {
_smgr.locked_saveComments(snark, comments);
}
/** @since 0.9.42 */
public boolean shouldAutoStart() {
return _smgr.shouldAutoStart();
}
/**
* @since 0.9.62
*/
public BandwidthListener getBandwidthListener() {
return _smgr.getBandwidthListener();
}
//////// end CompleteListener methods
private static String linkify(String url) {
String durl = url.length() <= 28 ? DataHelper.escapeHTML(url) :
DataHelper.escapeHTML(url.substring(0, 25)) + "…";
// TODO urlEncode instead
return "<a target=\"_blank\" href=\"" + DataHelper.escapeHTML(url) + "\"/>" + durl + "</a>";
}
private void updateStatus(String s) {
_umgr.notifyProgress(this, s);
}
@Override
public String toString() {
return getClass().getName() + ' ' + getType() + ' ' + getID() + ' ' + getMethod() + ' ' + getURI();
}
}