Compare commits

...

39 Commits

Author SHA1 Message Date
Zlatin Balevsky
a9dc636bce write pieces every time a downloader finishes 2019-07-06 00:52:49 +01:00
Zlatin Balevsky
3cc0574d11 working partial pieces 2019-07-06 00:47:45 +01:00
Zlatin Balevsky
20fab9b16d work on partial piece persistence 2019-07-06 00:17:46 +01:00
Zlatin Balevsky
4015818323 center buttons 2019-07-05 17:15:50 +01:00
Zlatin Balevsky
f569d45c8c reallign tables 2019-07-05 17:07:14 +01:00
Zlatin Balevsky
3773647869 remove diff rejects 2019-07-05 16:24:57 +01:00
Zlatin Balevsky
29cdbf018c remove trailing spaces 2019-07-05 16:24:19 +01:00
Zlatin Balevsky
94bb7022eb tabs -> spaces 2019-07-05 16:22:34 +01:00
Zlatin Balevsky
39808302df Show which file is hashing, thanks to Aegon 2019-07-05 16:20:03 +01:00
Zlatin Balevsky
2d22f9c39e override router log manager 2019-07-05 12:32:23 +01:00
Zlatin Balevsky
ee8f80bab6 up i2p to 0.9.41 2019-07-05 12:26:48 +01:00
Zlatin Balevsky
3e6242e583 break when matching search is found 2019-07-04 18:12:22 +01:00
Zlatin Balevsky
41181616ee compact display of incoming searches, thanks Aegon 2019-07-04 17:59:53 +01:00
Zlatin Balevsky
eb2530ca32 fix sorting of download/upload tables thanks Aegon 2019-07-04 17:58:06 +01:00
Zlatin Balevsky
b5233780ef Release 0.4.7 2019-07-03 20:36:54 +01:00
Zlatin Balevsky
78753d7538 shut down cache client on shutdown 2019-07-03 19:50:00 +01:00
Zlatin Balevsky
4740e8b4f5 log hostcache stats 2019-07-03 19:46:24 +01:00
Zlatin Balevsky
ad5b00fc90 prettier progress status thanks to Aegon 2019-07-03 12:50:24 +01:00
Zlatin Balevsky
d6c6880848 update readme 2019-07-03 07:27:48 +01:00
Zlatin Balevsky
4f948c1b9e Release 0.4.6 2019-07-03 07:11:59 +01:00
Zlatin Balevsky
2b68c24f9c use switch 2019-07-03 07:01:27 +01:00
Zlatin Balevsky
bcdf0422db update for embedded router 2019-07-03 07:00:04 +01:00
Zlatin Balevsky
f6434b478d remove FAQ 2019-07-03 06:56:20 +01:00
Zlatin Balevsky
e979fdd26f update list view tables 2019-07-03 06:51:21 +01:00
Zlatin Balevsky
e6bfcaaab9 size columns, center integers 2019-07-03 06:11:02 +01:00
Zlatin Balevsky
9780108e8a disable trust buttons on action 2019-07-03 06:00:09 +01:00
Zlatin Balevsky
697c7d2d6d enable/disable trust panel buttons 2019-07-03 05:41:17 +01:00
Zlatin Balevsky
887d10c8bf move buttons around 2019-07-03 05:30:39 +01:00
Zlatin Balevsky
ef6b8fe458 add a state for failed updates 2019-07-03 05:12:00 +01:00
Zlatin Balevsky
20ab55d763 update todo 2019-07-03 00:23:21 +01:00
Zlatin Balevsky
eda58c9e0d Merge branch 'trust-lists' 2019-07-03 00:04:50 +01:00
Zlatin Balevsky
1ccf6fbdfa participating bandwidth grid cell 2019-07-02 15:35:42 +01:00
Zlatin Balevsky
5711979272 Release 0.4.5 2019-07-02 15:01:51 +01:00
Zlatin Balevsky
9a5e2b1fa3 speed smoothing patch courtesy of Aegon 2019-07-02 14:46:40 +01:00
Zlatin Balevsky
a89b423dfc simpler speed calculation 2019-07-02 13:05:06 +01:00
Zlatin Balevsky
79e8438941 always assume interval is at least 1 second 2019-07-02 12:49:00 +01:00
Zlatin Balevsky
19c2c46491 prevent NPE on startup 2019-07-02 12:27:15 +01:00
Zlatin Balevsky
78f1d54b69 add new host cache 2019-07-02 10:04:24 +01:00
Zlatin Balevsky
9461649ed4 change sig type 2019-07-02 09:49:13 +01:00
141 changed files with 4978 additions and 4765 deletions

View File

@@ -4,7 +4,7 @@ MuWire is an easy to use file-sharing program which offers anonymity using [I2P
It is inspired by the LimeWire Gnutella client and developped by a former LimeWire developer.
The current stable release - 0.4.0 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
The current stable release - 0.4.6 is avaiable for download at https://muwire.com. You can find technical documentation in the "doc" folder.
### Building
@@ -23,34 +23,9 @@ Some of the UI tests will fail because they haven't been written yet :-/
### Running
You need to have an I2P router up and running on the same machine. After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar MuWire-x.y.z.jar` in a terminal or command prompt. If you use a custom I2CP host and port, create a file `$HOME/.MuWire/i2p.properties` and put `i2cp.tcp.host=<host>` and `i2cp.tcp.port=<port>` in there.
After you build the application, look inside `gui/build/distributions`. Untar/unzip one of the `shadow` files and then run the jar contained inside by typing `java -jar MuWire-x.y.z.jar` in a terminal or command prompt.
The first time you run MuWire it will ask you to select a nickname. This nickname will be displayed with search results, so that others can verify the file was shared by you. It is best to leave MuWire running all the time, just like I2P.
If you have an I2P router running on the same machine that is all you need to do. If you use a custom I2CP host and port, create a file `$HOME/.MuWire/i2p.properties` and put `i2cp.tcp.host=<host>` and `i2cp.tcp.port=<port>` in there.
If you do not have an I2P router, pass the following switch to the Java process: `-DembeddedRouter=true`. This will launch MuWire's embedded router. Be aware that this causes startup to take a lot longer.
### Known bugs and limitations
* Many UI features you would expect are not there yet
### Quick FAQ
* why is MuWire slow ?
- too few sources you're downloading from
- you can increase the number of tunnels by using more tunnels via Options->I2P Inbound/Outbound Quantity
the default is 4 and you could raise up to as high as 16 ( Caution !!!!)
* my search is not returning (enough) results !
- search is keyword or hash based
- keywords and hash(es) are NOT regexed or wildcarded so they have to be complete
so searching for 'musi' will not return results with 'music' - you have to search for 'music'
- ALL keywords have to match
- only use space for keyword separation
- if you already have the file in question it is not displayed ( can be changed via Options )
* what's this right click -> 'Copy hash to clipboard' for ?
- if you have a specific file you wish to share or download you can use the hash as a unique identifier
to make sure you have exactly the right file.
- you can share this hash with others to ensure they are getting the right file

View File

@@ -12,10 +12,6 @@ This reduces query traffic by not sending last hop queries to peers that definit
This helps with scalability
##### Trust List Sharing
For helping users make better decisions whom to trust
##### Content Control Panel
To allow every user to not route queries for content they do not like. This is mostly GUI work, the backend part is simple

View File

@@ -2,7 +2,7 @@ subprojects {
apply plugin: 'groovy'
dependencies {
compile 'net.i2p:i2p:0.9.40'
compile 'net.i2p:i2p:0.9.41'
compile 'org.codehaus.groovy:groovy-all:2.4.15'
}

View File

@@ -35,7 +35,7 @@ class Cli {
Core core
try {
core = new Core(props, home, "0.4.4")
core = new Core(props, home, "0.4.7")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"

View File

@@ -53,7 +53,7 @@ class CliDownloader {
Core core
try {
core = new Core(props, home, "0.4.4")
core = new Core(props, home, "0.4.7")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"

View File

@@ -2,9 +2,9 @@ apply plugin : 'application'
mainClassName = 'com.muwire.core.Core'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
dependencies {
compile 'net.i2p:router:0.9.40'
compile 'net.i2p.client:mstreaming:0.9.40'
compile 'net.i2p.client:streaming:0.9.40'
compile 'net.i2p:router:0.9.41'
compile 'net.i2p.client:mstreaming:0.9.41'
compile 'net.i2p.client:streaming:0.9.41'
testCompile 'org.junit.jupiter:junit-jupiter-api:5.4.2'
testCompile 'junit:junit:4.12'

View File

@@ -20,6 +20,7 @@ import com.muwire.core.download.UIDownloadPausedEvent
import com.muwire.core.download.UIDownloadResumedEvent
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileHashingEvent
import com.muwire.core.files.FileHasher
import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileManager
@@ -140,7 +141,7 @@ public class Core {
routerProps.setProperty("i2np.udp.port", i2pOptions["i2np.udp.port"])
routerProps.setProperty("i2np.udp.internalPort", i2pOptions["i2np.udp.port"])
router = new Router(routerProps)
I2PAppContext.getGlobalContext().metaClass = new RouterContextMetaClass()
router.getContext().setLogManager(new MuWireLogManager())
router.runRouter()
while(!router.isRunning())
Thread.sleep(100)
@@ -318,6 +319,8 @@ public class Core {
connectionEstablisher.stop()
log.info("shutting down directory watcher")
directoryWatcher.stop()
log.info("shutting down cache client")
cacheClient.stop()
log.info("shutting down connection manager")
connectionManager.shutdown()
if (router != null) {
@@ -326,19 +329,6 @@ public class Core {
}
}
static class RouterContextMetaClass extends DelegatingMetaClass {
private final Object logManager = new MuWireLogManager()
RouterContextMetaClass() {
super(RouterContext.class)
}
Object invokeMethod(Object object, String name, Object[] args) {
if (name == "logManager")
return logManager
super.invokeMethod(object, name, args)
}
}
static main(args) {
def home = System.getProperty("user.home") + File.separator + ".MuWire"
home = new File(home)
@@ -363,7 +353,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.4.4")
Core core = new Core(props, home, "0.4.7")
core.startServices()
// ... at the end, sleep or execute script

View File

@@ -80,7 +80,7 @@ class MuWireSettings {
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
props.setProperty("autoDownloadUpdate", String.valueOf(autoDownloadUpdate))
props.setProperty("updateType",updateType)
props.setProperty("updateType",String.valueOf(updateType))
props.setProperty("shareDownloadedFiles", String.valueOf(shareDownloadedFiles))
props.setProperty("downloadSequentialRatio", String.valueOf(downloadSequentialRatio))
props.setProperty("hostClearInterval", String.valueOf(hostClearInterval))

View File

@@ -14,6 +14,7 @@ import static com.muwire.core.util.DataUtil.readTillRN
import groovy.util.logging.Log
import java.nio.ByteBuffer
import java.nio.MappedByteBuffer
import java.nio.channels.FileChannel
import java.nio.charset.StandardCharsets
import java.nio.file.Files
@@ -25,8 +26,6 @@ import java.util.logging.Level
@Log
class DownloadSession {
private static int SAMPLES = 10
private final EventBus eventBus
private final String meB64
private final Pieces pieces
@@ -38,10 +37,10 @@ class DownloadSession {
private final Set<Integer> available
private final MessageDigest digest
private final LinkedList<Long> timestamps = new LinkedList<>()
private final LinkedList<Integer> reads = new LinkedList<>()
private long lastSpeedRead = System.currentTimeMillis()
private long dataSinceLastRead
private ByteBuffer mapped
private MappedByteBuffer mapped
DownloadSession(EventBus eventBus, String meB64, Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
int pieceSize, long fileLength, Set<Integer> available) {
@@ -71,21 +70,22 @@ class DownloadSession {
OutputStream os = endpoint.getOutputStream()
InputStream is = endpoint.getInputStream()
int piece
int[] pieceAndPosition
if (available.isEmpty())
piece = pieces.claim()
pieceAndPosition = pieces.claim()
else
piece = pieces.claim(new HashSet<>(available))
if (piece == -1)
pieceAndPosition = pieces.claim(new HashSet<>(available))
if (pieceAndPosition == null)
return false
int piece = pieceAndPosition[0]
int position = pieceAndPosition[1]
boolean unclaim = true
log.info("will download piece $piece")
long start = piece * pieceSize
long end = Math.min(fileLength, start + pieceSize) - 1
long length = end - start + 1
log.info("will download piece $piece from position $position")
long pieceStart = piece * pieceSize
long end = Math.min(fileLength, pieceStart + pieceSize) - 1
long start = pieceStart + position
String root = Base64.encode(infoHash.getRoot())
try {
@@ -174,8 +174,9 @@ class DownloadSession {
FileChannel channel
try {
channel = Files.newByteChannel(file.toPath(), EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE,
StandardOpenOption.SPARSE, StandardOpenOption.CREATE)) // TODO: double-check, maybe CREATE_NEW
mapped = channel.map(FileChannel.MapMode.READ_WRITE, start, end - start + 1)
StandardOpenOption.SPARSE, StandardOpenOption.CREATE))
mapped = channel.map(FileChannel.MapMode.READ_WRITE, pieceStart, end - pieceStart + 1)
mapped.position(position)
byte[] tmp = new byte[0x1 << 13]
while(mapped.hasRemaining()) {
@@ -186,26 +187,23 @@ class DownloadSession {
throw new IOException()
synchronized(this) {
mapped.put(tmp, 0, read)
if (timestamps.size() == SAMPLES) {
timestamps.removeFirst()
reads.removeFirst()
}
timestamps.addLast(System.currentTimeMillis())
reads.addLast(read)
dataSinceLastRead += read
pieces.markPartial(piece, mapped.position())
}
}
mapped.clear()
digest.update(mapped)
DataUtil.tryUnmap(mapped)
byte [] hash = digest.digest()
byte [] expected = new byte[32]
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
if (hash != expected)
throw new BadHashException()
if (hash != expected) {
pieces.markPartial(piece, 0)
throw new BadHashException("bad hash on piece $piece")
}
} finally {
try { channel?.close() } catch (IOException ignore) {}
DataUtil.tryUnmap(mapped)
}
pieces.markDownloaded(piece)
unclaim = false
@@ -223,24 +221,11 @@ class DownloadSession {
}
synchronized int speed() {
if (timestamps.size() < SAMPLES)
return 0
int totalRead = 0
int idx = 0
final long now = System.currentTimeMillis()
while(idx < SAMPLES && timestamps.get(idx) < now - 1000)
idx++
if (idx == SAMPLES)
return 0
if (idx == SAMPLES - 1)
return reads[idx]
long interval = timestamps.last - timestamps[idx]
if (interval == 0)
interval = 1
for (int i = idx; i < SAMPLES; i++)
totalRead += reads[idx]
(int)(totalRead * 1000.0 / interval)
long interval = Math.max(1000, now - lastSpeedRead)
lastSpeedRead = now;
int rv = (int) (dataSinceLastRead * 1000.0 / interval)
dataSinceLastRead = 0
rv
}
}

View File

@@ -7,6 +7,7 @@ import com.muwire.core.connection.Endpoint
import java.nio.file.AtomicMoveNotSupportedException
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.time.Instant
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -58,6 +59,11 @@ public class Downloader {
private final AtomicBoolean eventFired = new AtomicBoolean()
private boolean piecesFileClosed
private ArrayList speedArr = new ArrayList<Integer>()
private int speedPos = 0
private int speedAvg = 0
private long timestamp = Instant.now().toEpochMilli()
public Downloader(EventBus eventBus, DownloadManager downloadManager,
Persona me, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
@@ -76,6 +82,10 @@ public class Downloader {
this.pieceSize = 1 << pieceSizePow2
this.pieces = pieces
this.nPieces = pieces.nPieces
// default size suitable for an average of 5 seconds / 5 elements / 5 interval units
// it's easily adjustable by resizing the size of speedArr
this.speedArr = [ 0, 0, 0, 0, 0 ]
}
public synchronized InfoHash getInfoHash() {
@@ -101,8 +111,14 @@ public class Downloader {
if (!piecesFile.exists())
return
piecesFile.eachLine {
int piece = Integer.parseInt(it)
String [] split = it.split(",")
int piece = Integer.parseInt(split[0])
if (split.length == 1)
pieces.markDownloaded(piece)
else {
int position = Integer.parseInt(split[1])
pieces.markPartial(piece, position)
}
}
}
@@ -111,9 +127,7 @@ public class Downloader {
if (piecesFileClosed)
return
piecesFile.withPrintWriter { writer ->
pieces.getDownloaded().each { piece ->
writer.println(piece)
}
pieces.write(writer)
}
}
}
@@ -124,14 +138,35 @@ public class Downloader {
public int speed() {
int total = 0
int currSpeed = 0
if (getCurrentState() == DownloadState.DOWNLOADING) {
activeWorkers.values().each {
if (it.currentState == WorkerState.DOWNLOADING)
total += it.speed()
currSpeed += it.speed()
}
}
total
// normalize to speedArr.size
currSpeed /= speedArr.size()
// compute new speedAvg and update speedArr
if ( speedArr[speedPos] > speedAvg ) {
speedAvg = 0
} else {
speedAvg -= speedArr[speedPos]
}
speedAvg += currSpeed
speedArr[speedPos] = currSpeed
// this might be necessary due to rounding errors
if (speedAvg < 0)
speedAvg = 0
// rolling index over the speedArr
speedPos++
if (speedPos >= speedArr.size())
speedPos=0
speedAvg
}
public DownloadState getCurrentState() {
@@ -272,6 +307,7 @@ public class Downloader {
} catch (Exception bad) {
log.log(Level.WARNING,"Exception while downloading",DataUtil.findRoot(bad))
} finally {
writePieces()
currentState = WorkerState.FINISHED
if (pieces.isComplete() && eventFired.compareAndSet(false, true)) {
synchronized(piecesFile) {

View File

@@ -5,6 +5,7 @@ class Pieces {
private final int nPieces
private final float ratio
private final Random random = new Random()
private final Map<Integer,Integer> partials = new HashMap<>()
Pieces(int nPieces) {
this(nPieces, 1.0f)
@@ -17,16 +18,16 @@ class Pieces {
claimed = new BitSet(nPieces)
}
synchronized int claim() {
synchronized int[] claim() {
int claimedCardinality = claimed.cardinality()
if (claimedCardinality == nPieces)
return -1
return null
// if fuller than ratio just do sequential
if ( (1.0f * claimedCardinality) / nPieces > ratio) {
int rv = claimed.nextClearBit(0)
claimed.set(rv)
return rv
return [rv, partials.getOrDefault(rv, 0)]
}
while(true) {
@@ -34,11 +35,11 @@ class Pieces {
if (claimed.get(start))
continue
claimed.set(start)
return start
return [start, partials.getOrDefault(start,0)]
}
}
synchronized int claim(Set<Integer> available) {
synchronized int[] claim(Set<Integer> available) {
for (int i = claimed.nextSetBit(0); i >= 0; i = claimed.nextSetBit(i+1))
available.remove(i)
if (available.isEmpty())
@@ -47,7 +48,7 @@ class Pieces {
Collections.shuffle(toList)
int rv = toList[0]
claimed.set(rv)
rv
[rv, partials.getOrDefault(rv, 0)]
}
synchronized def getDownloaded() {
@@ -61,6 +62,11 @@ class Pieces {
synchronized void markDownloaded(int piece) {
done.set(piece)
claimed.set(piece)
partials.remove(piece)
}
synchronized void markPartial(int piece, int position) {
partials.put(piece, position)
}
synchronized void unclaim(int piece) {
@@ -82,5 +88,15 @@ class Pieces {
synchronized void clearAll() {
done.clear()
claimed.clear()
partials.clear()
}
synchronized void write(PrintWriter writer) {
for (int i = done.nextSetBit(0); i >= 0; i = done.nextSetBit(i+1)) {
writer.println(i)
}
partials.each { piece, position ->
writer.println("$piece,$position")
}
}
}

View File

@@ -0,0 +1,15 @@
package com.muwire.core.files
import com.muwire.core.Event
import com.muwire.core.SharedFile
class FileHashingEvent extends Event {
File hashingFile
@Override
public String toString() {
super.toString() + " hashingFile " + hashingFile.getAbsolutePath()
}
}

View File

@@ -39,6 +39,7 @@ class HasherService {
} else if (f.length() > FileHasher.MAX_SIZE) {
eventBus.publish new FileHashedEvent(error: "$f is too large to be shared ${f.length()}")
} else {
eventBus.publish new FileHashingEvent(hashingFile: f)
def hash = hasher.hashFile f
eventBus.publish new FileHashedEvent(sharedFile: new SharedFile(f, hash, FileHasher.getPieceSize(f.length())))
}

View File

@@ -6,7 +6,8 @@ class CacheServers {
private static final int TO_GIVE = 3
private static Set<Destination> CACHES = [
new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA")
new Destination("Wddh2E6FyyXBF7SvUYHKdN-vjf3~N6uqQWNeBDTM0P33YjiQCOsyedrjmDZmWFrXUJfJLWnCb5bnKezfk4uDaMyj~uvDG~yvLVcFgcPWSUd7BfGgym-zqcG1q1DcM8vfun-US7YamBlmtC6MZ2j-~Igqzmgshita8aLPCfNAA6S6e2UMjjtG7QIXlxpMec75dkHdJlVWbzrk9z8Qgru3YIk0UztYgEwDNBbm9wInsbHhr3HtAfa02QcgRVqRN2PnQXuqUJs7R7~09FZPEviiIcUpkY3FeyLlX1sgQFBeGeA96blaPvZNGd6KnNdgfLgMebx5SSxC-N4KZMSMBz5cgonQF3~m2HHFRSI85zqZNG5X9bJN85t80ltiv1W1es8ZnQW4es11r7MrvJNXz5bmSH641yJIvS6qI8OJJNpFVBIQSXLD-96TayrLQPaYw~uNZ-eXaE6G5dYhiuN8xHsFI1QkdaUaVZnvDGfsRbpS5GtpUbBDbyLkdPurG0i7dN1wAAAA"),
new Destination("JC63wJNOqSJmymkj4~UJWywBTvDGikKMoYP0HX2Wz9c5l3otXSkwnxWAFL4cKr~Ygh3BNNi2t93vuLIiI1W8AsE42kR~PwRx~Y-WvIHXR6KUejRmOp-n8WidtjKg9k4aDy428uSOedqXDxys5mpoeQXwDsv1CoPTTwnmb1GWFy~oTGIsCguCl~aJWGnqiKarPO3GJQ~ev-NbvAQzUfC3HeP1e6pdI5CGGjExahTCID5UjpJw8GaDXWlGmYWWH303Xu4x-vAHQy1dJLsOBCn8dZravsn5BKJk~j0POUon45CCx-~NYtaPe0Itt9cMdD2ciC76Rep1D0X0sm1SjlSs8sZ52KmF3oaLZ6OzgI9QLMIyBUrfi41sK5I0qTuUVBAkvW1xr~L-20dYJ9TrbOaOb2-vDIfKaxVi6xQOuhgQDiSBhd3qv2m0xGu-BM9DQYfNA0FdMjnZmqjmji9RMavzQSsVFIbQGLbrLepiEFlb7TseCK5UtRp8TxnG7L4gbYevBQAEAAcAAA==")
]
static List<Destination> getCacheServers() {

View File

@@ -7,7 +7,7 @@ import com.muwire.core.Persona
import net.i2p.util.ConcurrentHashSet
class RemoteTrustList {
public enum Status { NEW, UPDATING, UPDATED }
public enum Status { NEW, UPDATING, UPDATED, UPDATE_FAILED }
private final Persona persona
private final Set<Persona> good, bad

View File

@@ -94,13 +94,15 @@ class TrustSubscriber {
public void run() {
trustList.status = RemoteTrustList.Status.UPDATING
eventBus.publish(new TrustSubscriptionUpdatedEvent(trustList : trustList))
check(trustList, System.currentTimeMillis())
if (check(trustList, System.currentTimeMillis()))
trustList.status = RemoteTrustList.Status.UPDATED
else
trustList.status = RemoteTrustList.Status.UPDATE_FAILED
eventBus.publish(new TrustSubscriptionUpdatedEvent(trustList : trustList))
}
}
private void check(RemoteTrustList trustList, long now) {
private boolean check(RemoteTrustList trustList, long now) {
log.info("fetching trust list from ${trustList.persona.getHumanReadableName()}")
Endpoint endpoint = null
try {
@@ -118,7 +120,7 @@ class TrustSubscriber {
if (code != 200) {
log.info("couldn't fetch trust list, code $code")
return
return false
}
// swallow any headers
@@ -147,8 +149,10 @@ class TrustSubscriber {
trustList.bad.clear()
trustList.bad.addAll(bad)
return true
} catch (Exception e) {
log.log(Level.WARNING,"exception fetching trust list from ${trustList.persona.getHumanReadableName()}",e)
return false
} finally {
endpoint?.close()
}

View File

@@ -119,4 +119,8 @@ class ContentUploader extends Uploader {
return mesh.pieces.nPieces;
}
@Override
public long getTotalSize() {
return file.length();
}
}

View File

@@ -61,5 +61,8 @@ class HashListUploader extends Uploader {
return 1;
}
@Override
public long getTotalSize() {
return -1;
}
}

View File

@@ -35,5 +35,7 @@ abstract class Uploader {
abstract int getDonePieces();
abstract int getTotalPieces()
abstract int getTotalPieces();
abstract long getTotalSize();
}

View File

@@ -1,5 +1,5 @@
group = com.muwire
version = 0.4.4
version = 0.4.7
groovyVersion = 2.4.15
slf4jVersion = 1.7.25
spockVersion = 1.1-groovy-2.4

View File

@@ -191,27 +191,38 @@ class MainFrameController {
int row = view.getSelectedTrustTablesRow(tableName)
if (row < 0)
return
builder.getVariable(tableName).model.fireTableDataChanged()
core.eventBus.publish(new TrustEvent(persona : list[row], level : level))
}
@ControllerAction
void markTrusted() {
markTrust("distrusted-table", TrustLevel.TRUSTED, model.distrusted)
model.markTrustedButtonEnabled = false
model.markNeutralFromDistrustedButtonEnabled = false
}
@ControllerAction
void markNeutralFromDistrusted() {
markTrust("distrusted-table", TrustLevel.NEUTRAL, model.distrusted)
model.markTrustedButtonEnabled = false
model.markNeutralFromDistrustedButtonEnabled = false
}
@ControllerAction
void markDistrusted() {
markTrust("trusted-table", TrustLevel.DISTRUSTED, model.trusted)
model.subscribeButtonEnabled = false
model.markDistrustedButtonEnabled = false
model.markNeutralFromTrustedButtonEnabled = false
}
@ControllerAction
void markNeutralFromTrusted() {
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
model.subscribeButtonEnabled = false
model.markDistrustedButtonEnabled = false
model.markNeutralFromTrustedButtonEnabled = false
}
@ControllerAction
@@ -223,6 +234,9 @@ class MainFrameController {
core.muOptions.trustSubscriptions.add(p)
saveMuWireSettings()
core.eventBus.publish(new TrustSubscriptionEvent(persona : p, subscribe : true))
model.subscribeButtonEnabled = false
model.markDistrustedButtonEnabled = false
model.markNeutralFromTrustedButtonEnabled = false
}
@ControllerAction

View File

@@ -27,6 +27,7 @@ class TrustListController {
return
Persona p = model.trusted[selectedRow]
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED))
view.fireUpdate("trusted-table")
}
@ControllerAction
@@ -36,6 +37,7 @@ class TrustListController {
return
Persona p = model.distrusted[selectedRow]
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.TRUSTED))
view.fireUpdate("distrusted-table")
}
@ControllerAction
@@ -45,6 +47,7 @@ class TrustListController {
return
Persona p = model.trusted[selectedRow]
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED))
view.fireUpdate("trusted-table")
}
@ControllerAction
@@ -54,5 +57,6 @@ class TrustListController {
return
Persona p = model.distrusted[selectedRow]
eventBus.publish(new TrustEvent(persona : p, level : TrustLevel.DISTRUSTED))
view.fireUpdate("distrusted-table")
}
}

View File

@@ -49,7 +49,7 @@ class Ready extends AbstractLifecycleHandler {
log.info("creating new properties")
props = new MuWireSettings()
props.embeddedRouter = Boolean.parseBoolean(System.getProperties().getProperty("embeddedRouter"))
props.updateType = System.getProperty("updateType")
props.updateType = System.getProperty("updateType","jar")
def nickname
while (true) {
nickname = JOptionPane.showInputDialog(null,

View File

@@ -1,6 +1,8 @@
package com.muwire.gui
import java.util.concurrent.ConcurrentHashMap
import java.util.Calendar
import java.util.UUID
import javax.annotation.Nonnull
import javax.inject.Inject
@@ -20,6 +22,7 @@ import com.muwire.core.download.Downloader
import com.muwire.core.files.AllFilesLoadedEvent
import com.muwire.core.files.FileDownloadedEvent
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileHashingEvent
import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.files.FileUnsharedEvent
@@ -70,12 +73,19 @@ class MainFrameModel {
@Observable int connections
@Observable String me
@Observable int loadedFiles
@Observable File hashingFile
@Observable boolean downloadActionEnabled
@Observable boolean trustButtonsEnabled
@Observable boolean cancelButtonEnabled
@Observable boolean retryButtonEnabled
@Observable boolean pauseButtonEnabled
@Observable String resumeButtonText
@Observable boolean subscribeButtonEnabled
@Observable boolean markNeutralFromTrustedButtonEnabled
@Observable boolean markDistrustedButtonEnabled
@Observable boolean markNeutralFromDistrustedButtonEnabled
@Observable boolean markTrustedButtonEnabled
@Observable boolean reviewButtonEnabled
@Observable boolean updateButtonEnabled
@Observable boolean unsubscribeButtonEnabled
@@ -140,6 +150,7 @@ class MainFrameModel {
core.eventBus.register(ConnectionEvent.class, this)
core.eventBus.register(DisconnectionEvent.class, this)
core.eventBus.register(FileHashedEvent.class, this)
core.eventBus.register(FileHashingEvent.class, this)
core.eventBus.register(FileLoadedEvent.class, this)
core.eventBus.register(UploadEvent.class, this)
core.eventBus.register(UploadFinishedEvent.class, this)
@@ -256,7 +267,17 @@ class MainFrameModel {
}
}
void onFileHashingEvent(FileHashingEvent e) {
runInsideUIAsync {
loadedFiles = shared.size()
hashingFile = e.hashingFile
}
}
void onFileHashedEvent(FileHashedEvent e) {
runInsideUIAsync {
hashingFile = null
}
if (e.error != null)
return // TODO do something
if (infoHashes.contains(e.sharedFile.infoHash))
@@ -264,6 +285,7 @@ class MainFrameModel {
infoHashes.add(e.sharedFile.infoHash)
runInsideUIAsync {
shared << e.sharedFile
loadedFiles = shared.size()
JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged()
}
@@ -275,6 +297,7 @@ class MainFrameModel {
infoHashes.add(e.loadedFile.infoHash)
runInsideUIAsync {
shared << e.loadedFile
loadedFiles = shared.size()
JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged()
}
@@ -286,6 +309,7 @@ class MainFrameModel {
return
runInsideUIAsync {
shared.remove(e.unsharedFile)
loadedFiles = shared.size()
JTable table = builder.getVariable("shared-files-table")
table.model.fireTableDataChanged()
}
@@ -355,10 +379,32 @@ class MainFrameModel {
return
}
runInsideUIAsync {
searches.addFirst(new IncomingSearch(search : search, replyTo : e.replyTo, originator : e.originator))
JTable table = builder.getVariable("searches-table")
Boolean searchFound = false
Iterator searchIter = searches.iterator()
while ( searchIter.hasNext() ) {
IncomingSearch searchEle = searchIter.next()
if ( searchEle.search == search
&& searchEle.originator == e.originator
&& searchEle.uuid == e.searchEvent.getUuid() ) {
searchIter.remove()
table.model.fireTableDataChanged()
searchFound = true
searchEle.count++
searchEle.timestamp = Calendar.getInstance()
searches.addFirst(searchEle)
break
}
}
if (!searchFound) {
searches.addFirst(new IncomingSearch(search, e.replyTo, e.originator, e.searchEvent.getUuid()))
}
while(searches.size() > 200)
searches.removeLast()
JTable table = builder.getVariable("searches-table")
table.model.fireTableDataChanged()
}
}
@@ -367,6 +413,18 @@ class MainFrameModel {
String search
Destination replyTo
Persona originator
long count
UUID uuid
Calendar timestamp
IncomingSearch( String search, Destination replyTo, Persona originator, UUID uuid ) {
this.search = search
this.replyTo = replyTo
this.originator = originator
this.uuid = uuid
this.count = 1
this.timestamp = Calendar.getInstance()
}
}
void onUpdateAvailableEvent(UpdateAvailableEvent e) {

View File

@@ -44,7 +44,7 @@ class I2PStatusView {
label(text : "Participating Tunnels", constraints : gbc(gridx:0, gridy:4))
label(text : bind {model.participatingTunnels}, constraints : gbc(gridx: 1, gridy:4))
label(text : "Participating Bandwidth", constraints : gbc(gridx:0, gridy:5))
label(text : bind {model.participatingBW}, constraints : gbc(gridx: 1, gridy:6))
label(text : bind {model.participatingBW}, constraints : gbc(gridx: 1, gridy:5))
label(text : "Active Peers", constraints : gbc(gridx:0, gridy:6))
label(text : bind {model.activePeers}, constraints : gbc(gridx: 1, gridy:6))
label(text : "Receive Bps (15 seconds)", constraints : gbc(gridx:0, gridy:7))

View File

@@ -129,12 +129,19 @@ class MainFrameView {
scrollPane (constraints : BorderLayout.CENTER) {
downloadsTable = table(id : "downloads-table", autoCreateRowSorter : true) {
tableModel(list: model.downloads) {
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.downloader.file.getName()})
closureColumn(header: "Name", preferredWidth: 300, type: String, read : {row -> row.downloader.file.getName()})
closureColumn(header: "Status", preferredWidth: 50, type: String, read : {row -> row.downloader.getCurrentState().toString()})
closureColumn(header: "Progress", preferredWidth: 20, type: String, read: { row ->
closureColumn(header: "Progress", preferredWidth: 70, type: String, read: { row ->
int pieces = row.downloader.nPieces
int done = row.downloader.donePieces()
"$done/$pieces pieces".toString()
int percent = -1
if ( row.downloader.nPieces != 0 ) {
percent = (done * 100) / pieces
}
long size = row.downloader.pieceSize
size *= pieces
String totalSize = DataHelper.formatSize2Decimal(size, false) + "B"
String.format("%02d", percent) + "% of ${totalSize} ($done/$pieces pcs)".toString()
})
closureColumn(header: "Sources", preferredWidth : 10, type: Integer, read : {row -> row.downloader.activeWorkers()})
closureColumn(header: "Speed", preferredWidth: 50, type:String, read :{row ->
@@ -154,12 +161,20 @@ class MainFrameView {
panel (constraints: "uploads window"){
gridLayout(cols : 1, rows : 2)
panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
label(text : bind {
if (model.hashingFile == null) {
""
} else {
"hashing: " + model.hashingFile.getAbsolutePath() + " (" + DataHelper.formatSize2Decimal(model.hashingFile.length(), false).toString() + "B)"
}
})
}
panel (border : etchedBorder(), constraints : BorderLayout.CENTER) {
gridLayout(cols : 2, rows : 1)
panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
button(text : "Add directories to watch", actionPerformed : watchDirectories)
}
scrollPane (constraints : BorderLayout.CENTER) {
table(id : "watched-directories-table", autoCreateRowSorter: true) {
tableModel(list : model.watched) {
@@ -170,9 +185,6 @@ class MainFrameView {
}
panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
button(text : "Share files", actionPerformed : shareFiles)
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "shared-files-table", autoCreateRowSorter: true) {
tableModel(list : model.shared) {
@@ -183,7 +195,19 @@ class MainFrameView {
}
}
}
panel (constraints : BorderLayout.SOUTH) {
gridLayout(rows:1, cols:2)
panel {
button(text : "Add directories to watch", actionPerformed : watchDirectories)
button(text : "Share files", actionPerformed : shareFiles)
}
panel {
label("Shared:")
label(text : bind {model.loadedFiles.toString()})
}
}
}
panel (border : etchedBorder()) {
borderLayout()
panel (constraints : BorderLayout.NORTH){
label("Uploads")
@@ -200,7 +224,18 @@ class MainFrameView {
row.getDownloader()
})
closureColumn(header : "Remote Pieces", type : String, read : { row ->
"${row.getDonePieces()}/${row.getTotalPieces()}".toString()
int pieces = row.getTotalPieces()
int done = row.getDonePieces()
int percent = -1
if ( pieces != 0 ) {
percent = (done * 100) / pieces
}
long size = row.getTotalSize()
String totalSize = ""
if (size >= 0 ) {
totalSize = " of " + DataHelper.formatSize2Decimal(size, false) + "B"
}
String.format("%02d", percent) + "% ${totalSize} ($done/$pieces pcs)".toString()
})
}
}
@@ -247,6 +282,14 @@ class MainFrameView {
return it.replyTo.toBase32()
}
})
closureColumn(header : "Count", type : String, read : {
it.count.toString()
})
closureColumn(header : "Timestamp", type : String, read : {
String.format("%02d", it.timestamp.get(Calendar.HOUR_OF_DAY)) + ":" +
String.format("%02d", it.timestamp.get(Calendar.MINUTE)) + ":" +
String.format("%02d", it.timestamp.get(Calendar.SECOND))
})
}
}
}
@@ -265,11 +308,11 @@ class MainFrameView {
}
}
}
panel (constraints : BorderLayout.EAST) {
panel (constraints : BorderLayout.SOUTH) {
gridBagLayout()
button(text : "Mark Neutral", constraints : gbc(gridx: 0, gridy: 0), markNeutralFromTrustedAction)
button(text : "Mark Distrusted", constraints : gbc(gridx: 0, gridy:1), markDistrustedAction)
button(text : "Subscribe", constraints : gbc(gridx: 0, gridy : 2), subscribeAction)
button(text : "Subscribe", enabled : bind {model.subscribeButtonEnabled}, constraints : gbc(gridx: 0, gridy : 0), subscribeAction)
button(text : "Mark Neutral", enabled : bind {model.markNeutralFromTrustedButtonEnabled}, constraints : gbc(gridx: 1, gridy: 0), markNeutralFromTrustedAction)
button(text : "Mark Distrusted", enabled : bind {model.markDistrustedButtonEnabled}, constraints : gbc(gridx: 2, gridy:0), markDistrustedAction)
}
}
panel (border : etchedBorder()){
@@ -281,10 +324,10 @@ class MainFrameView {
}
}
}
panel(constraints : BorderLayout.WEST) {
panel(constraints : BorderLayout.SOUTH) {
gridBagLayout()
button(text: "Mark Neutral", constraints: gbc(gridx: 0, gridy: 0), markNeutralFromDistrustedAction)
button(text: "Mark Trusted", constraints : gbc(gridx: 0, gridy : 1), markTrustedAction)
button(text: "Mark Neutral", enabled : bind {model.markNeutralFromDistrustedButtonEnabled}, constraints: gbc(gridx: 0, gridy: 0), markNeutralFromDistrustedAction)
button(text: "Mark Trusted", enabled : bind {model.markTrustedButtonEnabled}, constraints : gbc(gridx: 1, gridy : 0), markTrustedAction)
}
}
}
@@ -296,11 +339,11 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "subscription-table", autoCreateRowSorter : true) {
tableModel(list : model.subscriptions) {
closureColumn(header : "Name", type: String, read : {it.persona.getHumanReadableName()})
closureColumn(header : "Trusted", type: Integer, read : {it.good.size()})
closureColumn(header : "Distrusted", type: Integer, read : {it.bad.size()})
closureColumn(header : "Status", type: String, read : {it.status.toString()})
closureColumn(header : "Last Updated", type : String, read : {
closureColumn(header : "Name", preferredWidth: 200, type: String, read : {it.persona.getHumanReadableName()})
closureColumn(header : "Trusted", preferredWidth : 20, type: Integer, read : {it.good.size()})
closureColumn(header : "Distrusted", preferredWidth: 20, type: Integer, read : {it.bad.size()})
closureColumn(header : "Status", preferredWidth: 30, type: String, read : {it.status.toString()})
closureColumn(header : "Last Updated", preferredWidth: 200, type : String, read : {
if (it.timestamp == 0)
return "Never"
else
@@ -461,6 +504,7 @@ class MainFrameView {
// subscription table
def subscriptionTable = builder.getVariable("subscription-table")
subscriptionTable.setDefaultRenderer(Integer.class, centerRenderer)
subscriptionTable.rowSorter.addRowSorterListener({evt -> trustTablesSortEvents["subscription-table"] = evt})
subscriptionTable.rowSorter.setSortsOnUpdates(true)
selectionModel = subscriptionTable.getSelectionModel()
@@ -488,6 +532,11 @@ class MainFrameView {
model.updateButtonEnabled = true
model.unsubscribeButtonEnabled = true
break
case RemoteTrustList.Status.UPDATE_FAILED:
model.reviewButtonEnabled = false
model.updateButtonEnabled = true
model.unsubscribeButtonEnabled = true
break
}
})
@@ -495,11 +544,37 @@ class MainFrameView {
def trustedTable = builder.getVariable("trusted-table")
trustedTable.rowSorter.addRowSorterListener({evt -> trustTablesSortEvents["trusted-table"] = evt})
trustedTable.rowSorter.setSortsOnUpdates(true)
selectionModel = trustedTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({
int selectedRow = getSelectedTrustTablesRow("trusted-table")
if (selectedRow < 0) {
model.subscribeButtonEnabled = false
model.markDistrustedButtonEnabled = false
model.markNeutralFromTrustedButtonEnabled = false
} else {
model.subscribeButtonEnabled = true
model.markDistrustedButtonEnabled = true
model.markNeutralFromTrustedButtonEnabled = true
}
})
// distrusted table
def distrustedTable = builder.getVariable("distrusted-table")
distrustedTable.rowSorter.addRowSorterListener({evt -> trustTablesSortEvents["distrusted-table"] = evt})
distrustedTable.rowSorter.setSortsOnUpdates(true)
selectionModel = distrustedTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({
int selectedRow = getSelectedTrustTablesRow("distrusted-table")
if (selectedRow < 0) {
model.markTrustedButtonEnabled = false
model.markNeutralFromDistrustedButtonEnabled = false
} else {
model.markTrustedButtonEnabled = true
model.markNeutralFromDistrustedButtonEnabled = true
}
})
}
private static void showPopupMenu(JPopupMenu menu, MouseEvent event) {

View File

@@ -83,10 +83,12 @@ class TrustListView {
def trustedTable = builder.getVariable("trusted-table")
trustedTable.rowSorter.addRowSorterListener({evt -> sortEvents["trusted-table"] = evt})
trustedTable.rowSorter.setSortsOnUpdates(true)
trustedTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
def distrustedTable = builder.getVariable("distrusted-table")
distrustedTable.rowSorter.addRowSorterListener({evt -> sortEvents["distrusted-table"] = evt})
distrustedTable.rowSorter.setSortsOnUpdates(true)
distrustedTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
dialog.getContentPane().add(mainPanel)
@@ -110,4 +112,9 @@ class TrustListView {
selectedRow = table.rowSorter.convertRowIndexToModel(selectedRow)
selectedRow
}
void fireUpdate(String tableName) {
def table = builder.getVariable(tableName)
table.model.fireTableDataChanged()
}
}

View File

@@ -11,6 +11,7 @@ import net.i2p.client.I2PSession
import net.i2p.client.I2PSessionMuxedListener
import net.i2p.client.datagram.I2PDatagramDissector
import net.i2p.client.datagram.I2PDatagramMaker
import net.i2p.crypto.SigType
import net.i2p.util.SystemVersion
import net.i2p.data.*
@@ -43,7 +44,7 @@ public class HostCache {
def session
if (!keyfile.exists()) {
def os = new FileOutputStream(keyfile);
myDest = i2pClient.createDestination(os)
myDest = i2pClient.createDestination(os, SigType.EdDSA_SHA512_Ed25519)
os.close()
println "No key.dat file was found, so creating a new destination."
println "This is the destination you want to give out for your new HostCache"
@@ -63,6 +64,9 @@ public class HostCache {
Timer timer = new Timer("timer", true)
timer.schedule({hostPool.age()} as TimerTask, 1000,1000)
timer.schedule({crawler.startCrawl()} as TimerTask, 10000, 10000)
File verified = new File("verified.json")
File unverified = new File("unverified.json")
timer.schedule({hostPool.serialize(verified, unverified)} as TimerTask, 10000, 60 * 60 * 1000)
session.addMuxedSessionListener(new Listener(hostPool: hostPool, toReturn: 2, crawler: crawler),
I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY)

View File

@@ -2,6 +2,8 @@ package com.muwire.hostcache
import java.util.stream.Collectors
import groovy.json.JsonOutput
class HostPool {
final def maxFailures
@@ -74,4 +76,25 @@ class HostPool {
}
}
}
synchronized void serialize(File verifiedFile, File unverifiedFile) {
write(verifiedFile, verified.values())
write(unverifiedFile, unverified.values())
}
private void write(File target, Collection hosts) {
JsonOutput jsonOutput = new JsonOutput()
target.withPrintWriter { writer ->
hosts.each {
def json = [:]
json.destination = it.destination.toBase64()
json.verifyTime = it.verifyTime
json.leafSlots = it.leafSlots
json.peerSlots = it.peerSlots
json.verificationFailures = it.verificationFailures
def str = jsonOutput.toJson(json)
writer.println(str)
}
}
}
}