Compare commits

..

24 Commits

Author SHA1 Message Date
Zlatin Balevsky
ed3943c1af 0.0.8 for UI tweaks and sanitization 2019-06-04 18:01:08 +01:00
Zlatin Balevsky
e195141a27 simpler sanitization 2019-06-04 17:58:19 +01:00
Zlatin Balevsky
bb02fdbee9 do not use regex in sanitization 2019-06-04 17:46:41 +01:00
Zlatin Balevsky
6e3a2c0d08 update split pattern 2019-06-04 17:30:55 +01:00
Zlatin Balevsky
bd5fecc19d fix 2019-06-04 17:04:24 +01:00
Zlatin Balevsky
d5db49fa79 initialize core 2019-06-04 16:56:58 +01:00
Zlatin Balevsky
f2ea8619bb CLI project 2019-06-04 16:46:32 +01:00
Zlatin Balevsky
b129e79196 do not count finished workers in total count 2019-06-04 16:22:48 +01:00
Zlatin Balevsky
404d5b60bc format length in shared file stable an resize columns 2019-06-04 14:05:33 +01:00
Zlatin Balevsky
de2753ac50 preferred sizes for download table columns 2019-06-04 13:35:18 +01:00
Zlatin Balevsky
2d53999c8e only show download speed if downloading 2019-06-04 13:23:48 +01:00
Zlatin Balevsky
5aecf72d6f format download speed 2019-06-04 13:19:14 +01:00
Zlatin Balevsky
a574a67ec6 format file size 2019-06-04 13:15:24 +01:00
Zlatin Balevsky
6b5ad969b7 pass logging properties 2019-06-04 13:00:10 +01:00
Zlatin Balevsky
617209c4e4 column widths tweaks 2019-06-04 12:46:48 +01:00
Zlatin Balevsky
16b475bd9a 0.0.7 for multi-source downloads 2019-06-04 04:17:29 +01:00
Zlatin Balevsky
3cea1870cd multisource downloads, untested 2019-06-04 03:30:55 +01:00
Zlatin Balevsky
e7240dcb6f keep track of claimed pieces in preparation for multi-source downloads 2019-06-04 02:18:30 +01:00
Zlatin Balevsky
c91440cbfc config option for update check interval 2019-06-03 23:30:39 +01:00
Zlatin Balevsky
294605f5c7 basic update notification 2019-06-03 23:23:07 +01:00
Zlatin Balevsky
986caf3a75 backend for checking updates 2019-06-03 23:11:03 +01:00
Zlatin Balevsky
8524d5309f typo 2019-06-03 21:53:51 +01:00
Zlatin Balevsky
48b3ac2b4a wip on update server 2019-06-03 21:50:46 +01:00
Zlatin Balevsky
18f21dc247 update server 2019-06-03 21:47:31 +01:00
28 changed files with 605 additions and 98 deletions

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

@@ -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
@@ -61,11 +62,12 @@ public class Core {
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()
@@ -154,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)
@@ -197,6 +202,7 @@ public class Core {
connectionAcceptor.start()
connectionEstablisher.start()
hostCache.waitForLoad()
updateClient.start()
}
public void shutdown() {
@@ -227,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

@@ -7,6 +7,7 @@ class MuWireSettings {
final boolean isLeaf
boolean allowUntrusted
int downloadRetryInterval
int updateCheckInterval
String nickname
File downloadLocation
String sharedFiles
@@ -25,6 +26,7 @@ class MuWireSettings {
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 {
@@ -35,6 +37,7 @@ class MuWireSettings {
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

@@ -3,6 +3,7 @@ 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
@@ -16,16 +17,13 @@ public class DownloadManager {
private final I2PConnector connector
private final Executor executor
private final File incompletes
private final String meB64
private final Persona me
public DownloadManager(EventBus eventBus, I2PConnector connector, File incompletes, Persona me) {
this.eventBus = eventBus
this.connector = connector
this.incompletes = incompletes
def baos = new ByteArrayOutputStream()
me.write(baos)
this.meB64 = Base64.encode(baos.toByteArray())
this.me = me
incompletes.mkdir()
@@ -39,8 +37,18 @@ public class DownloadManager {
public void onUIDownloadEvent(UIDownloadEvent e) {
def downloader = new Downloader(this, meB64, 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))

View File

@@ -23,7 +23,7 @@ class DownloadSession {
private static int SAMPLES = 10
private final String meB64
private final Pieces pieces
private final Pieces downloaded, claimed
private final InfoHash infoHash
private final Endpoint endpoint
private final File file
@@ -36,10 +36,11 @@ class DownloadSession {
private ByteBuffer mapped
DownloadSession(String meB64, 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.meB64 = meB64
this.pieces = pieces
this.downloaded = downloaded
this.claimed = claimed
this.endpoint = endpoint
this.infoHash = infoHash
this.file = file
@@ -53,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
@@ -145,10 +166,12 @@ class DownloadSession {
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() {

View File

@@ -1,8 +1,12 @@
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
@@ -14,35 +18,41 @@ import net.i2p.data.Destination
@Log
public class Downloader {
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 String meB64
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
private volatile boolean cancelled
private volatile Thread downloadThread
public Downloader(DownloadManager downloadManager, String meB64, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Destination destination,
public Downloader(DownloadManager downloadManager, Persona me, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Set<Destination> destinations,
File incompletes) {
this.meB64 = meB64
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
@@ -53,32 +63,18 @@ 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() {
readPieces()
downloadThread = Thread.currentThread()
Endpoint endpoint = null
try {
endpoint = connector.connect(destination)
currentState = DownloadState.DOWNLOADING
while(!pieces.isComplete()) {
currentSession = new DownloadSession(meB64, pieces, infoHash, endpoint, file, pieceSize, length)
currentSession.request()
writePieces()
destinations.each {
if (it != me.destination) {
def worker = new DownloadWorker(it)
activeWorkers.put(it, worker)
executorService.submit(worker)
}
currentState = DownloadState.FINISHED
piecesFile.delete()
} catch (Exception bad) {
log.log(Level.WARNING,"Exception while downloading",bad)
if (cancelled)
currentState = DownloadState.CANCELLED
else if (currentState != DownloadState.FINISHED)
currentState = DownloadState.FAILED
} finally {
endpoint?.close()
}
}
@@ -87,45 +83,123 @@ public class Downloader {
return
piecesFile.withReader {
int piece = Integer.parseInt(it.readLine())
pieces.markDownloaded(piece)
downloaded.markDownloaded(piece)
}
}
void writePieces() {
piecesFile.withPrintWriter { writer ->
pieces.getDownloaded().each { piece ->
downloaded.getDownloaded().each { piece ->
writer.println(piece)
}
}
}
public long donePieces() {
pieces.donePieces()
downloaded.donePieces()
}
public int positionInPiece() {
if (currentSession == null)
return 0
currentSession.positionInPiece()
}
public int speed() {
if (currentSession == null)
return 0
currentSession.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
downloadThread?.interrupt()
activeWorkers.values().each {
it.cancel()
}
}
public int activeWorkers() {
int active = 0
activeWorkers.values().each {
if (it.currentState != WorkerState.FINISHED)
active++
}
active
}
public void resume() {
currentState = DownloadState.CONNECTING
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,7 +28,8 @@ class Pieces {
while(true) {
int start = random.nextInt(nPieces)
while(bitSet.get(start) && ++start < nPieces);
if (bitSet.get(start))
continue
return start
}
}
@@ -45,10 +46,18 @@ class Pieces {
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

@@ -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

@@ -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()
@@ -138,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

@@ -1,5 +1,5 @@
group = com.muwire
version = 0.0.6
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'

View File

@@ -71,8 +71,15 @@ class MainFrameController {
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

View File

@@ -20,7 +20,11 @@ class OptionsController {
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)

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
@@ -20,6 +21,9 @@ import java.util.logging.Level
@Log
class Ready extends AbstractLifecycleHandler {
@Inject Metadata metadata
@Inject
Ready(@Nonnull GriffonApplication application) {
super(application)
@@ -90,9 +94,10 @@ class Ready extends AbstractLifecycleHandler {
props.write(it)
}
}
Core core
try {
core = new Core(props, home)
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",

View File

@@ -4,6 +4,7 @@ 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
@@ -21,6 +22,7 @@ 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
@@ -97,6 +99,7 @@ class MainFrameModel {
core.eventBus.register(UploadFinishedEvent.class, this)
core.eventBus.register(TrustEvent.class, this)
core.eventBus.register(QueryEvent.class, this)
core.eventBus.register(UpdateAvailableEvent.class, this)
timer.schedule({
int retryInterval = application.context.get("muwire-settings").downloadRetryInterval
@@ -250,4 +253,10 @@ class MainFrameModel {
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

@@ -7,8 +7,10 @@ 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,7 +21,7 @@ class SearchTabModel {
Core core
String uuid
def results = []
def hashCount = [:]
def hashBucket = [:]
void mvcGroupInit(Map<String, String> args) {
@@ -35,11 +35,12 @@ class SearchTabModel {
void handleResult(UIResultEvent e) {
runInsideUIAsync {
Integer count = hashCount.get(e.infohash)
if (count == null)
count = 0
count++
hashCount[e.infohash] = count
def bucket = hashBucket.get(e.infohash)
if (bucket == null) {
bucket = []
hashBucket[e.infohash] = bucket
}
bucket << e
results << e
JTable table = builder.getVariable("results-table")

View File

@@ -3,6 +3,7 @@ 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
@@ -13,6 +14,7 @@ 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
@@ -97,19 +99,17 @@ 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"
})
closureColumn(header: "Speed (bytes/second)", type:Integer, read :{row -> row.downloader.speed()})
}
}
}
@@ -130,8 +130,9 @@ class MainFrameView {
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"})
}
}
}
@@ -183,7 +184,10 @@ class MainFrameView {
scrollPane(constraints : BorderLayout.CENTER) {
table(id : "searches-table") {
tableModel(list : model.searches) {
closureColumn(header : "Keywords", type : String, read : { it.search })
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()

View File

@@ -22,6 +22,7 @@ class OptionsView {
def d
def p
def retryField
def updateField
def mainFrame
void initUI() {
@@ -34,8 +35,12 @@ class OptionsView {
retryField = textField(text : bind { model.downloadRetryInterval }, columns : 2, constraints : gbc(gridx: 1, gridy: 0))
label(text : "minutes", constraints : gbc(gridx : 2, gridy: 0))
button(text : "Save", constraints : gbc(gridx : 1, gridy: 1), saveAction)
button(text : "Cancel", constraints : gbc(gridx : 2, gridy: 1), cancelAction)
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)
}
}

View File

@@ -4,9 +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
@@ -22,6 +25,7 @@ class SearchTabView {
def pane
def parent
def searchTerms
def resultsTable
void initUI() {
builder.with {
@@ -29,20 +33,23 @@ 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: "Sources", type : Integer, read : { row -> model.hashCount[row.infohash]})
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( {
@@ -71,6 +78,12 @@ class SearchTabView {
}
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 = {

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)
}
}
}