Compare commits

...

25 Commits

Author SHA1 Message Date
Zlatin Balevsky
bcb41baca2 update version, add link to Packaging page 2020-09-27 07:26:06 +01:00
Zlatin Balevsky
72985bacb6 add ability to disable updates completely, intended for 3rd party packaging 2020-09-26 17:53:29 +01:00
Zlatin Balevsky
3387d22a6c remove HOPELESS downloads from the download list via an event 2020-09-26 17:10:24 +01:00
Zlatin Balevsky
10bd566d58 Release 0.7.4 2020-09-25 16:51:40 +01:00
Zlatin Balevsky
f4e0c707df fix cleaning up of hopeless downloads in plugin 2020-09-23 15:23:51 +01:00
Zlatin Balevsky
c11a427483 fix cleaning up of hopeless downloads in gui 2020-09-23 15:21:47 +01:00
Zlatin Balevsky
e9db22c562 option for download attempts before giving up in desktop gui 2020-09-23 14:54:19 +01:00
Zlatin Balevsky
fa53a35023 option for download attempts before giving up in plugin 2020-09-23 14:44:51 +01:00
Zlatin Balevsky
94dd6101aa show sequential status and hopeless host count in plugin 2020-09-23 14:33:18 +01:00
Zlatin Balevsky
e65fbe1bd1 show sequential status and hopeless host count in download details panel 2020-09-23 14:26:02 +01:00
Zlatin Balevsky
964e315367 add a hopeless state for a download where all sources are hopeless 2020-09-23 14:17:40 +01:00
Zlatin Balevsky
140231e362 Give up on download sources after a number of attempts 2020-09-23 14:00:52 +01:00
Zlatin Balevsky
c73a821c67 put verified sources in the responder cache as well 2020-09-23 11:30:20 +01:00
Zlatin Balevsky
0ebe00b526 reduce hopeless interval to 1hr and purge interval to 24hrs 2020-09-22 16:08:17 +01:00
Zlatin Balevsky
b2a3bfce54 make sure we have the Persona of the altloc 2020-09-22 16:07:18 +01:00
Zlatin Balevsky
c490a511bd distinguish between discovered sources and verified sources. Only propagate and persist verified sources 2020-09-22 13:34:49 +01:00
Zlatin Balevsky
cbaa3470d2 make responder cache size configurable 2020-09-22 12:17:27 +01:00
Zlatin Balevsky
84d61fccd5 cache recent responders and always forward queries to them. Thanks to qtm for the idea 2020-09-21 15:28:15 +01:00
Zlatin Balevsky
a88db8f50f FixedSizeFIFOSet 2020-09-21 15:13:44 +01:00
Zlatin Balevsky
5a38154e15 make sure the pongs uuid matches the last sent ping uuid 2020-09-20 18:42:26 +01:00
Zlatin Balevsky
e5891de136 send and read up to 2 hosts per pong 2020-09-20 18:26:29 +01:00
Zlatin Balevsky
1e729bae1c implement forgetting of hopeless hosts after some time 2020-09-20 17:49:07 +01:00
Zlatin Balevsky
3e6e0c7e9f cache calls to System.currenTimeMillis() 2020-09-20 17:34:28 +01:00
Zlatin Balevsky
44af23c162 restrict forwarding of queries to sqrt of neighboring connections. Thanks to 'qtm' for the idea 2020-09-19 17:28:19 +01:00
Zlatin Balevsky
a262c99efe reduce limit on peer connections 2020-09-19 17:16:36 +01:00
33 changed files with 446 additions and 87 deletions

View File

@@ -4,7 +4,7 @@ The GitHub repo is mirrored from the in-I2P GitLab repo. Please open PRs and is
MuWire is an easy to use file-sharing program which offers anonymity using [I2P technology](http://geti2p.net). It works on any platform Java works on, including Windows,MacOS,Linux.
The current stable release - 0.7.1 is avaiable for download at https://muwire.com. The latest plugin build and instructions how to install the plugin are available inside I2P at http://muwire.i2p.
The current stable release - 0.7.4 is avaiable for download at https://muwire.com. The latest plugin build and instructions how to install the plugin are available inside I2P at http://muwire.i2p.
You can find technical documentation in the [doc] folder. Also check out the [Wiki] for various other documentation.
@@ -21,7 +21,7 @@ If you want to run the unit tests, type
./gradlew clean build
```
If you want to build binary bundles that do not depend on Java or I2P, see the [muwire-pkg] project
If you want to build binary bundles that do not depend on Java or I2P, see the [muwire-pkg] project. If you want to package MuWire for a Linux distribution, see the [Packaging] wiki page.
## Running the GUI
@@ -68,6 +68,7 @@ You can find the full key at https://keybase.io/zlatinb
[Wiki]: https://github.com/zlatinb/muwire/wiki
[doc]: https://github.com/zlatinb/muwire/tree/master/doc
[muwire-pkg]: https://github.com/zlatinb/muwire-pkg
[Packaging]: https://github.com/zlatinb/muwire/wiki/Packaging
[cli options]: https://github.com/zlatinb/muwire/wiki/CLI-Configuration-Options
[I2P Github]: https://github.com/i2p/i2p.i2p
[Plugin]: https://github.com/zlatinb/muwire/wiki/Plugin

View File

@@ -32,7 +32,7 @@ import com.muwire.core.UILoadedEvent
import com.muwire.core.files.AllFilesLoadedEvent
class CliLanterna {
private static final String MW_VERSION = "0.7.3"
private static final String MW_VERSION = "0.7.4"
private static volatile Core core

View File

@@ -23,8 +23,10 @@ import com.muwire.core.connection.I2PAcceptor
import com.muwire.core.connection.I2PConnector
import com.muwire.core.connection.LeafConnectionManager
import com.muwire.core.connection.UltrapeerConnectionManager
import com.muwire.core.download.DownloadHopelessEvent
import com.muwire.core.download.DownloadManager
import com.muwire.core.download.SourceDiscoveredEvent
import com.muwire.core.download.SourceVerifiedEvent
import com.muwire.core.download.UIDownloadCancelledEvent
import com.muwire.core.download.UIDownloadEvent
import com.muwire.core.download.UIDownloadPausedEvent
@@ -70,6 +72,7 @@ import com.muwire.core.hostcache.HostDiscoveredEvent
import com.muwire.core.mesh.MeshManager
import com.muwire.core.search.BrowseManager
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.ResponderCache
import com.muwire.core.search.ResultsEvent
import com.muwire.core.search.ResultsSender
import com.muwire.core.search.SearchEvent
@@ -284,6 +287,7 @@ public class Core {
log.info("initializing mesh manager")
MeshManager meshManager = new MeshManager(fileManager, home, props)
eventBus.register(SourceDiscoveredEvent.class, meshManager)
eventBus.register(SourceVerifiedEvent.class, meshManager)
log.info "initializing persistence service"
persisterService = new PersisterService(new File(home, "files.json"), eventBus, 60000, fileManager)
@@ -306,10 +310,17 @@ public class Core {
eventBus.register(HostDiscoveredEvent.class, hostCache)
eventBus.register(ConnectionEvent.class, hostCache)
log.info("initializing responder cache")
ResponderCache responderCache = new ResponderCache(props.responderCacheSize)
eventBus.register(UIResultBatchEvent.class, responderCache)
eventBus.register(SourceVerifiedEvent.class, responderCache)
log.info("initializing connection manager")
connectionManager = props.isLeaf() ?
new LeafConnectionManager(eventBus, me, 3, hostCache, props) :
new UltrapeerConnectionManager(eventBus, me, props.peerConnections, props.leafConnections, hostCache, trustService, props)
new UltrapeerConnectionManager(eventBus, me, props.peerConnections, props.leafConnections, hostCache, responderCache, trustService, props)
eventBus.register(TrustEvent.class, connectionManager)
eventBus.register(ConnectionEvent.class, connectionManager)
eventBus.register(DisconnectionEvent.class, connectionManager)
@@ -318,13 +329,13 @@ public class Core {
log.info("initializing cache client")
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
if (!props.plugin) {
if (!(props.plugin || props.disableUpdates)) {
log.info("initializing update client")
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props, fileManager, me, spk)
eventBus.register(FileDownloadedEvent.class, updateClient)
eventBus.register(UIResultBatchEvent.class, updateClient)
} else
log.info("running as plugin, not initializing update client")
log.info("running as plugin or updates disabled, not initializing update client")
log.info("initializing connector")
I2PConnector i2pConnector = new I2PConnector(i2pSocketManager)
@@ -381,6 +392,7 @@ public class Core {
eventBus.register(SourceDiscoveredEvent.class, downloadManager)
eventBus.register(UIDownloadPausedEvent.class, downloadManager)
eventBus.register(UIDownloadResumedEvent.class, downloadManager)
eventBus.register(DownloadHopelessEvent.class, downloadManager)
log.info("initializing upload manager")
uploadManager = new UploadManager(eventBus, fileManager, meshManager, downloadManager, persisterFolderService, props)
@@ -549,7 +561,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.7.3")
Core core = new Core(props, home, "0.7.4")
core.startServices()
// ... at the end, sleep or execute script

View File

@@ -16,7 +16,7 @@ class MuWireSettings {
boolean allowTrustLists
int trustListInterval
Set<Persona> trustSubscriptions
int downloadRetryInterval
int downloadRetryInterval, downloadMaxFailures
int totalUploadSlots
int uploadSlotsPerUser
int updateCheckInterval
@@ -44,17 +44,20 @@ class MuWireSettings {
int peerConnections
int leafConnections
int responderCacheSize
boolean startChatServer
int maxChatConnections
boolean advertiseChat
File chatWelcomeFile
Set<String> watchedDirectories
float downloadSequentialRatio
int hostClearInterval, hostHopelessInterval, hostRejectInterval
int hostClearInterval, hostHopelessInterval, hostRejectInterval, hostHopelessPurgeInterval
int meshExpiration
int speedSmoothSeconds
boolean embeddedRouter
boolean plugin
boolean disableUpdates
int inBw, outBw
Set<String> watchedKeywords
Set<String> watchedRegexes
@@ -78,6 +81,7 @@ class MuWireSettings {
if (incompleteLocationProp != null)
incompleteLocation = new File(incompleteLocationProp)
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","60"))
downloadMaxFailures = Integer.parseInt(props.getProperty("downloadMaxFailures","10"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","24"))
lastUpdateCheck = Long.parseLong(props.getProperty("lastUpdateChec","0"))
autoDownloadUpdate = Boolean.parseBoolean(props.getProperty("autoDownloadUpdate","true"))
@@ -86,11 +90,13 @@ class MuWireSettings {
shareHiddenFiles = Boolean.parseBoolean(props.getProperty("shareHiddenFiles","false"))
downloadSequentialRatio = Float.valueOf(props.getProperty("downloadSequentialRatio","0.8"))
hostClearInterval = Integer.valueOf(props.getProperty("hostClearInterval","15"))
hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "1440"))
hostHopelessInterval = Integer.valueOf(props.getProperty("hostHopelessInterval", "60"))
hostRejectInterval = Integer.valueOf(props.getProperty("hostRejectInterval", "1"))
hostHopelessPurgeInterval = Integer.valueOf(props.getProperty("hostHopelessPurgeInterval","1440"))
meshExpiration = Integer.valueOf(props.getProperty("meshExpiration","60"))
embeddedRouter = Boolean.valueOf(props.getProperty("embeddedRouter","false"))
plugin = Boolean.valueOf(props.getProperty("plugin","false"))
disableUpdates = Boolean.valueOf(props.getProperty("disableUpdates","false"))
inBw = Integer.valueOf(props.getProperty("inBw","256"))
outBw = Integer.valueOf(props.getProperty("outBw","128"))
searchComments = Boolean.valueOf(props.getProperty("searchComments","true"))
@@ -108,7 +114,10 @@ class MuWireSettings {
// ultrapeer connection settings
leafConnections = Integer.valueOf(props.getProperty("leafConnections","512"))
peerConnections = Integer.valueOf(props.getProperty("peerConnections","512"))
peerConnections = Integer.valueOf(props.getProperty("peerConnections","128"))
// responder cache settings
responderCacheSize = Integer.valueOf(props.getProperty("responderCacheSize","32"))
speedSmoothSeconds = Integer.valueOf(props.getProperty("speedSmoothSeconds","10"))
totalUploadSlots = Integer.valueOf(props.getProperty("totalUploadSlots","-1"))
@@ -148,6 +157,7 @@ class MuWireSettings {
if (incompleteLocation != null)
props.setProperty("incompleteLocation", incompleteLocation.getAbsolutePath())
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
props.setProperty("downloadMaxFailures", String.valueOf(downloadMaxFailures))
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
props.setProperty("lastUpdateCheck", String.valueOf(lastUpdateCheck))
props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
@@ -158,9 +168,11 @@ class MuWireSettings {
props.setProperty("hostClearInterval", String.valueOf(hostClearInterval))
props.setProperty("hostHopelessInterval", String.valueOf(hostHopelessInterval))
props.setProperty("hostRejectInterval", String.valueOf(hostRejectInterval))
props.setProperty("hostHopelessPurgeInterval", String.valueOf(hostHopelessPurgeInterval))
props.setProperty("meshExpiration", String.valueOf(meshExpiration))
props.setProperty("embeddedRouter", String.valueOf(embeddedRouter))
props.setProperty("plugin", String.valueOf(plugin))
props.setProperty("disableUpdates", String.valueOf(disableUpdates))
props.setProperty("inBw", String.valueOf(inBw))
props.setProperty("outBw", String.valueOf(outBw))
props.setProperty("searchComments", String.valueOf(searchComments))
@@ -180,6 +192,9 @@ class MuWireSettings {
props.setProperty("peerConnections", String.valueOf(peerConnections))
props.setProperty("leafConnections", String.valueOf(leafConnections))
// responder cache settings
props.setProperty("responderCacheSize", String.valueOf(responderCacheSize))
props.setProperty("speedSmoothSeconds", String.valueOf(speedSmoothSeconds))
props.setProperty("totalUploadSlots", String.valueOf(totalUploadSlots))
props.setProperty("uploadSlotsPerUser", String.valueOf(uploadSlotsPerUser))

View File

@@ -49,6 +49,8 @@ abstract class Connection implements Closeable {
protected final String name
long lastPingSentTime, lastPongReceivedTime
private volatile UUID lastPingUUID
Connection(EventBus eventBus, Endpoint endpoint, boolean incoming,
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
@@ -132,7 +134,8 @@ abstract class Connection implements Closeable {
def ping = [:]
ping.type = "Ping"
ping.version = 1
ping.uuid = UUID.randomUUID().toString()
lastPingUUID = UUID.randomUUID()
ping.uuid = lastPingUUID.toString()
messages.put(ping)
lastPingSentTime = System.currentTimeMillis()
}
@@ -168,7 +171,7 @@ abstract class Connection implements Closeable {
pong.version = 1
if (ping.uuid != null)
pong.uuid = ping.uuid
pong.pongs = hostCache.getGoodHosts(10).collect { d -> d.toBase64() }
pong.pongs = hostCache.getGoodHosts(2).collect { d -> d.toBase64() }
messages.put(pong)
}
@@ -177,7 +180,23 @@ abstract class Connection implements Closeable {
lastPongReceivedTime = System.currentTimeMillis()
if (pong.pongs == null)
throw new Exception("Pong doesn't have pongs")
pong.pongs.stream().limit(10).forEach {
if (lastPingUUID == null) {
log.fine "$name received an unexpected pong"
return
}
if (pong.uuid == null) {
log.fine "$name pong doesn't have uuid"
return
}
UUID pongUUID = UUID.fromString(pong.uuid)
if (pongUUID != lastPingUUID) {
log.fine "$name ping/pong uuid mismatch"
return
}
lastPingUUID = null
pong.pongs.stream().limit(2).forEach {
def dest = new Destination(it)
eventBus.publish(new HostDiscoveredEvent(destination: dest))
}

View File

@@ -8,6 +8,7 @@ import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.hostcache.HostCache
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.ResponderCache
import com.muwire.core.trust.TrustService
import groovy.util.logging.Log
@@ -18,18 +19,22 @@ class UltrapeerConnectionManager extends ConnectionManager {
final int maxPeers, maxLeafs
final TrustService trustService
final ResponderCache responderCache
final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap()
final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap()
private final Random random = new Random()
UltrapeerConnectionManager() {}
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
HostCache hostCache, TrustService trustService, MuWireSettings settings) {
HostCache hostCache, ResponderCache responderCache, TrustService trustService, MuWireSettings settings) {
super(eventBus, me, hostCache, settings)
this.maxPeers = maxPeers
this.maxLeafs = maxLeafs
this.trustService = trustService
this.responderCache = responderCache
}
@Override
public void drop(Destination d) {
@@ -44,8 +49,18 @@ class UltrapeerConnectionManager extends ConnectionManager {
if (e.replyTo != me.destination && e.receivedOn != me.destination &&
!leafConnections.containsKey(e.receivedOn))
e.firstHop = false
final int connCount = peerConnections.size()
if (connCount == 0)
return
final int treshold = (int)(Math.sqrt(connCount)) + 1
peerConnections.values().each {
if (e.getReceivedOn() != it.getEndpoint().getDestination())
// 1. do not send query back to originator
// 2. if firstHop forward to everyone
// 3. otherwise to everyone who has recently responded/transferred to us + randomized sqrt of neighbors
if (e.getReceivedOn() != it.getEndpoint().getDestination() &&
(e.firstHop ||
responderCache.hasResponded(it.endpoint.destination) ||
random.nextInt(connCount) < treshold))
it.sendQuery(e)
}
}

View File

@@ -0,0 +1,7 @@
package com.muwire.core.download
import com.muwire.core.Event
class DownloadHopelessEvent extends Event {
Downloader downloader
}

View File

@@ -100,7 +100,7 @@ public class DownloadManager {
Pieces pieces = getPieces(infoHash, size, pieceSize, sequential)
def downloader = new Downloader(eventBus, this, chatServer, me, target, size,
infoHash, pieceSize, connector, destinations,
incompletes, pieces)
incompletes, pieces, muSettings.downloadMaxFailures)
downloaders.put(infoHash, downloader)
persistDownloaders()
executor.execute({downloader.download()} as Runnable)
@@ -112,6 +112,11 @@ public class DownloadManager {
persistDownloaders()
}
public void onDownloadHopelessEvent(DownloadHopelessEvent e) {
downloaders.remove(e.downloader.infoHash)
persistDownloaders()
}
public void onUIDownloadPausedEvent(UIDownloadPausedEvent e) {
persistDownloaders()
}
@@ -163,7 +168,7 @@ public class DownloadManager {
Pieces pieces = getPieces(infoHash, (long)json.length, json.pieceSizePow2, sequential)
def downloader = new Downloader(eventBus, this, chatServer, me, file, (long)json.length,
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces)
infoHash, json.pieceSizePow2, connector, destinations, incompletes, pieces, muSettings.downloadMaxFailures)
if (json.paused != null)
downloader.paused = json.paused

View File

@@ -215,6 +215,8 @@ class DownloadSession {
pieces.markPartial(piece, 0)
throw new BadHashException("bad hash on piece $piece")
}
eventBus.publish(new SourceVerifiedEvent(infoHash : infoHash, source : endpoint.destination))
} finally {
try { channel?.close() } catch (IOException ignore) {}
DataUtil.tryUnmap(mapped)

View File

@@ -31,7 +31,7 @@ import net.i2p.util.ConcurrentHashSet
@Log
public class Downloader {
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, CANCELLED, PAUSED, FINISHED }
public enum DownloadState { CONNECTING, HASHLIST, DOWNLOADING, FAILED, HOPELESS, CANCELLED, PAUSED, FINISHED }
private enum WorkerState { CONNECTING, HASHLIST, DOWNLOADING, FINISHED}
private static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
@@ -59,10 +59,14 @@ public class Downloader {
final int pieceSizePow2
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
private final Set<Destination> successfulDestinations = new ConcurrentHashSet<>()
/** LOCKING: itself */
private final Map<Destination, Integer> failingDestinations = new HashMap<>()
private final int maxFailures
private volatile boolean cancelled, paused
private final AtomicBoolean eventFired = new AtomicBoolean()
private final AtomicBoolean hopelessEventFired = new AtomicBoolean()
private boolean piecesFileClosed
private final AtomicLong dataSinceLastRead = new AtomicLong(0)
@@ -74,7 +78,7 @@ public class Downloader {
public Downloader(EventBus eventBus, DownloadManager downloadManager, ChatServer chatServer,
Persona me, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
File incompletes, Pieces pieces) {
File incompletes, Pieces pieces, int maxFailures) {
this.eventBus = eventBus
this.me = me
this.downloadManager = downloadManager
@@ -91,6 +95,7 @@ public class Downloader {
this.pieceSize = 1 << pieceSizePow2
this.pieces = pieces
this.nPieces = pieces.nPieces
this.maxFailures = maxFailures
}
public synchronized InfoHash getInfoHash() {
@@ -120,7 +125,7 @@ public class Downloader {
void download() {
readPieces()
destinations.each {
if (it != me.destination) {
if (it != me.destination && !isHopeless(it)) {
def worker = new DownloadWorker(it)
activeWorkers.put(it, worker)
executorService.submit(worker)
@@ -210,6 +215,8 @@ public class Downloader {
if (allFinished) {
if (pieces.isComplete())
return DownloadState.FINISHED
if (!hasLiveSources())
return DownloadState.HOPELESS
return DownloadState.FAILED
}
@@ -273,11 +280,22 @@ public class Downloader {
public int getTotalWorkers() {
return activeWorkers.size();
}
public int countHopelessSources() {
synchronized(failingDestinations) {
return destinations.count { isHopeless(it)}
}
}
private boolean hasLiveSources() {
destinations.size() > countHopelessSources()
}
public void resume() {
paused = false
readPieces()
destinations.each { destination ->
destinations.stream().filter({!isHopeless(it)}).forEach { destination ->
log.fine("resuming source ${destination.toBase32()}")
def worker = activeWorkers.get(destination)
if (worker != null) {
if (worker.currentState == WorkerState.FINISHED) {
@@ -294,8 +312,9 @@ public class Downloader {
}
void addSource(Destination d) {
if (activeWorkers.containsKey(d))
if (activeWorkers.containsKey(d) || isHopeless(d))
return
destinations.add(d)
DownloadWorker newWorker = new DownloadWorker(d)
activeWorkers.put(d, newWorker)
executorService.submit(newWorker)
@@ -351,6 +370,28 @@ public class Downloader {
try {os?.close() } catch (IOException ignore) {}
}
}
private boolean isHopeless(Destination d) {
if (maxFailures < 0)
return false
synchronized(failingDestinations) {
return !successfulDestinations.contains(d) &&
failingDestinations.containsKey(d) &&
failingDestinations[d] >= maxFailures
}
}
private void markFailed(Destination d) {
log.fine("marking failed ${d.toBase32()}")
synchronized(failingDestinations) {
Integer count = failingDestinations.get(d)
if (count == null) {
failingDestinations.put(d, 1)
} else {
failingDestinations.put(d, count + 1)
}
}
}
class DownloadWorker implements Runnable {
private final Destination destination
@@ -395,6 +436,9 @@ public class Downloader {
}
} catch (Exception bad) {
log.log(Level.WARNING,"Exception while downloading",DataUtil.findRoot(bad))
markFailed(destination)
if (!hasLiveSources() && hopelessEventFired.compareAndSet(false, true))
eventBus.publish(new DownloadHopelessEvent(downloader : Downloader.this))
} finally {
writePieces()
currentState = WorkerState.FINISHED

View File

@@ -0,0 +1,11 @@
package com.muwire.core.download
import com.muwire.core.Event
import com.muwire.core.InfoHash
import net.i2p.data.Destination
class SourceVerifiedEvent extends Event {
InfoHash infoHash
Destination source
}

View File

@@ -7,17 +7,19 @@ class Host {
private static final int MAX_FAILURES = 3
final Destination destination
private final int clearInterval, hopelessInterval, rejectionInterval
private final int clearInterval, hopelessInterval, rejectionInterval, purgeInterval
int failures,successes
long lastAttempt
long lastSuccessfulAttempt
long lastRejection
public Host(Destination destination, int clearInterval, int hopelessInterval, int rejectionInterval) {
public Host(Destination destination, int clearInterval, int hopelessInterval, int rejectionInterval,
int purgeInterval) {
this.destination = destination
this.clearInterval = clearInterval
this.hopelessInterval = hopelessInterval
this.rejectionInterval = rejectionInterval
this.purgeInterval = purgeInterval
}
private void connectSuccessful() {
@@ -54,17 +56,22 @@ class Host {
failures = 0
}
synchronized boolean canTryAgain() {
synchronized boolean canTryAgain(final long now) {
lastSuccessfulAttempt > 0 &&
System.currentTimeMillis() - lastAttempt > (clearInterval * 60 * 1000)
now - lastAttempt > (clearInterval * 60 * 1000)
}
synchronized boolean isHopeless() {
synchronized boolean isHopeless(final long now) {
isFailed() &&
System.currentTimeMillis() - lastSuccessfulAttempt > (hopelessInterval * 60 * 1000)
now - lastSuccessfulAttempt > (hopelessInterval * 60 * 1000)
}
synchronized boolean isRecentlyRejected() {
System.currentTimeMillis() - lastRejection < (rejectionInterval * 60 * 1000)
synchronized boolean isRecentlyRejected(final long now) {
now - lastRejection < (rejectionInterval * 60 * 1000)
}
synchronized boolean shouldBeForgotten(final long now) {
isHopeless(now) &&
now - lastAttempt > (purgeInterval * 60 * 1000)
}
}

View File

@@ -52,7 +52,8 @@ class HostCache extends Service {
hosts.get(e.destination).clearFailures()
return
}
Host host = new Host(e.destination, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
Host host = new Host(e.destination, settings.hostClearInterval, settings.hostHopelessInterval,
settings.hostRejectInterval, settings.hostHopelessPurgeInterval)
if (allowHost(host)) {
hosts.put(e.destination, host)
}
@@ -64,7 +65,8 @@ class HostCache extends Service {
Destination dest = e.endpoint.destination
Host host = hosts.get(dest)
if (host == null) {
host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval,
settings.hostRejectInterval, settings.hostHopelessPurgeInterval)
hosts.put(dest, host)
}
@@ -84,9 +86,10 @@ class HostCache extends Service {
List<Destination> getHosts(int n) {
List<Destination> rv = new ArrayList<>(hosts.keySet())
rv.retainAll {allowHost(hosts[it])}
final long now = System.currentTimeMillis()
rv.removeAll {
def h = hosts[it];
(h.isFailed() && !h.canTryAgain()) || h.isRecentlyRejected() || h.isHopeless()
(h.isFailed() && !h.canTryAgain(now)) || h.isRecentlyRejected(now) || h.isHopeless(now)
}
if (rv.size() <= n)
return rv
@@ -116,8 +119,9 @@ class HostCache extends Service {
int countHopelessHosts() {
List<Destination> rv = new ArrayList<>(hosts.keySet())
final long now = System.currentTimeMillis()
rv.retainAll {
hosts[it].isHopeless()
hosts[it].isHopeless(now)
}
rv.size()
}
@@ -128,7 +132,8 @@ class HostCache extends Service {
storage.eachLine {
def entry = slurper.parseText(it)
Destination dest = new Destination(entry.destination)
Host host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval, settings.hostRejectInterval)
Host host = new Host(dest, settings.hostClearInterval, settings.hostHopelessInterval,
settings.hostRejectInterval, settings.hostHopelessPurgeInterval)
host.failures = Integer.valueOf(String.valueOf(entry.failures))
host.successes = Integer.valueOf(String.valueOf(entry.successes))
if (entry.lastAttempt != null)
@@ -161,10 +166,12 @@ class HostCache extends Service {
}
private void save() {
final long now = System.currentTimeMillis()
hosts.keySet().removeAll { hosts[it].shouldBeForgotten(now) }
storage.delete()
storage.withPrintWriter { writer ->
hosts.each { dest, host ->
if (allowHost(host) && !host.isHopeless()) {
if (allowHost(host) && !host.isHopeless(now)) {
def map = [:]
map.destination = dest.toBase64()
map.failures = host.failures

View File

@@ -1,15 +1,29 @@
package com.muwire.core.mesh
import java.util.concurrent.ConcurrentHashMap
import java.util.stream.Collectors
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.download.Pieces
import com.muwire.core.util.DataUtil
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet
/**
* Representation of a download mesh.
*
* Two data structures - collection of known sources and collection of sources
* we have successfully transferred data with.
*
* @author zab
*/
class Mesh {
private final InfoHash infoHash
private final Set<Persona> sources = new ConcurrentHashSet<>()
private final Map<Destination,Persona> sources = new HashMap<>()
private final Set<Destination> verified = new HashSet<>()
final Pieces pieces
Mesh(InfoHash infoHash, Pieces pieces) {
@@ -17,12 +31,38 @@ class Mesh {
this.pieces = pieces
}
Set<Persona> getRandom(int n, Persona exclude) {
List<Persona> tmp = new ArrayList<>(sources)
tmp.remove(exclude)
synchronized Set<Persona> getRandom(int n, Persona exclude) {
List<Destination> tmp = new ArrayList<>(verified)
if (exclude != null)
tmp.remove(exclude.destination)
tmp.retainAll(sources.keySet()) // verified may contain nodes not in sources
Collections.shuffle(tmp)
if (tmp.size() < n)
return tmp
tmp[0..n-1]
if (tmp.size() > n)
tmp = tmp[0..n-1]
tmp.collect(new HashSet<>(), { sources[it] })
}
synchronized void add(Persona persona) {
sources.put(persona.destination, persona)
}
synchronized void verify(Destination d) {
verified.add(d)
}
synchronized def toJson() {
def json = [:]
json.timestamp = System.currentTimeMillis()
json.infoHash = Base64.encode(infoHash.getRoot())
Set<Persona> toPersist = new HashSet<>(sources.values())
toPersist.retainAll { verified.contains(it.destination) }
json.sources = toPersist.collect {it.toBase64()}
json.nPieces = pieces.nPieces
List<Integer> downloaded = pieces.getDownloaded()
if( downloaded.size() > pieces.nPieces)
return null
json.xHave = DataUtil.encodeXHave(downloaded, pieces.nPieces)
json
}
}

View File

@@ -9,6 +9,7 @@ import com.muwire.core.MuWireSettings
import com.muwire.core.Persona
import com.muwire.core.download.Pieces
import com.muwire.core.download.SourceDiscoveredEvent
import com.muwire.core.download.SourceVerifiedEvent
import com.muwire.core.files.FileManager
import com.muwire.core.util.DataUtil
@@ -56,25 +57,25 @@ class MeshManager {
Mesh mesh = meshes.get(e.infoHash)
if (mesh == null)
return
mesh.sources.add(e.source)
save()
mesh.add(e.source)
}
void onSourceVerifiedEvent(SourceVerifiedEvent e) {
Mesh mesh = meshes.get(e.infoHash)
if (mesh == null)
return
mesh.verify(e.source)
save()
}
private void save() {
File meshFile = new File(home, "mesh.json")
synchronized(meshes) {
meshFile.withPrintWriter { writer ->
meshes.values().each { mesh ->
def json = [:]
json.timestamp = System.currentTimeMillis()
json.infoHash = Base64.encode(mesh.infoHash.getRoot())
json.sources = mesh.sources.stream().map({it.toBase64()}).collect(Collectors.toList())
json.nPieces = mesh.pieces.nPieces
List<Integer> downloaded = mesh.pieces.getDownloaded()
if( downloaded.size() > mesh.pieces.nPieces)
return
json.xHave = DataUtil.encodeXHave(downloaded, mesh.pieces.nPieces)
writer.println(JsonOutput.toJson(json))
def json = mesh.toJson()
if (json != null)
writer.println(JsonOutput.toJson(json))
}
}
}
@@ -99,7 +100,8 @@ class MeshManager {
Mesh mesh = new Mesh(infoHash, pieces)
json.sources.each { source ->
Persona persona = new Persona(new ByteArrayInputStream(Base64.decode(source)))
mesh.sources.add(persona)
mesh.add(persona)
mesh.verify(persona.destination) // assume if persisted it was verified
}
if (json.xHave != null) {

View File

@@ -0,0 +1,30 @@
package com.muwire.core.search
import com.muwire.core.download.SourceVerifiedEvent
import com.muwire.core.util.FixedSizeFIFOSet
import net.i2p.data.Destination
/**
* Caches destinations that have recently responded to with results.
*/
class ResponderCache {
private final FixedSizeFIFOSet<Destination> cache
ResponderCache(int capacity) {
cache = new FixedSizeFIFOSet<>(capacity)
}
synchronized void onUIResultBatchEvent(UIResultBatchEvent e) {
cache.add(e.results[0].sender.destination)
}
synchronized void onSourceVerifiedEvent(SourceVerifiedEvent e) {
cache.add(e.source)
}
synchronized boolean hasResponded(Destination d) {
cache.contains(d)
}
}

View File

@@ -11,6 +11,7 @@ import com.muwire.core.connection.Endpoint
import com.muwire.core.download.DownloadManager
import com.muwire.core.download.Downloader
import com.muwire.core.download.SourceDiscoveredEvent
import com.muwire.core.download.SourceVerifiedEvent
import com.muwire.core.files.FileManager
import com.muwire.core.files.PersisterFolderService
import com.muwire.core.mesh.Mesh
@@ -123,6 +124,7 @@ public class UploadManager {
eventBus.publish(new UploadEvent(uploader : uploader))
try {
uploader.respond()
eventBus.publish(new SourceVerifiedEvent(infoHash : request.infoHash, source : request.downloader.destination))
} finally {
decrementUploads(request.downloader)
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
@@ -259,6 +261,7 @@ public class UploadManager {
eventBus.publish(new UploadEvent(uploader : uploader))
try {
uploader.respond()
eventBus.publish(new SourceVerifiedEvent(infoHash : request.infoHash, source : request.downloader.destination))
} finally {
eventBus.publish(new UploadFinishedEvent(uploader : uploader))
}

View File

@@ -0,0 +1,35 @@
package com.muwire.core.util;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Set;
public class FixedSizeFIFOSet<T> {
private final int capacity;
private final Set<T> set = new HashSet<>();
private final Deque<T> fifo = new ArrayDeque<>();
public FixedSizeFIFOSet(final int capacity) {
this.capacity = capacity;
}
public boolean contains(T element) {
return set.contains(element);
}
public void add(T element) {
if (!set.contains(element)) {
if (set.size() == capacity) {
T toRemove = fifo.removeLast();
set.remove(toRemove);
}
fifo.addFirst(element);
set.add(element);
} else {
fifo.remove(element);
fifo.addFirst(element);
}
}
}

View File

@@ -75,6 +75,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
@@ -97,6 +98,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
@@ -114,6 +116,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
@@ -136,6 +139,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -160,6 +164,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 100 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -182,6 +187,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -211,6 +217,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -246,6 +253,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -266,6 +274,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
cache.onHostDiscoveredEvent(new HostDiscoveredEvent(destination: destinations.dest1))
@@ -301,6 +310,7 @@ class HostCacheTest {
settingsMock.ignore.getHostClearInterval { 0 }
settingsMock.ignore.getHostHopelessInterval { 0 }
settingsMock.ignore.getHostRejectInterval { 0 }
settingsMock.ignore.getHostHopelessPurgeInterval { 0 }
initMocks()
def rv = cache.getHosts(5)

View File

@@ -0,0 +1,49 @@
package com.muwire.core.util
import org.junit.Test
class FixedSizeFIFOSetTest {
@Test
public void testFifo() {
FixedSizeFIFOSet<String> fifoSet = new FixedSizeFIFOSet(3);
fifoSet.add("a")
assert fifoSet.contains("a")
fifoSet.add("b")
assert fifoSet.contains("a")
assert fifoSet.contains("b")
fifoSet.add("c")
assert fifoSet.contains("a")
assert fifoSet.contains("b")
assert fifoSet.contains("c")
fifoSet.add("d")
assert !fifoSet.contains("a")
assert fifoSet.contains("b")
assert fifoSet.contains("c")
assert fifoSet.contains("d")
}
@Test
public void testDuplicateElement() {
FixedSizeFIFOSet<String> fifoSet = new FixedSizeFIFOSet(3);
fifoSet.add("a")
fifoSet.add("b")
fifoSet.add("c")
fifoSet.add("a")
assert fifoSet.contains("a")
assert fifoSet.contains("b")
assert fifoSet.contains("c")
fifoSet.add("d")
assert fifoSet.contains("a")
assert !fifoSet.contains("b")
assert fifoSet.contains("c")
assert fifoSet.contains("d")
}
}

View File

@@ -1,5 +1,5 @@
group = com.muwire
version = 0.7.3
version = 0.7.4
i2pVersion = 0.9.47
groovyVersion = 3.0.4
slf4jVersion = 1.7.25

View File

@@ -222,17 +222,13 @@ class MainFrameController {
@ControllerAction
void clear() {
def toRemove = []
model.downloads.each {
if (it.downloader.getCurrentState() == Downloader.DownloadState.CANCELLED) {
toRemove << it
} else if (it.downloader.getCurrentState() == Downloader.DownloadState.FINISHED) {
toRemove << it
}
}
toRemove.each {
model.downloads.remove(it)
}
model.downloads.removeAll {
def state = it.downloader.getCurrentState()
state == Downloader.DownloadState.CANCELLED ||
state == Downloader.DownloadState.FINISHED ||
state == Downloader.DownloadState.HOPELESS
}
model.clearButtonEnabled = false
}

View File

@@ -59,8 +59,11 @@ class OptionsController {
text = view.retryField.text
model.downloadRetryInterval = text
settings.downloadRetryInterval = Integer.valueOf(text)
text = view.downloadMaxFailuresField.text
model.downloadMaxFailures = text
settings.downloadMaxFailures = Integer.valueOf(text)
text = view.updateField.text
model.updateCheckInterval = text

View File

@@ -50,6 +50,9 @@ class Ready extends AbstractLifecycleHandler {
props = new MuWireSettings(props)
if (props.incompleteLocation == null)
props.incompleteLocation = new File(home, "incompletes")
if (System.getProperties().containsKey("disableUpdates"))
props.disableUpdates = Boolean.valueOf(System.getProperty("disableUpdates"))
} else {
log.info("creating new properties")
props = new MuWireSettings()
@@ -88,6 +91,7 @@ class Ready extends AbstractLifecycleHandler {
props.embeddedRouter = embeddedRouterAvailable
props.updateType = System.getProperty("updateType","jar")
props.disableUpdates = Boolean.parseBoolean(System.getProperty("disableUpdates", "false"))
propsFile.withPrintWriter("UTF-8", {

View File

@@ -177,22 +177,25 @@ class MainFrameModel {
if (!mvcGroup.alive)
return
// remove cancelled or finished downloads
// remove cancelled or finished or hopeless downloads
if (!clearButtonEnabled || uiSettings.clearCancelledDownloads || uiSettings.clearFinishedDownloads) {
def toRemove = []
downloads.each {
if (it.downloader.getCurrentState() == Downloader.DownloadState.CANCELLED) {
def state = it.downloader.getCurrentState()
if (state == Downloader.DownloadState.CANCELLED) {
if (uiSettings.clearCancelledDownloads) {
toRemove << it
} else {
clearButtonEnabled = true
}
} else if (it.downloader.getCurrentState() == Downloader.DownloadState.FINISHED) {
} else if (state == Downloader.DownloadState.FINISHED) {
if (uiSettings.clearFinishedDownloads) {
toRemove << it
} else {
clearButtonEnabled = true
}
} else if (state == Downloader.DownloadState.HOPELESS) {
clearButtonEnabled = true
}
}
toRemove.each {

View File

@@ -12,6 +12,7 @@ import java.awt.Font
@ArtifactProviderFor(GriffonModel)
class OptionsModel {
@Observable String downloadRetryInterval
@Observable String downloadMaxFailures
@Observable String updateCheckInterval
@Observable boolean autoDownloadUpdate
@Observable boolean shareDownloadedFiles
@@ -74,10 +75,13 @@ class OptionsModel {
@Observable boolean advertiseChat
@Observable int maxChatLines
@Observable String chatWelcomeFile
boolean disableUpdates
void mvcGroupInit(Map<String, String> args) {
MuWireSettings settings = application.context.get("muwire-settings")
downloadRetryInterval = settings.downloadRetryInterval
downloadMaxFailures = settings.downloadMaxFailures
updateCheckInterval = settings.updateCheckInterval
autoDownloadUpdate = settings.autoDownloadUpdate
shareDownloadedFiles = settings.shareDownloadedFiles
@@ -137,5 +141,7 @@ class OptionsModel {
advertiseChat = settings.advertiseChat
maxChatLines = uiSettings.maxChatLines
chatWelcomeFile = settings.chatWelcomeFile?.getAbsolutePath()
disableUpdates = settings.disableUpdates
}
}

View File

@@ -261,10 +261,14 @@ class MainFrameView {
constraints: gbc(gridx:1, gridy:0, gridwidth: 2, insets : [0,0,0,20]))
label(text : "Piece Size", constraints : gbc(gridx: 0, gridy:1))
label(text : bind {model.downloader?.pieceSize}, constraints : gbc(gridx:1, gridy:1))
label(text : "Sequential", constraints : gbc(gridx: 0, gridy: 2))
label(text : bind {model.downloader?.isSequential()}, constraints : gbc(gridx:1, gridy:2, insets : [0,0,0,20]))
label(text : "Known Sources:", constraints : gbc(gridx:3, gridy: 0))
label(text : bind {model.downloader?.activeWorkers?.size()}, constraints : gbc(gridx:4, gridy:0, insets : [0,0,0,20]))
label(text : "Active Sources:", constraints : gbc(gridx:3, gridy:1))
label(text : bind {model.downloader?.activeWorkers()}, constraints : gbc(gridx:4, gridy:1, insets : [0,0,0,20]))
label(text : "Hopeless Sources:", constraints : gbc(gridx:3, gridy:2))
label(text : bind {model.downloader?.countHopelessSources()}, constraints : gbc(gridx:4, gridy:2, insets : [0,0,0,20]))
label(text : "Total Pieces:", constraints : gbc(gridx:5, gridy: 0))
label(text : bind {model.downloader?.nPieces}, constraints : gbc(gridx:6, gridy:0, insets : [0,0,0,20]))
label(text : "Done Pieces:", constraints: gbc(gridx:5, gridy: 1))

View File

@@ -39,6 +39,7 @@ class OptionsView {
def chat
def retryField
def downloadMaxFailuresField
def updateField
def autoDownloadUpdateCheckbox
def shareDownloadedCheckbox
@@ -123,13 +124,17 @@ class OptionsView {
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2,
constraints : gbc(gridx: 2, gridy: 0, anchor : GridBagConstraints.LINE_END, weightx: 0))
label(text : "Save downloaded files to:", constraints: gbc(gridx:0, gridy:1, anchor : GridBagConstraints.LINE_START))
label(text : bind {model.downloadLocation}, constraints: gbc(gridx:1, gridy:1, anchor : GridBagConstraints.LINE_START))
button(text : "Choose", constraints : gbc(gridx : 2, gridy:1), downloadLocationAction)
label(text : "Give up on sources after this many failures (-1 means never)", constraints: gbc(gridx: 0, gridy: 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
downloadMaxFailuresField = textField(text : bind { model.downloadMaxFailures }, columns : 2,
constraints : gbc(gridx: 2, gridy: 1, anchor : GridBagConstraints.LINE_END, weightx: 0))
label(text : "Store incomplete files in:", constraints: gbc(gridx:0, gridy:2, anchor : GridBagConstraints.LINE_START))
label(text : bind {model.incompleteLocation}, constraints: gbc(gridx:1, gridy:2, anchor : GridBagConstraints.LINE_START))
button(text : "Choose", constraints : gbc(gridx : 2, gridy:2), incompleteLocationAction)
label(text : "Save downloaded files to:", constraints: gbc(gridx:0, gridy:2, anchor : GridBagConstraints.LINE_START))
label(text : bind {model.downloadLocation}, constraints: gbc(gridx:1, gridy:2, anchor : GridBagConstraints.LINE_START))
button(text : "Choose", constraints : gbc(gridx : 2, gridy:2), downloadLocationAction)
label(text : "Store incomplete files in:", constraints: gbc(gridx:0, gridy:3, anchor : GridBagConstraints.LINE_START))
label(text : bind {model.incompleteLocation}, constraints: gbc(gridx:1, gridy:3, anchor : GridBagConstraints.LINE_START))
button(text : "Choose", constraints : gbc(gridx : 2, gridy:3), incompleteLocationAction)
}
panel (border : titledBorder(title : "Upload Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
@@ -153,16 +158,18 @@ class OptionsView {
shareHiddenCheckbox = checkBox(selected : bind {model.shareHiddenFiles}, constraints : gbc(gridx :1, gridy:1, weightx : 0))
}
panel (border : titledBorder(title : "Update Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
if (!model.disableUpdates) {
panel (border : titledBorder(title : "Update Settings", border : etchedBorder(), titlePosition : TitledBorder.TOP,
constraints : gbc(gridx : 0, gridy : 4, fill : GridBagConstraints.HORIZONTAL))) {
gridBagLayout()
label(text : "Check for updates every (hours)", constraints : gbc(gridx : 0, gridy: 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 0, weightx: 0))
gridBagLayout()
label(text : "Check for updates every (hours)", constraints : gbc(gridx : 0, gridy: 0, anchor : GridBagConstraints.LINE_START, weightx : 100))
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 0, weightx: 0))
label(text : "Download updates automatically", constraints: gbc(gridx :0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
autoDownloadUpdateCheckbox = checkBox(selected : bind {model.autoDownloadUpdate},
label(text : "Download updates automatically", constraints: gbc(gridx :0, gridy : 1, anchor : GridBagConstraints.LINE_START, weightx: 100))
autoDownloadUpdateCheckbox = checkBox(selected : bind {model.autoDownloadUpdate},
constraints : gbc(gridx:1, gridy : 1, anchor : GridBagConstraints.LINE_END))
}
}
}
i = builder.panel {

View File

@@ -23,6 +23,7 @@ public class ConfigurationServlet extends HttpServlet {
static {
INPUT_VALIDATORS.put("trustListInterval", new PositiveIntegerValidator("Trust list update frequency (hours)"));
INPUT_VALIDATORS.put("downloadRetryInterval", new PositiveIntegerValidator("Download retry frequency (seconds)"));
INPUT_VALIDATORS.put("downloadMaxFailures", new IntegerValidator("Give up on sources after this many failures (-1 means never)"));
INPUT_VALIDATORS.put("totalUploadSlots", new IntegerValidator("Total upload slots (-1 means unlimited)"));
INPUT_VALIDATORS.put("uploadSlotsPerUser", new IntegerValidator("Upload slots per user (-1 means unlimited)"));
INPUT_VALIDATORS.put("downloadLocation", new DirectoryValidator());
@@ -92,6 +93,7 @@ public class ConfigurationServlet extends HttpServlet {
case "allowTrustLists": core.getMuOptions().setAllowTrustLists(true); break;
case "trustListInterval" : core.getMuOptions().setTrustListInterval(Integer.parseInt(value)); break;
case "downloadRetryInterval" : core.getMuOptions().setDownloadRetryInterval(Integer.parseInt(value)); break;
case "downloadMaxFailures" : core.getMuOptions().setDownloadMaxFailures(Integer.parseInt(value)); break;
case "totalUploadSlots" : core.getMuOptions().setTotalUploadSlots(Integer.parseInt(value)); break;
case "uploadSlotsPerUser" : core.getMuOptions().setUploadSlotsPerUser(Integer.parseInt(value)); break;
case "downloadLocation" : core.getMuOptions().setDownloadLocation(getDirectory(value)); break;

View File

@@ -63,7 +63,8 @@ public class DownloadManager {
Map.Entry<InfoHash, Downloader> entry = iter.next();
Downloader.DownloadState state = entry.getValue().getCurrentState();
if (state == Downloader.DownloadState.CANCELLED ||
state == Downloader.DownloadState.FINISHED)
state == Downloader.DownloadState.FINISHED ||
state == Downloader.DownloadState.HOPELESS)
iter.remove();
}
}

View File

@@ -104,8 +104,10 @@ public class DownloadServlet extends HttpServlet {
sb.append("<Details>");
sb.append("<Path>").append(Util.escapeHTMLinXML(downloader.getFile().getAbsolutePath())).append("</Path>");
sb.append("<PieceSize>").append(downloader.getPieceSize()).append("</PieceSize>");
sb.append("<Sequential>").append(downloader.isSequential()).append("</Sequential>");
sb.append("<KnownSources>").append(downloader.getTotalWorkers()).append("</KnownSources>");
sb.append("<ActiveSources>").append(downloader.activeWorkers()).append("</ActiveSources>");
sb.append("<HopelessSources>").append(downloader.countHopelessSources()).append("</HopelessSources>");
sb.append("<TotalPieces>").append(downloader.getNPieces()).append("</TotalPieces>");
sb.append("<DonePieces>").append(downloader.donePieces()).append("</DonePieces>");
sb.append("</Details>");

View File

@@ -42,7 +42,7 @@ class Downloader {
}
getPauseResumeRetryBlock() {
if (this.state == "FINISHED" || this.state == "CANCELLED")
if (this.state == "FINISHED" || this.state == "CANCELLED" || this.state == "HOPELESS")
return ""
if (this.state == "FAILED") {
var retryLink = new Link(_t("Retry"), "resumeDownload", [this.infoHash])
@@ -105,8 +105,10 @@ function updateDownloader(infoHash) {
if (this.readyState == 4 && this.status == 200) {
var path = this.responseXML.getElementsByTagName("Path")[0].childNodes[0].nodeValue
var pieceSize = this.responseXML.getElementsByTagName("PieceSize")[0].childNodes[0].nodeValue
var sequential = this.responseXML.getElementsByTagName("Sequential")[0].childNodes[0].nodeValue
var knownSources = this.responseXML.getElementsByTagName("KnownSources")[0].childNodes[0].nodeValue
var activeSources = this.responseXML.getElementsByTagName("ActiveSources")[0].childNodes[0].nodeValue
var hopelessSources = this.responseXML.getElementsByTagName("HopelessSources")[0].childNodes[0].nodeValue
var totalPieces = this.responseXML.getElementsByTagName("TotalPieces")[0].childNodes[0].nodeValue
var donePieces = this.responseXML.getElementsByTagName("DonePieces")[0].childNodes[0].nodeValue
@@ -116,6 +118,10 @@ function updateDownloader(infoHash) {
html += "<td>" + "<p align='right'>" + path + "</p>" + "</td>"
html += "</tr>"
html += "<tr>"
html += "<td>" + _t("Sequential") + "</td>"
html += "<td>" + "<p align='right'>" + sequential + "</p>" + "</td>"
html += "</tr>"
html += "<tr>"
html += "<td>" + _t("Known Sources") + "</td>"
html += "<td>" + "<p align='right'>" + knownSources + "</p>" + "</td>"
html += "</tr>"
@@ -124,6 +130,10 @@ function updateDownloader(infoHash) {
html += "<td>" + "<p align='right'>" + activeSources + "</p>" + "</td>"
html += "</tr>"
html += "<tr>"
html += "<td>" + _t("Hopeless Sources") + "</td>"
html += "<td>" + "<p align='right'>" + hopelessSources + "</p>" + "</td>"
html += "</tr>"
html += "<tr>"
html += "<td>" + _t("Piece Size") + "</td>"
html += "<td>" + "<p align='right'>" + pieceSize + "</p>" + "</td>"
html += "</tr>"

View File

@@ -82,6 +82,13 @@ Exception error = (Exception) application.getAttribute("MWConfigError");
</td>
<td><p align="right"><input type="text" size="1" name="downloadRetryInterval" class="right" value="<%= core.getMuOptions().getDownloadRetryInterval()%>"></p></td>
</tr>
<tr>
<td><div class="tooltip"><%=Util._t("Give up on sources after this many failures (-1 means never)")%>
<span class="tooltiptext"><%=Util._t("After how many download attempts MuWire should give up on the download source.")%></span>
</div>
</td>
<td><p align="right"><input type="text" size="1" name="downloadMaxFailures" class="right" value="<%= core.getMuOptions().getDownloadMaxFailures()%>"></p></td>
</tr>
<tr>
<td><div class="tooltip"><%=Util._t("Directory for downloaded files")%>
<span class="tooltiptext"><%=Util._t("Where to save downloaded files. MuWire must be able to write to this location.")%></span>