Compare commits
25 Commits
muwire-0.7
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bcb41baca2 | ||
![]() |
72985bacb6 | ||
![]() |
3387d22a6c | ||
![]() |
10bd566d58 | ||
![]() |
f4e0c707df | ||
![]() |
c11a427483 | ||
![]() |
e9db22c562 | ||
![]() |
fa53a35023 | ||
![]() |
94dd6101aa | ||
![]() |
e65fbe1bd1 | ||
![]() |
964e315367 | ||
![]() |
140231e362 | ||
![]() |
c73a821c67 | ||
![]() |
0ebe00b526 | ||
![]() |
b2a3bfce54 | ||
![]() |
c490a511bd | ||
![]() |
cbaa3470d2 | ||
![]() |
84d61fccd5 | ||
![]() |
a88db8f50f | ||
![]() |
5a38154e15 | ||
![]() |
e5891de136 | ||
![]() |
1e729bae1c | ||
![]() |
3e6e0c7e9f | ||
![]() |
44af23c162 | ||
![]() |
a262c99efe |
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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))
|
||||
|
@@ -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))
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,7 @@
|
||||
package com.muwire.core.download
|
||||
|
||||
import com.muwire.core.Event
|
||||
|
||||
class DownloadHopelessEvent extends Event {
|
||||
Downloader downloader
|
||||
}
|
@@ -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
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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))
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
|
@@ -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")
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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", {
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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))
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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>");
|
||||
|
@@ -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>"
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user