Compare commits

...

74 Commits

Author SHA1 Message Date
ed3943c1af 0.0.8 for UI tweaks and sanitization 2019-06-04 18:01:08 +01:00
e195141a27 simpler sanitization 2019-06-04 17:58:19 +01:00
bb02fdbee9 do not use regex in sanitization 2019-06-04 17:46:41 +01:00
6e3a2c0d08 update split pattern 2019-06-04 17:30:55 +01:00
bd5fecc19d fix 2019-06-04 17:04:24 +01:00
d5db49fa79 initialize core 2019-06-04 16:56:58 +01:00
f2ea8619bb CLI project 2019-06-04 16:46:32 +01:00
b129e79196 do not count finished workers in total count 2019-06-04 16:22:48 +01:00
404d5b60bc format length in shared file stable an resize columns 2019-06-04 14:05:33 +01:00
de2753ac50 preferred sizes for download table columns 2019-06-04 13:35:18 +01:00
2d53999c8e only show download speed if downloading 2019-06-04 13:23:48 +01:00
5aecf72d6f format download speed 2019-06-04 13:19:14 +01:00
a574a67ec6 format file size 2019-06-04 13:15:24 +01:00
6b5ad969b7 pass logging properties 2019-06-04 13:00:10 +01:00
617209c4e4 column widths tweaks 2019-06-04 12:46:48 +01:00
16b475bd9a 0.0.7 for multi-source downloads 2019-06-04 04:17:29 +01:00
3cea1870cd multisource downloads, untested 2019-06-04 03:30:55 +01:00
e7240dcb6f keep track of claimed pieces in preparation for multi-source downloads 2019-06-04 02:18:30 +01:00
c91440cbfc config option for update check interval 2019-06-03 23:30:39 +01:00
294605f5c7 basic update notification 2019-06-03 23:23:07 +01:00
986caf3a75 backend for checking updates 2019-06-03 23:11:03 +01:00
8524d5309f typo 2019-06-03 21:53:51 +01:00
48b3ac2b4a wip on update server 2019-06-03 21:50:46 +01:00
18f21dc247 update server 2019-06-03 21:47:31 +01:00
e69a5eac18 0.0.6 2019-06-03 18:30:27 +01:00
6e0f1778b7 rudimentary speed gauge 2019-06-03 18:02:10 +01:00
abbb741d73 show the number of sources for a result, counted by infohash 2019-06-03 17:21:08 +01:00
07dfc0a1d1 destroy mvc group on options window close 2019-06-03 15:33:16 +01:00
00c12cfd49 hook up download retry logic 2019-06-03 15:02:04 +01:00
1ee389ff91 options dialog 2019-06-03 14:40:32 +01:00
3642736cfe options dialog, wip 2019-06-03 11:32:34 +01:00
b6f7f51476 verify X-Persona header if present 2019-06-03 08:12:33 +01:00
4c21f2d5ae show full persona in searches 2019-06-03 08:06:51 +01:00
9e0d52d548 show source in incoming searches 2019-06-03 07:43:28 +01:00
fad01603de fix replyTo field 2019-06-03 07:35:09 +01:00
da007795fb learn about new hosts from incoming connections too 2019-06-03 07:27:12 +01:00
881d755dd3 update test work with personas 2019-06-02 22:47:43 +01:00
bc3b6f500f 0.0.5 for trust panel 2019-06-02 12:18:44 +01:00
8f8710801c update any result tabs on trust events 2019-06-02 12:16:28 +01:00
43f3cf9b7a small ui tweak 2019-06-02 12:00:14 +01:00
6fe4155678 delete accidental commit 2019-06-02 11:57:15 +01:00
32f944a089 trust panel ui 2019-06-02 11:56:19 +01:00
b19b5ef315 Fix for java 9+ #1 2019-06-02 10:04:27 +01:00
5138935c20 add options for portable installation, issue #2 2019-06-02 09:33:28 +01:00
ba596af778 Trust panel, wip 2019-06-02 05:40:44 +01:00
0f4533c867 persist personas in trust files instead of destinations 2019-06-02 05:12:14 +01:00
727834390c slightly better looking message 2019-06-02 04:18:15 +01:00
c51e3874da show a message instead of search bar while disconnected 2019-06-02 04:12:11 +01:00
d18a618575 focus on the tab of the new search 2019-06-02 03:54:34 +01:00
15508f417d hack to add some horizontal space 2019-06-02 01:33:53 +01:00
44dad55178 update test 2019-06-02 01:28:00 +01:00
5c17e77190 change groovy version to match griffon 2019-06-02 01:20:55 +01:00
de856cd085 canonize search terms 2019-06-02 00:42:18 +01:00
d2533cc4d6 retry failed downloads, every 15 minutes by default 2019-06-02 00:22:33 +01:00
f41cc39659 show who is downloading 2019-06-01 21:53:14 +01:00
656b62fc2e 0.0.4 with download retry 2019-06-01 18:31:36 +01:00
13b3f0f63b retry implemented 2019-06-01 18:30:30 +01:00
98ea8154a5 store done pieces on disk to enable resume 2019-06-01 18:09:14 +01:00
82377aa9df hook up cancel button 2019-06-01 17:44:52 +01:00
bd2368e23a cancelled downloader state 2019-06-01 17:31:18 +01:00
70078c309b add cancel and retry buttons, not hooked up yet 2019-06-01 17:30:29 +01:00
15a0eda713 preserve selection in downloads table 2019-06-01 17:09:23 +01:00
9645716e18 prevent rare stacktraces on shutdown 2019-06-01 16:55:37 +01:00
03d6af39ed icon for closing tabs 2019-06-01 16:43:05 +01:00
9435cb003b Show warning if cannot find I2P router 2019-06-01 16:36:23 +01:00
63399803d5 ui tweaks 2019-06-01 15:59:55 +01:00
4d6541030f disable system l&f on osx 2019-06-01 14:55:17 +01:00
16c51e7cd6 add a failed download state 2019-06-01 14:14:20 +01:00
9d75550b6f do not show local searches in monitor 2019-06-01 13:48:12 +01:00
1996681677 incoming searches monitor 2019-06-01 13:44:46 +01:00
9dac1891b2 connection monitor 2019-06-01 13:32:40 +01:00
1255ac936b close connections on shutdown 2019-06-01 13:04:22 +01:00
2db3276b07 fix rare NPE on shutdown 2019-06-01 13:03:42 +01:00
7e3b0795af disable buttons if no row is selected 2019-06-01 12:23:20 +01:00
51 changed files with 1392 additions and 149 deletions

View File

@ -3,7 +3,7 @@ subprojects {
dependencies {
compile 'net.i2p:i2p:0.9.40'
compile 'org.codehaus.groovy:groovy-all:2.5.7'
compile 'org.codehaus.groovy:groovy-all:2.4.15'
}
compileGroovy {

7
cli/build.gradle Normal file
View File

@ -0,0 +1,7 @@
apply plugin : 'application'
mainClassName = 'com.muwire.cli.Cli'
dependencies {
compile project(":core")
}

View File

@ -0,0 +1,42 @@
package com.muwire.cli
import com.muwire.core.Core
import com.muwire.core.MuWireSettings
class Cli {
public static void main(String[] args) {
def home = System.getProperty("user.home") + File.separator + ".MuWire"
home = new File(home)
if (!home.exists())
home.mkdirs()
def propsFile = new File(home,"MuWire.properties")
if (!propsFile.exists()) {
println "create props file ${propsFile.getAbsoluteFile()} before launching MuWire"
System.exit(1)
}
def props = new Properties()
propsFile.withInputStream { props.load(it) }
props = new MuWireSettings(props)
Core core
try {
core = new Core(props, home, "0.0.8")
} catch (Exception bad) {
bad.printStackTrace(System.out)
println "Failed to initialize core, exiting"
System.exit(1)
}
core.startServices()
// now we begin
println "MuWire is ready"
println "Enter a file containing list of files to share"
def reader = new BufferedReader(new InputStreamReader(System.in))
def filesList = reader.readLine()
Thread.sleep(Integer.MAX_VALUE)
}
}

View File

@ -10,4 +10,6 @@ class Constants {
public static final int MAX_HEADERS = 16
public static final float DOWNLOAD_SEQUENTIAL_RATIO = 0.8f
public static final String SPLIT_PATTERN = "[\\.,_-]"
}

View File

@ -32,6 +32,7 @@ import com.muwire.core.search.SearchEvent
import com.muwire.core.search.SearchManager
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustService
import com.muwire.core.update.UpdateClient
import com.muwire.core.upload.UploadManager
import com.muwire.core.util.MuWireLogManager
@ -54,17 +55,20 @@ public class Core {
final EventBus eventBus
final Persona me
final File home
private final TrustService trustService
private final PersisterService persisterService
private final HostCache hostCache
private final ConnectionManager connectionManager
private final CacheClient cacheClient
private final UpdateClient updateClient
private final ConnectionAcceptor connectionAcceptor
private final ConnectionEstablisher connectionEstablisher
private final HasherService hasherService
public Core(MuWireSettings props, File home) {
public Core(MuWireSettings props, File home, String myVersion) {
this.home = home
log.info "Initializing I2P context"
I2PAppContext.getGlobalContext().logManager()
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
@ -118,8 +122,8 @@ public class Core {
eventBus = new EventBus()
log.info("initializing trust service")
File goodTrust = new File(home, "trust.good")
File badTrust = new File(home, "trust.bad")
File goodTrust = new File(home, "trusted")
File badTrust = new File(home, "distrusted")
trustService = new TrustService(goodTrust, badTrust, 5000)
eventBus.register(TrustEvent.class, trustService)
@ -152,6 +156,9 @@ public class Core {
log.info("initializing cache client")
cacheClient = new CacheClient(eventBus,hostCache, connectionManager, i2pSession, props, 10000)
log.info("initializing update client")
updateClient = new UpdateClient(eventBus, i2pSession, myVersion, props)
log.info("initializing connector")
I2PConnector i2pConnector = new I2PConnector(socketManager)
@ -164,7 +171,7 @@ public class Core {
eventBus.register(ResultsEvent.class, searchManager)
log.info("initializing download manager")
DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector)
DownloadManager downloadManager = new DownloadManager(eventBus, i2pConnector, new File(home, "incompletes"), me)
eventBus.register(UIDownloadEvent.class, downloadManager)
log.info("initializing upload manager")
@ -195,6 +202,11 @@ public class Core {
connectionAcceptor.start()
connectionEstablisher.start()
hostCache.waitForLoad()
updateClient.start()
}
public void shutdown() {
connectionManager.shutdown()
}
static main(args) {
@ -221,7 +233,7 @@ public class Core {
}
}
Core core = new Core(props, home)
Core core = new Core(props, home, "0.0.8")
core.startServices()
// ... at the end, sleep or execute script

View File

@ -6,6 +6,8 @@ class MuWireSettings {
final boolean isLeaf
boolean allowUntrusted
int downloadRetryInterval
int updateCheckInterval
String nickname
File downloadLocation
String sharedFiles
@ -23,6 +25,8 @@ class MuWireSettings {
downloadLocation = new File((String)props.getProperty("downloadLocation",
System.getProperty("user.home")))
sharedFiles = props.getProperty("sharedFiles")
downloadRetryInterval = Integer.parseInt(props.getProperty("downloadRetryInterval","15"))
updateCheckInterval = Integer.parseInt(props.getProperty("updateCheckInterval","36"))
}
void write(OutputStream out) throws IOException {
@ -32,6 +36,8 @@ class MuWireSettings {
props.setProperty("crawlerResponse", crawlerResponse.toString())
props.setProperty("nickname", nickname)
props.setProperty("downloadLocation", downloadLocation.getAbsolutePath())
props.setProperty("downloadRetryInterval", String.valueOf(downloadRetryInterval))
props.setProperty("updateCheckInterval", String.valueOf(updateCheckInterval))
if (sharedFiles != null)
props.setProperty("sharedFiles", sharedFiles)
props.store(out, "")

View File

@ -2,6 +2,7 @@ package com.muwire.core
import net.i2p.crypto.DSAEngine
import net.i2p.crypto.SigType
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.data.Signature
import net.i2p.data.SigningPublicKey
@ -14,6 +15,7 @@ public class Persona {
private final Destination destination
private final byte[] sig
private volatile String humanReadableName
private volatile String base64
private volatile byte[] payload
public Persona(InputStream personaStream) throws IOException, InvalidSignatureException {
@ -59,6 +61,15 @@ public class Persona {
humanReadableName
}
public String toBase64() {
if (base64 == null) {
def baos = new ByteArrayOutputStream()
write(baos)
base64 = Base64.encode(baos.toByteArray())
}
base64
}
@Override
public int hashCode() {
name.hashCode() ^ destination.hashCode()

View File

@ -6,6 +6,7 @@ import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level
import com.muwire.core.EventBus
import com.muwire.core.Persona
import com.muwire.core.hostcache.HostCache
import com.muwire.core.hostcache.HostDiscoveredEvent
import com.muwire.core.search.QueryEvent
@ -14,6 +15,7 @@ import com.muwire.core.trust.TrustLevel
import com.muwire.core.trust.TrustService
import groovy.util.logging.Log
import net.i2p.data.Base64
import net.i2p.data.Destination
@Log
@ -82,7 +84,6 @@ abstract class Connection implements Closeable {
read()
}
} catch (SocketTimeoutException e) {
close()
} catch (Exception e) {
log.log(Level.WARNING,"unhandled exception in reader",e)
} finally {
@ -121,9 +122,10 @@ abstract class Connection implements Closeable {
query.version = 1
query.uuid = e.searchEvent.getUuid()
query.firstHop = e.firstHop
// TODO: first hop figure out
query.keywords = e.searchEvent.getSearchTerms()
query.replyTo = e.getReceivedOn().toBase64()
query.replyTo = e.replyTo.toBase64()
if (e.originator != null)
query.originator = e.originator.toBase64()
messages.put(query)
}
@ -159,11 +161,22 @@ abstract class Connection implements Closeable {
}
// TODO: add option to respond only to trusted peers
Persona originator = null
if (search.originator != null) {
originator = new Persona(new ByteArrayInputStream(Base64.decode(search.originator)))
if (originator.destination != replyTo) {
log.info("originator doesn't match destination")
return
}
}
SearchEvent searchEvent = new SearchEvent(searchTerms : search.keywords,
searchHash : search.infohash,
uuid : uuid)
QueryEvent event = new QueryEvent ( searchEvent : searchEvent,
replyTo : replyTo,
originator : originator,
receivedOn : endpoint.destination,
firstHop : search.firstHop )
eventBus.publish(event)

View File

@ -40,7 +40,7 @@ abstract class ConnectionManager {
void onTrustEvent(TrustEvent e) {
if (e.level == TrustLevel.DISTRUSTED)
drop(e.destination)
drop(e.persona.destination)
}
abstract void drop(Destination d)
@ -58,6 +58,8 @@ abstract class ConnectionManager {
abstract void onConnectionEvent(ConnectionEvent e)
abstract void onDisconnectionEvent(DisconnectionEvent e)
abstract void shutdown()
protected void sendPings() {
final long now = System.currentTimeMillis()

View File

@ -71,4 +71,8 @@ class LeafConnectionManager extends ConnectionManager {
log.severe("removed destination not present in connection manager ${e.destination.toBase32()}")
}
@Override
void shutdown() {
}
}

View File

@ -20,7 +20,7 @@ class UltrapeerConnectionManager extends ConnectionManager {
final Map<Destination, PeerConnection> peerConnections = new ConcurrentHashMap()
final Map<Destination, LeafConnection> leafConnections = new ConcurrentHashMap()
UltrapeerConnectionManager() {}
public UltrapeerConnectionManager(EventBus eventBus, Persona me, int maxPeers, int maxLeafs,
@ -100,6 +100,14 @@ class UltrapeerConnectionManager extends ConnectionManager {
if (removed == null)
log.severe("Removed connection not present in either leaf or peer map ${e.destination.toBase32()}")
}
@Override
void shutdown() {
peerConnections.each {k,v -> v.close() }
leafConnections.each {k,v -> v.close() }
peerConnections.clear()
leafConnections.clear()
}
void forwardQueryToLeafs(QueryEvent e) {

View File

@ -0,0 +1,25 @@
package com.muwire.core.download
class BadHashException extends Exception {
public BadHashException() {
super();
}
public BadHashException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
public BadHashException(String message, Throwable cause) {
super(message, cause);
}
public BadHashException(String message) {
super(message);
}
public BadHashException(Throwable cause) {
super(cause);
}
}

View File

@ -1,7 +1,12 @@
package com.muwire.core.download
import com.muwire.core.connection.I2PConnector
import net.i2p.data.Base64
import net.i2p.data.Destination
import com.muwire.core.EventBus
import com.muwire.core.Persona
import java.util.concurrent.Executor
import java.util.concurrent.Executors
@ -11,10 +16,17 @@ public class DownloadManager {
private final EventBus eventBus
private final I2PConnector connector
private final Executor executor
private final File incompletes
private final Persona me
public DownloadManager(EventBus eventBus, I2PConnector connector) {
public DownloadManager(EventBus eventBus, I2PConnector connector, File incompletes, Persona me) {
this.eventBus = eventBus
this.connector = connector
this.incompletes = incompletes
this.me = me
incompletes.mkdir()
this.executor = Executors.newCachedThreadPool({ r ->
Thread rv = new Thread(r)
rv.setName("download-worker")
@ -25,9 +37,24 @@ public class DownloadManager {
public void onUIDownloadEvent(UIDownloadEvent e) {
def downloader = new Downloader(e.target, e.result.size,
e.result.infohash, e.result.pieceSize, connector, e.result.sender.destination)
def size = e.result[0].size
def infohash = e.result[0].infohash
def pieceSize = e.result[0].pieceSize
Set<Destination> destinations = new HashSet<>()
e.result.each {
destinations.add(it.sender.destination)
}
def downloader = new Downloader(this, me, e.target, size,
infohash, pieceSize, connector, destinations,
incompletes)
executor.execute({downloader.download()} as Runnable)
eventBus.publish(new DownloadStartedEvent(downloader : downloader))
}
void resume(Downloader downloader) {
executor.execute({downloader.download() as Runnable})
}
}

View File

@ -20,7 +20,10 @@ import java.security.NoSuchAlgorithmException
@Log
class DownloadSession {
private final Pieces pieces
private static int SAMPLES = 10
private final String meB64
private final Pieces downloaded, claimed
private final InfoHash infoHash
private final Endpoint endpoint
private final File file
@ -28,11 +31,16 @@ class DownloadSession {
private final long fileLength
private final MessageDigest digest
private final ArrayDeque<Long> timestamps = new ArrayDeque<>(SAMPLES)
private final ArrayDeque<Integer> reads = new ArrayDeque<>(SAMPLES)
private ByteBuffer mapped
DownloadSession(Pieces pieces, InfoHash infoHash, Endpoint endpoint, File file,
DownloadSession(String meB64, Pieces downloaded, Pieces claimed, InfoHash infoHash, Endpoint endpoint, File file,
int pieceSize, long fileLength) {
this.pieces = pieces
this.meB64 = meB64
this.downloaded = downloaded
this.claimed = claimed
this.endpoint = endpoint
this.infoHash = infoHash
this.file = file
@ -46,11 +54,31 @@ class DownloadSession {
}
}
public void request() throws IOException {
/**
* @return if the request will proceed. The only time it may not
* is if all the pieces have been claimed by other sessions.
* @throws IOException
*/
public boolean request() throws IOException {
OutputStream os = endpoint.getOutputStream()
InputStream is = endpoint.getInputStream()
int piece = pieces.getRandomPiece()
int piece
while(true) {
piece = downloaded.getRandomPiece()
if (claimed.isMarked(piece)) {
if (downloaded.donePieces() + claimed.donePieces() == downloaded.nPieces) {
log.info("all pieces claimed")
return false
}
continue
}
break
}
claimed.markDownloaded(piece)
log.info("will download piece $piece")
long start = piece * pieceSize
long end = Math.min(fileLength, start + pieceSize) - 1
long length = end - start + 1
@ -60,7 +88,8 @@ class DownloadSession {
FileChannel channel
try {
os.write("GET $root\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Range: $start-$end\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("Range: $start-$end\r\n".getBytes(StandardCharsets.US_ASCII))
os.write("X-Persona: $meB64\r\n\r\n".getBytes(StandardCharsets.US_ASCII))
os.flush()
String code = readTillRN(is)
if (code.startsWith("404 ")) {
@ -119,6 +148,13 @@ 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)
}
}
@ -127,16 +163,15 @@ class DownloadSession {
byte [] hash = digest.digest()
byte [] expected = new byte[32]
System.arraycopy(infoHash.getHashList(), piece * 32, expected, 0, 32)
if (hash != expected) {
log.warning("hash mismatch")
endpoint.close()
return
}
if (hash != expected)
throw new BadHashException()
pieces.markDownloaded(piece)
downloaded.markDownloaded(piece)
} finally {
claimed.clear(piece)
try { channel?.close() } catch (IOException ignore) {}
}
return true
}
synchronized int positionInPiece() {
@ -144,4 +179,13 @@ class DownloadSession {
return 0
mapped.position()
}
synchronized int speed() {
if (timestamps.size() < SAMPLES)
return 0
long interval = timestamps.last - timestamps.first
int totalRead = 0
reads.each { totalRead += it }
(int)(totalRead * 1000.0 / interval)
}
}

View File

@ -1,34 +1,59 @@
package com.muwire.core.download
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.connection.Endpoint
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.logging.Level
import com.muwire.core.Constants
import com.muwire.core.connection.I2PConnector
import groovy.util.logging.Log
import net.i2p.data.Destination
@Log
public class Downloader {
public enum DownloadState { CONNECTING, DOWNLOADING, FINISHED }
public enum DownloadState { CONNECTING, DOWNLOADING, FAILED, CANCELLED, FINISHED }
private enum WorkerState { CONNECTING, DOWNLOADING, FINISHED}
private static final ExecutorService executorService = Executors.newCachedThreadPool({r ->
Thread rv = new Thread(r)
rv.setName("download worker")
rv.setDaemon(true)
rv
})
private final DownloadManager downloadManager
private final Persona me
private final File file
private final Pieces pieces
private final Pieces downloaded, claimed
private final long length
private final InfoHash infoHash
private final int pieceSize
private final I2PConnector connector
private final Destination destination
private final Set<Destination> destinations
private final int nPieces
private final File piecesFile
private final Map<Destination, DownloadWorker> activeWorkers = new ConcurrentHashMap<>()
private Endpoint endpoint
private volatile DownloadSession currentSession
private volatile DownloadState currentState
public Downloader(File file, long length, InfoHash infoHash, int pieceSizePow2, I2PConnector connector, Destination destination) {
private volatile boolean cancelled
public Downloader(DownloadManager downloadManager, Persona me, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
File incompletes) {
this.me = me
this.downloadManager = downloadManager
this.file = file
this.infoHash = infoHash
this.length = length
this.connector = connector
this.destination = destination
this.destinations = destinations
this.piecesFile = new File(incompletes, file.getName()+".pieces")
this.pieceSize = 1 << pieceSizePow2
int nPieces
@ -38,32 +63,143 @@ public class Downloader {
nPieces = length / pieceSize + 1
this.nPieces = nPieces
pieces = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
currentState = DownloadState.CONNECTING
downloaded = new Pieces(nPieces, Constants.DOWNLOAD_SEQUENTIAL_RATIO)
claimed = new Pieces(nPieces)
}
void download() {
Endpoint endpoint = connector.connect(destination)
currentState = DownloadState.DOWNLOADING
while(!pieces.isComplete()) {
currentSession = new DownloadSession(pieces, infoHash, endpoint, file, pieceSize, length)
currentSession.request()
readPieces()
destinations.each {
if (it != me.destination) {
def worker = new DownloadWorker(it)
activeWorkers.put(it, worker)
executorService.submit(worker)
}
}
}
void readPieces() {
if (!piecesFile.exists())
return
piecesFile.withReader {
int piece = Integer.parseInt(it.readLine())
downloaded.markDownloaded(piece)
}
}
void writePieces() {
piecesFile.withPrintWriter { writer ->
downloaded.getDownloaded().each { piece ->
writer.println(piece)
}
}
currentState = DownloadState.FINISHED
endpoint.close()
}
public long donePieces() {
pieces.donePieces()
downloaded.donePieces()
}
public int positionInPiece() {
if (currentSession == null)
return 0
currentSession.positionInPiece()
public int speed() {
int total = 0
if (getCurrentState() == DownloadState.DOWNLOADING) {
activeWorkers.values().each {
total += it.speed()
}
}
total
}
public DownloadState getCurrentState() {
currentState
if (cancelled)
return DownloadState.CANCELLED
boolean allFinished = true
activeWorkers.values().each {
allFinished &= it.currentState == WorkerState.FINISHED
}
if (allFinished) {
if (downloaded.isComplete())
return DownloadState.FINISHED
return DownloadState.FAILED
}
// if at least one is downloading...
boolean oneDownloading = false
activeWorkers.values().each {
if (it.currentState == WorkerState.DOWNLOADING) {
oneDownloading = true
return
}
}
if (oneDownloading)
return DownloadState.DOWNLOADING
return DownloadState.CONNECTING
}
public void cancel() {
cancelled = true
activeWorkers.values().each {
it.cancel()
}
}
public int activeWorkers() {
int active = 0
activeWorkers.values().each {
if (it.currentState != WorkerState.FINISHED)
active++
}
active
}
public void resume() {
downloadManager.resume(this)
}
class DownloadWorker implements Runnable {
private final Destination destination
private volatile WorkerState currentState
private volatile Thread downloadThread
private Endpoint endpoint
private volatile DownloadSession currentSession
DownloadWorker(Destination destination) {
this.destination = destination
}
public void run() {
downloadThread = Thread.currentThread()
currentState = WorkerState.CONNECTING
Endpoint endpoint = null
try {
endpoint = connector.connect(destination)
currentState = WorkerState.DOWNLOADING
boolean requestPerformed
while(!downloaded.isComplete()) {
currentSession = new DownloadSession(me.toBase64(), downloaded, claimed, infoHash, endpoint, file, pieceSize, length)
requestPerformed = currentSession.request()
if (!requestPerformed)
break
writePieces()
}
} catch (Exception bad) {
log.log(Level.WARNING,"Exception while downloading",bad)
} finally {
currentState = WorkerState.FINISHED
endpoint?.close()
}
}
int speed() {
if (currentSession == null)
return 0
currentSession.speed()
}
void cancel() {
downloadThread?.interrupt()
}
}
}

View File

@ -28,19 +28,36 @@ class Pieces {
while(true) {
int start = random.nextInt(nPieces)
while(bitSet.get(start) && ++start < nPieces);
if (bitSet.get(start))
continue
return start
}
}
def getDownloaded() {
def rv = []
for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i+1)) {
rv << i
}
rv
}
synchronized void markDownloaded(int piece) {
bitSet.set(piece)
}
synchronized void clear(int piece) {
bitSet.clear(piece)
}
synchronized boolean isComplete() {
bitSet.cardinality() == nPieces
}
synchronized boolean isMarked(int piece) {
bitSet.get(piece)
}
synchronized int donePieces() {
bitSet.cardinality()
}

View File

@ -5,6 +5,6 @@ import com.muwire.core.search.UIResultEvent
class UIDownloadEvent extends Event {
UIResultEvent result
UIResultEvent[] result
File target
}

View File

@ -65,7 +65,7 @@ class CacheClient {
options.setSendLeaseSet(true)
CacheServers.getCacheServers().each {
log.info "Querying hostcache ${it.toBase32()}"
session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 0, 0, options)
session.sendMessage(it, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 1, 0, options)
}
}

View File

@ -55,7 +55,7 @@ class HostCache extends Service {
}
void onConnectionEvent(ConnectionEvent e) {
if (e.incoming || e.leaf)
if (e.leaf)
return
Destination dest = e.endpoint.destination
Host host = hosts.get(dest)

View File

@ -1,6 +1,7 @@
package com.muwire.core.search
import com.muwire.core.Event
import com.muwire.core.Persona
import net.i2p.data.Destination
@ -9,6 +10,7 @@ class QueryEvent extends Event {
SearchEvent searchEvent
boolean firstHop
Destination replyTo
Persona originator
Destination receivedOn
}

View File

@ -1,5 +1,6 @@
package com.muwire.core.search
import com.muwire.core.Constants
class SearchIndex {
@ -31,7 +32,7 @@ class SearchIndex {
}
private static String[] split(String source) {
source = source.replaceAll("[\\.,_-]", " ")
source = source.replaceAll(Constants.SPLIT_PATTERN, " ").toLowerCase()
source.split(" ")
}

View File

@ -1,11 +1,10 @@
package com.muwire.core.trust
import com.muwire.core.Event
import net.i2p.data.Destination
import com.muwire.core.Persona
class TrustEvent extends Event {
Destination destination
Persona persona
TrustLevel level
}

View File

@ -1,7 +1,11 @@
package com.muwire.core.trust
import java.util.concurrent.ConcurrentHashMap
import com.muwire.core.Persona
import com.muwire.core.Service
import net.i2p.data.Base64
import net.i2p.data.Destination
import net.i2p.util.ConcurrentHashSet
@ -10,8 +14,8 @@ class TrustService extends Service {
final File persistGood, persistBad
final long persistInterval
final Set<Destination> good = new ConcurrentHashSet<>()
final Set<Destination> bad = new ConcurrentHashSet<>()
final Map<Destination, Persona> good = new ConcurrentHashMap<>()
final Map<Destination, Persona> bad = new ConcurrentHashMap<>()
final Timer timer
@ -35,12 +39,16 @@ class TrustService extends Service {
void load() {
if (persistGood.exists()) {
persistGood.eachLine {
good.add(new Destination(it))
byte [] decoded = Base64.decode(it)
Persona persona = new Persona(new ByteArrayInputStream(decoded))
good.put(persona.destination, persona)
}
}
if (persistBad.exists()) {
persistBad.eachLine {
bad.add(new Destination(it))
byte [] decoded = Base64.decode(it)
Persona persona = new Persona(new ByteArrayInputStream(decoded))
bad.put(persona.destination, persona)
}
}
timer.schedule({persist()} as TimerTask, persistInterval, persistInterval)
@ -50,22 +58,22 @@ class TrustService extends Service {
private void persist() {
persistGood.delete()
persistGood.withPrintWriter { writer ->
good.each {
writer.println it.toBase64()
good.each {k,v ->
writer.println v.toBase64()
}
}
persistBad.delete()
persistBad.withPrintWriter { writer ->
bad.each {
writer.println it.toBase64()
bad.each { k,v ->
writer.println v.toBase64()
}
}
}
TrustLevel getLevel(Destination dest) {
if (good.contains(dest))
if (good.containsKey(dest))
return TrustLevel.TRUSTED
else if (bad.contains(dest))
else if (bad.containsKey(dest))
return TrustLevel.DISTRUSTED
TrustLevel.NEUTRAL
}
@ -73,16 +81,16 @@ class TrustService extends Service {
void onTrustEvent(TrustEvent e) {
switch(e.level) {
case TrustLevel.TRUSTED:
bad.remove(e.destination)
good.add(e.destination)
bad.remove(e.persona.destination)
good.put(e.persona.destination, e.persona)
break
case TrustLevel.DISTRUSTED:
good.remove(e.destination)
bad.add(e.destination)
good.remove(e.persona.destination)
bad.put(e.persona.destination, e.persona)
break
case TrustLevel.NEUTRAL:
good.remove(e.destination)
bad.remove(e.destination)
good.remove(e.persona.destination)
bad.remove(e.persona.destination)
break
}
}

View File

@ -0,0 +1,8 @@
package com.muwire.core.update
import com.muwire.core.Event
class UpdateAvailableEvent extends Event {
String version
String signer
}

View File

@ -0,0 +1,132 @@
package com.muwire.core.update
import java.util.logging.Level
import com.muwire.core.EventBus
import com.muwire.core.MuWireSettings
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import groovy.util.logging.Log
import net.i2p.client.I2PSession
import net.i2p.client.I2PSessionMuxedListener
import net.i2p.client.SendMessageOptions
import net.i2p.client.datagram.I2PDatagramDissector
import net.i2p.client.datagram.I2PDatagramMaker
import net.i2p.util.VersionComparator
@Log
class UpdateClient {
final EventBus eventBus
final I2PSession session
final String myVersion
final MuWireSettings settings
private final Timer timer
private long lastUpdateCheckTime
UpdateClient(EventBus eventBus, I2PSession session, String myVersion, MuWireSettings settings) {
this.eventBus = eventBus
this.session = session
this.myVersion = myVersion
this.settings = settings
timer = new Timer("update-client",true)
}
void start() {
session.addMuxedSessionListener(new Listener(), I2PSession.PROTO_DATAGRAM, 2)
timer.schedule({checkUpdate()} as TimerTask, 30000, 60 * 60 * 1000)
}
void stop() {
timer.cancel()
}
private void checkUpdate() {
final long now = System.currentTimeMillis()
if (lastUpdateCheckTime > 0) {
if (now - lastUpdateCheckTime < settings.updateCheckInterval * 60 * 60 * 1000)
return
}
lastUpdateCheckTime = now
log.info("checking for update")
def ping = [version : 1, myVersion : myVersion]
ping = JsonOutput.toJson(ping)
def maker = new I2PDatagramMaker(session)
ping = maker.makeI2PDatagram(ping.bytes)
def options = new SendMessageOptions()
options.setSendLeaseSet(true)
session.sendMessage(UpdateServers.UPDATE_SERVER, ping, 0, ping.length, I2PSession.PROTO_DATAGRAM, 2, 0, options)
}
class Listener implements I2PSessionMuxedListener {
final JsonSlurper slurper = new JsonSlurper()
@Override
public void messageAvailable(I2PSession session, int msgId, long size) {
}
@Override
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
if (proto != I2PSession.PROTO_DATAGRAM) {
log.warning "Received unexpected protocol $proto"
return
}
def payload = session.receiveMessage(msgId)
def dissector = new I2PDatagramDissector()
try {
dissector.loadI2PDatagram(payload)
def sender = dissector.getSender()
if (sender != UpdateServers.UPDATE_SERVER) {
log.warning("received something not from update server " + sender.toBase32())
return
}
log.info("Received something from update server")
payload = dissector.getPayload()
payload = slurper.parse(payload)
if (payload.version == null) {
log.warning("version missing")
return
}
if (payload.signer == null) {
log.warning("signer missing")
}
if (VersionComparator.comp(myVersion, payload.version) >= 0) {
log.info("no new version available")
return
}
log.info("new version $payload.version available, publishing event")
eventBus.publish(new UpdateAvailableEvent(version : payload.version, signer : payload.signer))
} catch (Exception e) {
log.log(Level.WARNING,"Invalid datagram",e)
}
}
@Override
public void reportAbuse(I2PSession session, int severity) {
}
@Override
public void disconnected(I2PSession session) {
log.severe("I2P session disconnected")
}
@Override
public void errorOccurred(I2PSession session, String message, Throwable error) {
log.log(Level.SEVERE, message, error)
}
}
}

View File

@ -0,0 +1,7 @@
package com.muwire.core.update
import net.i2p.data.Destination
class UpdateServers {
static final Destination UPDATE_SERVER = new Destination("pSWieSRB3czCl3Zz4WpKp4Z8tjv-05zbogRDS7SEnKcSdWOupVwjzQ92GsgQh1VqgoSRk1F8dpZOnHxxz5HFy9D7ri0uFdkMyXdSKoB7IgkkvCfTAyEmeaPwSYnurF3Zk7u286E7YG2rZkQZgJ77tow7ZS0mxFB7Z0Ti-VkZ9~GeGePW~howwNm4iSQACZA0DyTpI8iv5j4I0itPCQRgaGziob~Vfvjk49nd8N4jtaDGo9cEcafikVzQ2OgBgYWL6LRbrrItwuGqsDvITUHWaElUYIDhRQYUq8gYiUA6rwAJputfhFU0J7lIxFR9vVY7YzRvcFckfr0DNI4VQVVlPnRPkUxQa--BlldMaCIppWugjgKLwqiSiHywKpSMlBWgY2z1ry4ueEBo1WEP-mEf88wRk4cFQBCKtctCQnIG2GsnATqTl-VGUAsuzeNWZiFSwXiTy~gQ094yWx-K06fFZUDt4CMiLZVhGlixiInD~34FCRC9LVMtFcqiFB2M-Ql2AAAA")
}

View File

@ -4,6 +4,7 @@ import java.nio.charset.StandardCharsets
import com.muwire.core.Constants
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import groovy.util.logging.Log
import net.i2p.data.Base64
@ -16,6 +17,7 @@ class Request {
InfoHash infoHash
Range range
Persona downloader
Map<String, String> headers
static Request parse(InfoHash infoHash, InputStream is) throws IOException {
@ -85,7 +87,13 @@ class Request {
if (start < 0 || end < start)
throw new IOException("Invalid range $start - $end")
new Request( infoHash : infoHash, range : new Range(start, end), headers : headers)
Persona downloader = null
if (headers.containsKey("X-Persona")) {
def encoded = headers["X-Persona"].trim()
def decoded = Base64.decode(encoded)
downloader = new Persona(new ByteArrayInputStream(decoded))
}
new Request( infoHash : infoHash, range : new Range(start, end), headers : headers, downloader : downloader)
}
}

View File

@ -62,6 +62,11 @@ public class UploadManager {
}
Request request = Request.parse(new InfoHash(infoHashRoot), e.getInputStream())
if (request.downloader != null && request.downloader.destination != e.destination) {
log.info("Downloader persona doesn't match their destination")
e.close()
return
}
Uploader uploader = new Uploader(sharedFiles.iterator().next().file, request, e)
eventBus.publish(new UploadEvent(uploader : uploader))
try {

View File

@ -0,0 +1,11 @@
package com.muwire.core
import net.i2p.data.Base64
class Personas {
private final String encoded1 = "AQADemFiO~pgSoEo8wQfwncYMvBQWkvPY9I7DYUllHp289UE~zBaLdbl~wbliktAUsW-S70f3UeYgHq34~c7zVuUQjgHZ506iG9hX8B9S3a9gQ3CSG0GuDpeNyiXmZkpHp5m8vT9PZ1zMWzxvzZY~fP9yKFKgO4yrso5I9~DGOPeyJZJ4BFsTJDERv41aZqjFLYUBDmeHGgg9RjYy~93h-nQMVYj9JSO3AgowW-ix49rtiKYIXHMa2PxWHUXkUHWJZtIZntNIDEFeMnPdzLxjAl8so2G6pDcTMZPLLwyb73Ee5ZVfxUynPqyp~fIGVP8Rl4rlaGFli2~ATGBz3XY54aObC~0p7us2JnWaTC~oQT5DVDM7gaOO885o-m8BB8b0duzMBelbdnMZFQJ5jIHVKxkC6Niw4fxTOoXTyOqQmVhtK-9xcwxMuN5DF9IewkR5bhpq5rgnfBP5zvyBaAHMq-d3TCOjTsZ-d3liB98xX5p8G5zmS7gfKArQtM5~CcK~AlX-lGLBQAEAAcAAN5MW1Tq983szfZgY1l8tQFqy8I9tdMf7vc1Ktj~TCIvXYw6AYMbMGy3S67FSPLZVmfHEMQKj2KLAdaRKQkHPAY"
private final String encoded2 = "AQAHemxhdGluYiN~3G-hPoBfJ04mhcC52lC6TYSwWxH-WNWno9Y35JS-WrXlnPsodZtwy96ttEaiKTg-hkRqMsaYKpWar1FwayR6qlo0pZCo5pQOLfR7GIM3~wde0JIBEp8BUpgzF1-QXLhuRG1t7tBbenW2tSgp5jQH61RI-c9flyUlOvf6nrhQMZ3aoviZ4aZW23Fx-ajYQBDk7PIxuyn8qYNwWy3kWOhGan05c54NnumS3XCzQWFDDPlADmco1WROeY9qrwwtmLM8lzDCEtJQXJlk~K5yLbyB63hmAeTK7J4iS6f9nnWv7TbB5r-Z3kC6D9TLYrQbu3h4AAxrqso45P8yHQtKUA4QJicS-6NJoBOnlCCU887wx2k9YSxxwNydlIxb1mZsX65Ke4uY0HDFokZHTzUcxvfLB6G~5JkSPDCyZz~2fREgW2-VXu7gokEdEugkuZRrsiQzyfAOOkv53ti5MzTbMOXinBskSb1vZyN2-XcZNaDJvEqUNj~qpfhe-ov2F7FuwQUABAAHAAAfqq-MneIqWBQY92-sy9Z0s~iQsq6lUFa~sYMdY-5o-94fF8a140dm-emF3rO8vuidUIPNaS-37Rl05mAKUCcB"
Persona persona1 = new Persona(new ByteArrayInputStream(Base64.decode(encoded1)))
Persona persona2 = new Persona(new ByteArrayInputStream(Base64.decode(encoded2)))
}

View File

@ -15,7 +15,7 @@ class DownloadSessionTest {
private File source, target
private InfoHash infoHash
private Endpoint endpoint
private Pieces pieces
private Pieces pieces, claimed
private String rootBase64
private DownloadSession session
@ -24,7 +24,7 @@ class DownloadSessionTest {
private InputStream fromDownloader, fromUploader
private OutputStream toDownloader, toUploader
private void initSession(int size) {
private void initSession(int size, def claimedPieces = []) {
Random r = new Random()
byte [] content = new byte[size]
r.nextBytes(content)
@ -48,6 +48,8 @@ class DownloadSessionTest {
else
nPieces = size / pieceSize + 1
pieces = new Pieces(nPieces)
claimed = new Pieces(nPieces)
claimedPieces.each {claimed.markDownloaded(it)}
fromDownloader = new PipedInputStream()
fromUploader = new PipedInputStream()
@ -55,7 +57,7 @@ class DownloadSessionTest {
toUploader = new PipedOutputStream(fromDownloader)
endpoint = new Endpoint(null, fromUploader, toUploader, null)
session = new DownloadSession(pieces, infoHash, endpoint, target, pieceSize, size)
session = new DownloadSession("",pieces, claimed, infoHash, endpoint, target, pieceSize, size)
downloadThread = new Thread( { session.request() } as Runnable)
downloadThread.setDaemon(true)
downloadThread.start()
@ -74,6 +76,7 @@ class DownloadSessionTest {
initSession(20)
assert "GET $rootBase64" == readTillRN(fromDownloader)
assert "Range: 0-19" == readTillRN(fromDownloader)
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes)
@ -95,6 +98,7 @@ class DownloadSessionTest {
assert "GET $rootBase64" == readTillRN(fromDownloader)
readTillRN(fromDownloader)
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes)
@ -122,6 +126,7 @@ class DownloadSessionTest {
assert (start == 0 && end == ((1 << pieceSize) - 1)) ||
(start == (1 << pieceSize) && end == (1 << pieceSize))
readTillRN(fromDownloader)
assert "" == readTillRN(fromDownloader)
toDownloader.write("200 OK\r\n".bytes)
@ -135,4 +140,29 @@ class DownloadSessionTest {
assert !pieces.isComplete()
assert 1 == pieces.donePieces()
}
@Test
public void testSmallFileClaimed() {
initSession(20, [0])
long now = System.currentTimeMillis()
downloadThread.join(100)
assert 100 > (System.currentTimeMillis() - now)
}
@Test
public void testClaimedPiecesAvoided() {
int pieceSize = FileHasher.getPieceSize(1)
int size = (1 << pieceSize) * 10
initSession(size, [1,2,3,4,5,6,7,8,9])
assert !claimed.isMarked(0)
assert "GET $rootBase64" == readTillRN(fromDownloader)
String range = readTillRN(fromDownloader)
def matcher = (range =~ /^Range: (\d+)-(\d+)$/)
int start = Integer.parseInt(matcher[0][1])
int end = Integer.parseInt(matcher[0][2])
assert claimed.isMarked(0)
assert start == 0 && end == (1 << pieceSize) - 1
}
}

View File

@ -5,14 +5,17 @@ import org.junit.Before
import org.junit.Test
import com.muwire.core.Destinations
import com.muwire.core.Persona
import com.muwire.core.Personas
import net.i2p.data.Base64
import net.i2p.data.Destination
class TrustServiceTest {
TrustService service
File persistGood, persistBad
Destinations dests = new Destinations()
Personas personas = new Personas()
@Before
void before() {
@ -33,51 +36,50 @@ class TrustServiceTest {
@Test
void testEmpty() {
assert TrustLevel.NEUTRAL == service.getLevel(dests.dest1)
assert TrustLevel.NEUTRAL == service.getLevel(dests.dest2)
assert TrustLevel.NEUTRAL == service.getLevel(personas.persona1.destination)
assert TrustLevel.NEUTRAL == service.getLevel(personas.persona2.destination)
}
@Test
void testOnEvent() {
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, destination: dests.dest1)
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, destination: dests.dest2)
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, persona: personas.persona1)
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, persona: personas.persona2)
assert TrustLevel.TRUSTED == service.getLevel(dests.dest1)
assert TrustLevel.DISTRUSTED == service.getLevel(dests.dest2)
assert TrustLevel.TRUSTED == service.getLevel(personas.persona1.destination)
assert TrustLevel.DISTRUSTED == service.getLevel(personas.persona2.destination)
}
@Test
void testPersist() {
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, destination: dests.dest1)
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, destination: dests.dest2)
service.onTrustEvent new TrustEvent(level: TrustLevel.TRUSTED, persona: personas.persona1)
service.onTrustEvent new TrustEvent(level: TrustLevel.DISTRUSTED, persona: personas.persona2)
Thread.sleep(250)
def trusted = new HashSet<>()
persistGood.eachLine {
trusted.add(new Destination(it))
trusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
}
def distrusted = new HashSet<>()
persistBad.eachLine {
distrusted.add(new Destination(it))
distrusted.add(new Persona(new ByteArrayInputStream(Base64.decode(it))))
}
assert trusted.size() == 1
assert trusted.contains(dests.dest1)
assert trusted.contains(personas.persona1)
assert distrusted.size() == 1
assert distrusted.contains(dests.dest2)
assert distrusted.contains(personas.persona2)
}
@Test
void testLoad() {
service.stop()
persistGood.append("${dests.dest1.toBase64()}\n")
persistBad.append("${dests.dest2.toBase64()}\n")
persistGood.append("${personas.persona1.toBase64()}\n")
persistBad.append("${personas.persona2.toBase64()}\n")
service = new TrustService(persistGood, persistBad, 100)
service.start()
Thread.sleep(10)
Thread.sleep(50)
assert TrustLevel.TRUSTED == service.getLevel(dests.dest1)
assert TrustLevel.DISTRUSTED == service.getLevel(dests.dest2)
assert TrustLevel.TRUSTED == service.getLevel(personas.persona1.destination)
assert TrustLevel.DISTRUSTED == service.getLevel(personas.persona2.destination)
}
}

View File

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

View File

@ -41,6 +41,7 @@ griffon {
}
mainClassName = 'com.muwire.gui.Launcher'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']
apply from: 'gradle/publishing.gradle'
apply from: 'gradle/code-coverage.gradle'
@ -58,6 +59,7 @@ dependencies {
compile "org.codehaus.griffon:griffon-guice:${griffon.version}"
runtime "org.slf4j:slf4j-simple:${slf4jVersion}"
runtime "javax.annotation:javax.annotation-api:1.3.2"
testCompile "org.codehaus.griffon:griffon-fest-test:${griffon.version}"
testCompile "org.spockframework:spock-core:${spockVersion}"

View File

@ -21,4 +21,9 @@ mvcGroups {
view = 'com.muwire.gui.SearchTabView'
controller = 'com.muwire.gui.SearchTabController'
}
}
'Options' {
model = 'com.muwire.gui.OptionsModel'
view = 'com.muwire.gui.OptionsView'
controller = 'com.muwire.gui.OptionsController'
}
}

View File

@ -10,6 +10,7 @@ import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
import javax.inject.Inject
import com.muwire.core.Constants
import com.muwire.core.Core
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.UIDownloadEvent
@ -31,6 +32,9 @@ class MainFrameController {
@ControllerAction
void search() {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel, "search window")
def search = builder.getVariable("search-field").text
def uuid = UUID.randomUUID()
Map<String, Object> params = new HashMap<>()
@ -39,9 +43,12 @@ class MainFrameController {
def group = mvcGroup.createMVCGroup("SearchTab", uuid.toString(), params)
model.results[uuid.toString()] = group
def searchEvent = new SearchEvent(searchTerms : [search], uuid : uuid)
// this can be improved a lot
def terms = search.toLowerCase().trim().split(Constants.SPLIT_PATTERN)
def searchEvent = new SearchEvent(searchTerms : terms, uuid : uuid)
core.eventBus.publish(new QueryEvent(searchEvent : searchEvent, firstHop : true,
replyTo: core.me.destination, receivedOn: core.me.destination))
replyTo: core.me.destination, receivedOn: core.me.destination,
originator : core.me))
}
private def selectedResult() {
@ -49,17 +56,30 @@ class MainFrameController {
def group = selected.getClientProperty("mvc-group")
def table = selected.getClientProperty("results-table")
int row = table.getSelectedRow()
if (row == -1)
return
group.model.results[row]
}
private def selectedDownload() {
def selected = builder.getVariable("downloads-table").getSelectedRow()
model.downloads[selected].downloader
}
@ControllerAction
void download() {
def result = selectedResult()
if (result == null)
return // TODO disable button
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
core.eventBus.publish(new UIDownloadEvent(result : result, target : file))
def file = new File(application.context.get("muwire-settings").downloadLocation, result.name)
def selected = builder.getVariable("result-tabs").getSelectedComponent()
def group = selected.getClientProperty("mvc-group")
def resultsBucket = group.model.hashBucket[result.infohash]
core.eventBus.publish(new UIDownloadEvent(result : resultsBucket, target : file))
}
@ControllerAction
@ -67,7 +87,7 @@ class MainFrameController {
def result = selectedResult()
if (result == null)
return // TODO disable button
core.eventBus.publish( new TrustEvent(destination : result.sender.destination, level : TrustLevel.TRUSTED))
core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.TRUSTED))
}
@ControllerAction
@ -75,7 +95,46 @@ class MainFrameController {
def result = selectedResult()
if (result == null)
return // TODO disable button
core.eventBus.publish( new TrustEvent(destination : result.sender.destination, level : TrustLevel.DISTRUSTED))
core.eventBus.publish( new TrustEvent(persona : result.sender, level : TrustLevel.DISTRUSTED))
}
@ControllerAction
void cancel() {
def downloader = selectedDownload()
downloader.cancel()
}
@ControllerAction
void resume() {
def downloader = selectedDownload()
downloader.resume()
}
private void markTrust(String tableName, TrustLevel level, def list) {
int row = builder.getVariable(tableName).getSelectedRow()
if (row < 0)
return
core.eventBus.publish(new TrustEvent(persona : list[row], level : level))
}
@ControllerAction
void markTrusted() {
markTrust("distrusted-table", TrustLevel.TRUSTED, model.distrusted)
}
@ControllerAction
void markNeutralFromDistrusted() {
markTrust("distrusted-table", TrustLevel.NEUTRAL, model.distrusted)
}
@ControllerAction
void markDistrusted() {
markTrust("trusted-table", TrustLevel.DISTRUSTED, model.trusted)
}
@ControllerAction
void markNeutralFromTrusted() {
markTrust("trusted-table", TrustLevel.NEUTRAL, model.trusted)
}
void mvcGroupInit(Map<String, String> args) {

View File

@ -0,0 +1,41 @@
package com.muwire.gui
import griffon.core.artifact.GriffonController
import griffon.core.controller.ControllerAction
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonController)
class OptionsController {
@MVCMember @Nonnull
OptionsModel model
@MVCMember @Nonnull
OptionsView view
@ControllerAction
void save() {
String text = view.retryField.text
model.downloadRetryInterval = text
def settings = application.context.get("muwire-settings")
settings.downloadRetryInterval = Integer.valueOf(text)
text = view.updateField.text
model.updateCheckInterval = text
settings.updateCheckInterval = Integer.valueOf(text)
File settingsFile = new File(application.context.get("core").home, "MuWire.properties")
settingsFile.withOutputStream {
settings.write(it)
}
cancel()
}
@ControllerAction
void cancel() {
view.d.setVisible(false)
mvcGroup.destroy()
}
}

View File

@ -21,7 +21,11 @@ class Initialize extends AbstractLifecycleHandler {
@Override
void execute() {
lookAndFeel((isMacOSX ? 'system' : 'nimbus'), 'gtk', ['metal', [boldFonts: false]])
if (isMacOSX()) {
lookAndFeel('nimbus') // otherwise the file chooser doesn't open???
} else {
lookAndFeel('system', 'gtk')
}
}
}

View File

@ -1,4 +1,5 @@
import griffon.core.GriffonApplication
import griffon.core.env.Metadata
import groovy.util.logging.Log
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
@ -16,9 +17,13 @@ import static griffon.util.GriffonApplicationUtils.isMacOSX
import static groovy.swing.SwingBuilder.lookAndFeel
import java.beans.PropertyChangeEvent
import java.util.logging.Level
@Log
class Ready extends AbstractLifecycleHandler {
@Inject Metadata metadata
@Inject
Ready(@Nonnull GriffonApplication application) {
super(application)
@ -27,7 +32,11 @@ class Ready extends AbstractLifecycleHandler {
@Override
void execute() {
log.info "starting core services"
def home = System.getProperty("user.home") + File.separator + ".MuWire"
def portableHome = System.getProperty("portable.home")
def home = portableHome == null ?
System.getProperty("user.home") + File.separator + ".MuWire" :
portableHome
home = new File(home)
if (!home.exists()) {
log.info("creating home dir")
@ -63,8 +72,13 @@ class Ready extends AbstractLifecycleHandler {
nickname = nickname.trim()
break
}
props.setNickname(nickname)
while(true) {
def portableDownloads = System.getProperty("portable.downloads")
if (portableDownloads != null) {
props.downloadLocation = new File(portableDownloads)
} else {
def chooser = new JFileChooser()
chooser.setDialogTitle("Select a directory where downloads will be saved")
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY)
@ -74,15 +88,22 @@ class Ready extends AbstractLifecycleHandler {
System.exit(0)
}
props.downloadLocation = chooser.getSelectedFile()
break
}
props.setNickname(nickname)
propsFile.withOutputStream {
props.write(it)
}
}
Core core = new Core(props, home)
Core core
try {
core = new Core(props, home, metadata["application.version"])
} catch (Exception bad) {
log.log(Level.SEVERE,"couldn't initialize core",bad)
JOptionPane.showMessageDialog(null, "Couldn't connect to I2P router. Make sure I2P is running and restart MuWire",
"Can't connect to I2P router", JOptionPane.WARNING_MESSAGE)
System.exit(0)
}
core.startServices()
application.context.put("muwire-settings", props)
application.context.put("core",core)

View File

@ -0,0 +1,25 @@
import javax.annotation.Nonnull
import javax.inject.Inject
import org.codehaus.griffon.runtime.core.AbstractLifecycleHandler
import com.muwire.core.Core
import griffon.core.GriffonApplication
import groovy.util.logging.Log
@Log
class Shutdown extends AbstractLifecycleHandler {
@Inject
Shutdown(@Nonnull GriffonApplication application) {
super(application)
}
@Override
void execute() {
log.info("shutting down")
Core core = application.context.get("core")
core.shutdown()
}
}

View File

@ -4,20 +4,25 @@ import java.util.concurrent.ConcurrentHashMap
import javax.annotation.Nonnull
import javax.inject.Inject
import javax.swing.JOptionPane
import javax.swing.JTable
import com.muwire.core.Core
import com.muwire.core.InfoHash
import com.muwire.core.Persona
import com.muwire.core.connection.ConnectionAttemptStatus
import com.muwire.core.connection.ConnectionEvent
import com.muwire.core.connection.DisconnectionEvent
import com.muwire.core.download.DownloadStartedEvent
import com.muwire.core.download.Downloader
import com.muwire.core.files.FileHashedEvent
import com.muwire.core.files.FileLoadedEvent
import com.muwire.core.files.FileSharedEvent
import com.muwire.core.search.QueryEvent
import com.muwire.core.search.UIResultEvent
import com.muwire.core.trust.TrustEvent
import com.muwire.core.trust.TrustService
import com.muwire.core.update.UpdateAvailableEvent
import com.muwire.core.upload.UploadEvent
import com.muwire.core.upload.UploadFinishedEvent
@ -27,6 +32,7 @@ import griffon.core.mvc.MVCGroup
import griffon.inject.MVCMember
import griffon.transform.FXObservable
import griffon.transform.Observable
import net.i2p.data.Destination
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
@ -37,17 +43,48 @@ class MainFrameModel {
@Observable boolean coreInitialized = false
def results = new ConcurrentHashMap<>()
@Observable def downloads = []
@Observable def uploads = []
@Observable def shared = []
def downloads = []
def uploads = []
def shared = []
def connectionList = []
def searches = new LinkedList()
def trusted = []
def distrusted = []
@Observable int connections
@Observable String me
@Observable boolean searchButtonsEnabled
@Observable boolean cancelButtonEnabled
@Observable boolean retryButtonEnabled
private final Set<InfoHash> infoHashes = new HashSet<>()
volatile Core core
private long lastRetryTime = System.currentTimeMillis()
void updateTablePreservingSelection(String tableName) {
def downloadTable = builder.getVariable(tableName)
int selectedRow = downloadTable.getSelectedRow()
downloadTable.model.fireTableDataChanged()
downloadTable.selectionModel.setSelectionInterval(selectedRow,selectedRow)
}
void mvcGroupInit(Map<String, Object> args) {
Timer timer = new Timer("download-pumper", true)
timer.schedule({
runInsideUIAsync {
if (!mvcGroup.alive)
return
builder.getVariable("uploads-table")?.model.fireTableDataChanged()
updateTablePreservingSelection("downloads-table")
updateTablePreservingSelection("trusted-table")
updateTablePreservingSelection("distrusted-table")
}
}, 1000, 1000)
application.addPropertyChangeListener("core", {e ->
coreInitialized = (e.getNewValue() != null)
core = e.getNewValue()
@ -61,14 +98,34 @@ class MainFrameModel {
core.eventBus.register(UploadEvent.class, this)
core.eventBus.register(UploadFinishedEvent.class, this)
core.eventBus.register(TrustEvent.class, this)
})
Timer timer = new Timer("download-pumper", true)
timer.schedule({
core.eventBus.register(QueryEvent.class, this)
core.eventBus.register(UpdateAvailableEvent.class, this)
timer.schedule({
int retryInterval = application.context.get("muwire-settings").downloadRetryInterval
if (retryInterval > 0) {
retryInterval *= 60000
long now = System.currentTimeMillis()
if (now - lastRetryTime > retryInterval) {
lastRetryTime = now
runInsideUIAsync {
downloads.each {
if (it.downloader.currentState == Downloader.DownloadState.FAILED)
it.downloader.resume()
updateTablePreservingSelection("downloads-table")
}
}
}
}
}, 60000, 60000)
runInsideUIAsync {
builder.getVariable("downloads-table").model.fireTableDataChanged()
builder.getVariable("uploads-table").model.fireTableDataChanged()
trusted.addAll(core.trustService.good.values())
distrusted.addAll(core.trustService.bad.values())
}
}, 1000, 1000)
})
}
void onUIResultEvent(UIResultEvent e) {
@ -83,14 +140,34 @@ class MainFrameModel {
}
void onConnectionEvent(ConnectionEvent e) {
if (e.getStatus() != ConnectionAttemptStatus.SUCCESSFUL)
return
runInsideUIAsync {
connections = core.connectionManager.getConnections().size()
if (connections > 0) {
def topPanel = builder.getVariable("top-panel")
topPanel.getLayout().show(topPanel, "top-search-panel")
}
connectionList.add(e.endpoint.destination)
JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged()
}
}
void onDisconnectionEvent(DisconnectionEvent e) {
runInsideUIAsync {
connections = core.connectionManager.getConnections().size()
if (connections == 0) {
def topPanel = builder.getVariable("top-panel")
topPanel.getLayout().show(topPanel, "top-connect-panel")
}
connectionList.remove(e.destination)
JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged()
}
}
@ -136,8 +213,50 @@ class MainFrameModel {
void onTrustEvent(TrustEvent e) {
runInsideUIAsync {
JTable table = builder.getVariable("results-table")
trusted.clear()
trusted.addAll(core.trustService.good.values())
distrusted.clear()
distrusted.addAll(core.trustService.bad.values())
updateTablePreservingSelection("trusted-table")
updateTablePreservingSelection("distrusted-table")
results.values().each {
it.view.pane.getClientProperty("results-table")?.model.fireTableDataChanged()
}
}
}
void onQueryEvent(QueryEvent e) {
if (e.replyTo == core.me.destination)
return
StringBuilder sb = new StringBuilder()
e.searchEvent.searchTerms?.each {
sb.append(it)
sb.append(" ")
}
def search = sb.toString()
if (search.trim().size() == 0)
return
runInsideUIAsync {
searches.addFirst(new IncomingSearch(search : search, replyTo : e.replyTo, originator : e.originator))
while(searches.size() > 200)
searches.removeLast()
JTable table = builder.getVariable("searches-table")
table.model.fireTableDataChanged()
}
}
class IncomingSearch {
String search
Destination replyTo
Persona originator
}
void onUpdateAvailableEvent(UpdateAvailableEvent e) {
runInsideUIAsync {
JOptionPane.showMessageDialog(null, "A new version of MuWire is available from $e.signer. Please update to $e.version")
}
}
}

View File

@ -0,0 +1,16 @@
package com.muwire.gui
import griffon.core.artifact.GriffonModel
import griffon.transform.Observable
import griffon.metadata.ArtifactProviderFor
@ArtifactProviderFor(GriffonModel)
class OptionsModel {
@Observable String downloadRetryInterval
@Observable String updateCheckInterval
void mvcGroupInit(Map<String, String> args) {
downloadRetryInterval = application.context.get("muwire-settings").downloadRetryInterval
updateCheckInterval = application.context.get("muwire-settings").updateCheckInterval
}
}

View File

@ -21,6 +21,7 @@ class SearchTabModel {
Core core
String uuid
def results = []
def hashBucket = [:]
void mvcGroupInit(Map<String, String> args) {
@ -34,6 +35,13 @@ class SearchTabModel {
void handleResult(UIResultEvent e) {
runInsideUIAsync {
def bucket = hashBucket.get(e.infohash)
if (bucket == null) {
bucket = []
hashBucket[e.infohash] = bucket
}
bucket << e
results << e
JTable table = builder.getVariable("results-table")
table.model.fireTableDataChanged()

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

View File

@ -3,15 +3,19 @@ package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.DataHelper
import javax.swing.BorderFactory
import javax.swing.Box
import javax.swing.BoxLayout
import javax.swing.JFileChooser
import javax.swing.JSplitPane
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.border.Border
import com.muwire.core.Constants
import com.muwire.core.download.Downloader
import com.muwire.core.files.FileSharedEvent
import java.awt.BorderLayout
@ -20,6 +24,7 @@ import java.awt.FlowLayout
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import java.awt.Insets
import java.nio.charset.StandardCharsets
import javax.annotation.Nonnull
@ -41,6 +46,11 @@ class MainFrameView {
imageIcon('/griffon-icon-16x16.png').image],
pack : false,
visible : bind { model.coreInitialized }) {
menuBar {
menu (text : "Options") {
menuItem("Configuration", actionPerformed : {mvcGroup.createMVCGroup("Options")})
}
}
borderLayout()
panel (border: etchedBorder(), constraints : BorderLayout.NORTH) {
borderLayout()
@ -48,15 +58,25 @@ class MainFrameView {
gridLayout(rows:1, cols: 2)
button(text: "Searches", actionPerformed : showSearchWindow)
button(text: "Uploads", actionPerformed : showUploadsWindow)
button(text: "Monitor", actionPerformed : showMonitorWindow)
button(text: "Trust", actionPerformed : showTrustWindow)
}
panel(constraints: BorderLayout.CENTER) {
borderLayout()
label("Enter search here:", constraints: BorderLayout.WEST)
textField(id: "search-field", constraints: BorderLayout.CENTER, action : searchAction)
}
panel( constraints: BorderLayout.EAST) {
button(text: "Search", searchAction)
panel(id: "top-panel", constraints: BorderLayout.CENTER) {
cardLayout()
label(constraints : "top-connect-panel",
text : " MuWire is connecting, please wait. You will be able to search soon.") // TODO: real padding
panel(constraints : "top-search-panel") {
borderLayout()
panel(constraints: BorderLayout.CENTER) {
borderLayout()
label(" Enter search here:", constraints: BorderLayout.WEST) // TODO: fix this
textField(id: "search-field", constraints: BorderLayout.CENTER, action : searchAction)
}
panel( constraints: BorderLayout.EAST) {
button(text: "Search", searchAction)
}
}
}
}
panel (id: "cards-panel", constraints : BorderLayout.CENTER) {
@ -69,9 +89,9 @@ class MainFrameView {
borderLayout()
tabbedPane(id : "result-tabs", constraints: BorderLayout.CENTER)
panel(constraints : BorderLayout.SOUTH) {
button(text : "Download", downloadAction)
button(text : "Trust", trustAction)
button(text : "Distrust", distrustAction)
button(text : "Download", enabled : bind {model.searchButtonsEnabled}, downloadAction)
button(text : "Trust", enabled: bind {model.searchButtonsEnabled }, trustAction)
button(text : "Distrust", enabled : bind {model.searchButtonsEnabled}, distrustAction)
}
}
panel (constraints : JSplitPane.BOTTOM) {
@ -79,21 +99,24 @@ class MainFrameView {
scrollPane (constraints : BorderLayout.CENTER) {
table(id : "downloads-table") {
tableModel(list: model.downloads) {
closureColumn(header: "Name", type: String, read : {row -> row.downloader.file.getName()})
closureColumn(header: "Status", type: String, read : {row -> row.downloader.getCurrentState()})
closureColumn(header: "Progress", type: String, read: { row ->
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.downloader.file.getName()})
closureColumn(header: "Status", preferredWidth: 50, type: String, read : {row -> row.downloader.getCurrentState()})
closureColumn(header: "Progress", preferredWidth: 20, type: String, read: { row ->
int pieces = row.downloader.nPieces
int done = row.downloader.donePieces()
"$done/$pieces pieces"
})
closureColumn(header: "Piece", type: String, read: { row ->
int position = row.downloader.positionInPiece()
int pieceSize = row.downloader.pieceSize // TODO: fix for last piece
"$position/$pieceSize bytes"
closureColumn(header: "Sources", preferredWidth : 10, type: Integer, read : {row -> row.downloader.activeWorkers()})
closureColumn(header: "Speed", preferredWidth: 50, type:String, read :{row ->
DataHelper.formatSize2Decimal(row.downloader.speed(), false) + "B/sec"
})
}
}
}
panel (constraints : BorderLayout.SOUTH) {
button(text: "Cancel", enabled : bind {model.cancelButtonEnabled }, cancelAction )
button(text: "Retry", enabled : bind {model.retryButtonEnabled}, resumeAction)
}
}
}
}
@ -102,20 +125,23 @@ class MainFrameView {
panel {
borderLayout()
panel (constraints : BorderLayout.NORTH) {
button(text : "Shared files", actionPerformed : shareFiles)
button(text : "Click here to share files", actionPerformed : shareFiles)
}
scrollPane ( constraints : BorderLayout.CENTER) {
table(id : "shared-files-table") {
tableModel(list : model.shared) {
closureColumn(header : "Name", type : String, read : {row -> row.file.getAbsolutePath()})
closureColumn(header : "Size", type : Long, read : {row -> row.file.length()})
closureColumn(header : "Name", preferredWidth : 550, type : String, read : {row -> row.file.getAbsolutePath()})
closureColumn(header : "Size", preferredWidth : 50, type : String,
read : {row -> DataHelper.formatSize2Decimal(row.file.length(),false) + "B"})
}
}
}
}
panel {
borderLayout()
label("Uploads", constraints : BorderLayout.NORTH)
panel (constraints : BorderLayout.NORTH){
label("Uploads")
}
scrollPane (constraints : BorderLayout.CENTER) {
table(id : "uploads-table") {
tableModel(list : model.uploads) {
@ -127,11 +153,86 @@ class MainFrameView {
int percent = (int)((position * 100.0) / total)
"$percent%"
})
closureColumn(header : "Downloader", type : String, read : { row ->
row.request.downloader?.getHumanReadableName()
})
}
}
}
}
}
panel (constraints: "monitor window") {
gridLayout(rows : 1, cols : 2)
panel {
borderLayout()
panel (constraints : BorderLayout.NORTH){
label("Connections")
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "connections-table") {
tableModel(list : model.connectionList) {
closureColumn(header : "Destination", type: String, read : { row -> row.toBase32() })
}
}
}
}
panel {
borderLayout()
panel (constraints : BorderLayout.NORTH){
label("Incoming searches")
}
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "searches-table") {
tableModel(list : model.searches) {
closureColumn(header : "Keywords", type : String, read : {
sanitized = it.search.replace('<', ' ')
sanitized
})
closureColumn(header : "From", type : String, read : {
if (it.originator != null) {
return it.originator.getHumanReadableName()
} else {
return it.replyTo.toBase32()
}
})
}
}
}
}
}
panel(constraints : "trust window") {
gridLayout(rows: 1, cols :2)
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "trusted-table") {
tableModel(list : model.trusted) {
closureColumn(header : "Trusted Users", type : String, read : { it.getHumanReadableName() } )
}
}
}
panel (constraints : BorderLayout.EAST) {
gridBagLayout()
button(text : "Mark Neutral", constraints : gbc(gridx: 0, gridy: 0), markNeutralFromTrustedAction)
button(text : "Mark Distrusted", constraints : gbc(gridx: 0, gridy:1), markDistrustedAction)
}
}
panel (border : etchedBorder()){
borderLayout()
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "distrusted-table") {
tableModel(list : model.distrusted) {
closureColumn(header: "Distrusted Users", type : String, read : { it.getHumanReadableName() } )
}
}
}
panel(constraints : BorderLayout.WEST) {
gridBagLayout()
button(text: "Mark Neutral", constraints: gbc(gridx: 0, gridy: 0), markNeutralFromDistrustedAction)
button(text: "Mark Trusted", constraints : gbc(gridx: 0, gridy : 1), markTrustedAction)
}
}
}
}
panel (border: etchedBorder(), constraints : BorderLayout.SOUTH) {
borderLayout()
@ -145,6 +246,30 @@ class MainFrameView {
}
}
}
void mvcGroupInit(Map<String, String> args) {
def downloadsTable = builder.getVariable("downloads-table")
def selectionModel = downloadsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener({
int selectedRow = downloadsTable.getSelectedRow()
def downloader = model.downloads[selectedRow].downloader
switch(downloader.getCurrentState()) {
case Downloader.DownloadState.CONNECTING :
case Downloader.DownloadState.DOWNLOADING :
model.cancelButtonEnabled = true
model.retryButtonEnabled = false
break
case Downloader.DownloadState.FAILED:
model.cancelButtonEnabled = false
model.retryButtonEnabled = true
break
default:
model.cancelButtonEnabled = false
model.retryButtonEnabled = false
}
})
}
def showSearchWindow = {
def cardsPanel = builder.getVariable("cards-panel")
@ -156,6 +281,16 @@ class MainFrameView {
cardsPanel.getLayout().show(cardsPanel, "uploads window")
}
def showMonitorWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel,"monitor window")
}
def showTrustWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel,"trust window")
}
def shareFiles = {
def chooser = new JFileChooser()
chooser.setDialogTitle("Select file or directory to share")

View File

@ -0,0 +1,59 @@
package com.muwire.gui
import griffon.core.artifact.GriffonView
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.JDialog
import javax.swing.SwingConstants
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.annotation.Nonnull
@ArtifactProviderFor(GriffonView)
class OptionsView {
@MVCMember @Nonnull
FactoryBuilderSupport builder
@MVCMember @Nonnull
OptionsModel model
def d
def p
def retryField
def updateField
def mainFrame
void initUI() {
mainFrame = application.windowManager.findWindow("main-frame")
d = new JDialog(mainFrame, "Options", true)
d.setResizable(false)
p = builder.panel {
gridBagLayout()
label(text : "Retry failed downloads every", constraints : gbc(gridx: 0, gridy: 0))
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2, constraints : gbc(gridx: 1, gridy: 0))
label(text : "minutes", constraints : gbc(gridx : 2, gridy: 0))
label(text : "Check for updates every", constraints : gbc(gridx : 0, gridy: 1))
updateField = textField(text : bind {model.updateCheckInterval }, columns : 2, constraints : gbc(gridx : 1, gridy: 1))
label(text : "hours", constraints : gbc(gridx: 2, gridy : 1))
button(text : "Save", constraints : gbc(gridx : 1, gridy: 2), saveAction)
button(text : "Cancel", constraints : gbc(gridx : 2, gridy: 2), cancelAction)
}
}
void mvcGroupInit(Map<String,String> args) {
d.getContentPane().add(p)
d.pack()
d.setLocationRelativeTo(mainFrame)
d.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE)
d.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
mvcGroup.destroy()
}
})
d.show()
}
}

View File

@ -4,7 +4,12 @@ import griffon.core.artifact.GriffonView
import griffon.core.mvc.MVCGroup
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import net.i2p.data.DataHelper
import javax.swing.JLabel
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import javax.swing.table.DefaultTableCellRenderer
import java.awt.BorderLayout
@ -20,6 +25,7 @@ class SearchTabView {
def pane
def parent
def searchTerms
def resultsTable
void initUI() {
builder.with {
@ -27,18 +33,28 @@ class SearchTabView {
def pane = scrollPane {
resultsTable = table(id : "results-table") {
tableModel(list: model.results) {
closureColumn(header: "Name", type: String, read : {row -> row.name})
closureColumn(header: "Size", preferredWidth: 150, type: Long, read : {row -> row.size})
closureColumn(header: "Sender", type: String, read : {row -> row.sender.getHumanReadableName()})
closureColumn(header: "Trust", type: String, read : {row ->
closureColumn(header: "Name", preferredWidth: 350, type: String, read : {row -> row.name})
closureColumn(header: "Size", preferredWidth: 50, type: String, read : {row -> DataHelper.formatSize2Decimal(row.size, false)+"B"})
closureColumn(header: "Sources", preferredWidth: 10, type : Integer, read : { row -> model.hashBucket[row.infohash].size()})
closureColumn(header: "Sender", preferredWidth: 170, type: String, read : {row -> row.sender.getHumanReadableName()})
closureColumn(header: "Trust", preferredWidth: 50, type: String, read : {row ->
model.core.trustService.getLevel(row.sender.destination)
})
}
}
}
this.pane = pane
this.pane.putClientProperty("mvc-group", mvcGroup)
this.pane.putClientProperty("results-table",resultsTable)
this.resultsTable = resultsTable
def selectionModel = resultsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener( {
mvcGroup.parentGroup.model.searchButtonsEnabled = true
})
}
}
@ -47,6 +63,7 @@ class SearchTabView {
parent = mvcGroup.parentGroup.view.builder.getVariable("result-tabs")
parent.addTab(searchTerms, pane)
int index = parent.indexOfTab(searchTerms)
parent.setSelectedIndex(index)
def tabPanel
builder.with {
@ -55,17 +72,24 @@ class SearchTabView {
panel {
label(text : searchTerms, constraints : BorderLayout.CENTER)
}
button(text : "x", preferredSize : [17,17], constraints : BorderLayout.EAST, // TODO: in osx is probably WEST
button(icon : imageIcon("/close_tab.png"), preferredSize : [20,20], constraints : BorderLayout.EAST, // TODO: in osx is probably WEST
actionPerformed : closeTab )
}
}
parent.setTabComponentAt(index, tabPanel)
def centerRenderer = new DefaultTableCellRenderer()
centerRenderer.setHorizontalAlignment(JLabel.CENTER)
resultsTable.columnModel.getColumn(1).setCellRenderer(centerRenderer)
resultsTable.setDefaultRenderer(Integer.class,centerRenderer)
resultsTable.columnModel.getColumn(4).setCellRenderer(centerRenderer)
}
def closeTab = {
int index = parent.indexOfTab(searchTerms)
parent.removeTabAt(index)
mvcGroup.parentGroup.model.searchButtonsEnabled = false
mvcGroup.destroy()
}
}

View File

@ -0,0 +1,25 @@
package com.muwire.gui
import griffon.core.test.GriffonFestRule
import org.fest.swing.fixture.FrameFixture
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
class OptionsIntegrationTest {
static {
System.setProperty('griffon.swing.edt.violations.check', 'true')
System.setProperty('griffon.swing.edt.hang.monitor', 'true')
}
@Rule
public final GriffonFestRule fest = new GriffonFestRule()
private FrameFixture window
@Test
void smokeTest() {
fail('Not implemented yet!')
}
}

View File

@ -0,0 +1,21 @@
package com.muwire.gui
import griffon.core.test.GriffonUnitRule
import griffon.core.test.TestFor
import org.junit.Rule
import org.junit.Test
import static org.junit.Assert.fail
@TestFor(OptionsController)
class OptionsControllerTest {
private OptionsController controller
@Rule
public final GriffonUnitRule griffon = new GriffonUnitRule()
@Test
void smokeTest() {
fail('Not yet implemented!')
}
}

View File

@ -1,4 +1,6 @@
include 'pinger'
include 'host-cache'
include 'update-server'
include 'core'
include 'gui'
include 'cli'

View File

@ -0,0 +1,3 @@
apply plugin : 'application'
mainClassName = 'com.muwire.update.UpdateServer'
applicationDefaultJvmArgs = ['-Djava.util.logging.config.file=logging.properties']

View File

@ -0,0 +1,105 @@
package com.muwire.update
import java.util.logging.Level
import groovy.util.logging.Log
import net.i2p.client.I2PClientFactory
import net.i2p.client.I2PSession
import net.i2p.client.I2PSessionMuxedListener
import net.i2p.client.datagram.I2PDatagramDissector
import net.i2p.client.datagram.I2PDatagramMaker
@Log
class UpdateServer {
public static void main(String[] args) {
def home = System.getProperty("user.home") + "/.MuWireUpdateServer"
home = new File(home)
if (!home.exists())
home.mkdirs()
def keyFile = new File(home, "key.dat")
def i2pClientFactory = new I2PClientFactory()
def i2pClient = i2pClientFactory.createClient()
def myDest
def session
if (!keyFile.exists()) {
def os = new FileOutputStream(keyFile);
myDest = i2pClient.createDestination(os)
os.close()
log.info "No key.dat file was found, so creating a new destination."
log.info "This is the destination you want to give out for your new UpdateServer"
log.info myDest.toBase64()
}
def update = new File(home, "update.json")
if (!update.exists()) {
log.warning("update file doesn't exist, exiting")
System.exit(1)
}
def props = System.getProperties().clone()
props.putAt("inbound.nickname", "MuWire UpdateServer")
session = i2pClient.createSession(new FileInputStream(keyFile), props)
myDest = session.getMyDestination()
session.addMuxedSessionListener(new Listener(update), I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY)
session.connect()
log.info("Connected, going to sleep")
Thread.sleep(Integer.MAX_VALUE)
}
static class Listener implements I2PSessionMuxedListener {
private final File json
Listener(File json) {
this.json = json
}
@Override
public void messageAvailable(I2PSession session, int msgId, long size) {
}
@Override
public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromport, int toport) {
if (proto != I2PSession.PROTO_DATAGRAM) {
log.warning("received uknown protocol $proto")
return
}
def payload = session.receiveMessage(msgId)
def dissector = new I2PDatagramDissector()
try {
dissector.loadI2PDatagram(payload)
def sender = dissector.getSender()
log.info("Got an update ping from "+sender.toBase32())
// I don't think we care about the payload at this point
def maker = new I2PDatagramMaker(session)
def response = maker.makeI2PDatagram(json.bytes)
session.sendMessage(sender, response, I2PSession.PROTO_DATAGRAM, 0, 2)
} catch (Exception e) {
log.log(Level.WARNING, "exception responding to update request",e)
}
}
@Override
public void reportAbuse(I2PSession session, int severity) {
}
@Override
public void disconnected(I2PSession session) {
Log.severe("Disconnected from I2P router")
System.exit(1)
}
@Override
public void errorOccurred(I2PSession session, String message, Throwable error) {
log.log(Level.SEVERE, message, error)
}
}
}