Compare commits

..

12 Commits

Author SHA1 Message Date
Zlatin Balevsky
821555f3f1 Release 0.3.1 2019-06-20 14:02:22 +01:00
Zlatin Balevsky
089ab4f0d9 do not retry downloads if core is shut(ting) down 2019-06-20 13:40:04 +01:00
Zlatin Balevsky
948b6292fe add shutdown hook to shutdown core on SIGTERM 2019-06-20 13:29:15 +01:00
Zlatin Balevsky
4e2a530a13 Release 0.3.0 2019-06-20 07:04:45 +01:00
Zlatin Balevsky
03646e2b90 Document download mesh 2019-06-20 01:19:15 +01:00
Zlatin Balevsky
3dce228bbb always clean 2019-06-19 22:42:05 +01:00
Zlatin Balevsky
15a49ad550 show git revision in title 2019-06-19 22:36:22 +01:00
Zlatin Balevsky
3d91c0f4c7 increase default tunnel count 2019-06-19 22:24:04 +01:00
Zlatin Balevsky
2825a8d9a4 Release 0.2.10 2019-06-19 17:18:30 +01:00
Zlatin Balevsky
8dcce9bda6 Merge branch 'connection-logic' 2019-06-19 17:16:13 +01:00
Zlatin Balevsky
d8d3e2cd58 update tests 2019-06-19 15:54:35 +01:00
Zlatin Balevsky
51d5dbe47e Prevent rare exception on changing trust when result tabs are open 2019-06-19 12:23:18 +01:00
18 changed files with 67 additions and 37 deletions

View File

@@ -11,12 +11,12 @@ The current stable release - 0.2.5 is avaiable for download at http://muwire.com
You need JRE 8 or newer. After installing that and setting up the appropriate paths, just type
```
./gradlew assemble
./gradlew clean assemble
```
If you want to run the unit tests, type
```
./gradlew build
./gradlew clean build
```
Some of the UI tests will fail because they haven't been written yet :-/

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package com.muwire.core
import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.AtomicBoolean
import com.muwire.core.connection.ConnectionAcceptor
import com.muwire.core.connection.ConnectionEstablisher
@@ -73,6 +74,8 @@ public class Core {
private final DownloadManager downloadManager
private final DirectoryWatcher directoryWatcher
final FileManager fileManager
final AtomicBoolean shutdown = new AtomicBoolean()
public Core(MuWireSettings props, File home, String myVersion) {
this.home = home
@@ -104,9 +107,9 @@ public class Core {
i2pOptions["inbound.nickname"] = "MuWire"
i2pOptions["outbound.nickname"] = "MuWire"
i2pOptions["inbound.length"] = "3"
i2pOptions["inbound.quantity"] = "2"
i2pOptions["inbound.quantity"] = "4"
i2pOptions["outbound.length"] = "3"
i2pOptions["outbound.quantity"] = "2"
i2pOptions["outbound.quantity"] = "4"
i2pOptions["i2cp.tcp.host"] = "127.0.0.1"
i2pOptions["i2cp.tcp.port"] = "7654"
}
@@ -241,6 +244,10 @@ public class Core {
}
public void shutdown() {
if (!shutdown.compareAndSet(false, true)) {
log.info("already shutting down")
return
}
log.info("shutting down download manageer")
downloadManager.shutdown()
log.info("shutting down connection acceeptor")
@@ -277,7 +284,7 @@ public class Core {
}
}
Core core = new Core(props, home, "0.2.9")
Core core = new Core(props, home, "0.3.1")
core.startServices()
// ... at the end, sleep or execute script

View File

@@ -15,7 +15,7 @@ class DownloadSessionTest {
private File source, target
private InfoHash infoHash
private Endpoint endpoint
private Pieces pieces, claimed
private Pieces pieces
private String rootBase64
private DownloadSession session
@@ -48,8 +48,7 @@ class DownloadSessionTest {
else
nPieces = size / pieceSize + 1
pieces = new Pieces(nPieces)
claimed = new Pieces(nPieces)
claimedPieces.each {claimed.markDownloaded(it)}
claimedPieces.each {pieces.claimed.set(it)}
fromDownloader = new PipedInputStream()
fromUploader = new PipedInputStream()
@@ -57,7 +56,7 @@ class DownloadSessionTest {
toUploader = new PipedOutputStream(fromDownloader)
endpoint = new Endpoint(null, fromUploader, toUploader, null)
session = new DownloadSession("",pieces, claimed, infoHash, endpoint, target, pieceSize, size)
session = new DownloadSession("",pieces, infoHash, endpoint, target, pieceSize, size)
downloadThread = new Thread( { session.request() } as Runnable)
downloadThread.setDaemon(true)
downloadThread.start()
@@ -154,7 +153,7 @@ class DownloadSessionTest {
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 !pieces.claimed.get(0)
assert "GET $rootBase64" == readTillRN(fromDownloader)
String range = readTillRN(fromDownloader)
@@ -162,7 +161,7 @@ class DownloadSessionTest {
int start = Integer.parseInt(matcher[0][1])
int end = Integer.parseInt(matcher[0][2])
assert claimed.isMarked(0)
assert pieces.claimed.get(0)
assert start == 0 && end == (1 << pieceSize) - 1
}
}

View File

@@ -16,7 +16,7 @@ class PiecesTest {
public void testSinglePiece() {
pieces = new Pieces(1)
assert !pieces.isComplete()
assert pieces.getRandomPiece() == 0
assert pieces.claim() == 0
pieces.markDownloaded(0)
assert pieces.isComplete()
}
@@ -25,11 +25,11 @@ class PiecesTest {
public void testTwoPieces() {
pieces = new Pieces(2)
assert !pieces.isComplete()
int piece = pieces.getRandomPiece()
int piece = pieces.claim()
assert piece == 0 || piece == 1
pieces.markDownloaded(piece)
assert !pieces.isComplete()
int piece2 = pieces.getRandomPiece()
int piece2 = pieces.claim()
assert piece != piece2
pieces.markDownloaded(piece2)
assert pieces.isComplete()

View File

@@ -26,7 +26,7 @@ class FileHasherTest extends GroovyTestCase {
void testPieceSize() {
assert 17 == FileHasher.getPieceSize(1000000)
assert 17 == FileHasher.getPieceSize(100000000)
assert 27 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
assert 24 == FileHasher.getPieceSize(FileHasher.MAX_SIZE)
shouldFail IllegalArgumentException, {
FileHasher.getPieceSize(Long.MAX_VALUE)
}

View File

@@ -27,6 +27,7 @@ class HasherServiceTest {
hasher = new FileHasher()
service = new HasherService(hasher, eventBus, new FileManager(eventBus, new MuWireSettings()))
eventBus.register(FileHashedEvent.class, listener)
eventBus.register(FileSharedEvent.class, service)
service.start()
}

View File

@@ -78,7 +78,7 @@ class PersisterServiceLoadingTest {
persisted.write json
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
ps.start()
ps.onUILoadedEvent(null)
Thread.sleep(2000)
assert listener.publishedFiles.size() == 1
@@ -121,7 +121,7 @@ class PersisterServiceLoadingTest {
persisted.write json
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
ps.start()
ps.onUILoadedEvent(null)
Thread.sleep(2000)
assert listener.publishedFiles.size() == 1
@@ -163,7 +163,7 @@ class PersisterServiceLoadingTest {
persisted.append "$json2\n"
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
ps.start()
ps.onUILoadedEvent(null)
Thread.sleep(2000)
assert listener.publishedFiles.size() == 2
@@ -195,7 +195,7 @@ class PersisterServiceLoadingTest {
persisted.write json1
PersisterService ps = new PersisterService(persisted, eventBus, 100, null)
ps.start()
ps.onUILoadedEvent(null)
Thread.sleep(2000)
assert listener.publishedFiles.size() == 1

View File

@@ -58,7 +58,7 @@ class PersisterServiceSavingTest {
sf = new SharedFile(f, ih, 0)
ps = new PersisterService(persisted, eventBus, 100, fileSource)
ps.start()
ps.onUILoadedEvent(null)
Thread.sleep(1500)
JsonSlurper jsonSlurper = new JsonSlurper()
@@ -77,7 +77,7 @@ class PersisterServiceSavingTest {
sf = new DownloadedFile(f, ih, 0, new HashSet([dests.dest1, dests.dest2]))
ps = new PersisterService(persisted, eventBus, 100, fileSource)
ps.start()
ps.onUILoadedEvent(null)
Thread.sleep(1500)
JsonSlurper jsonSlurper = new JsonSlurper()

View File

@@ -9,18 +9,18 @@ import com.muwire.core.InfoHash
class RequestParsingTest {
Request request
ContentRequest request
private void fromString(String requestString) {
def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII))
request = Request.parse(new InfoHash(new byte[InfoHash.SIZE]), is)
request = Request.parseContentRequest(new InfoHash(new byte[InfoHash.SIZE]), is)
}
private static void failed(String requestString) {
try {
def is = new ByteArrayInputStream(requestString.getBytes(StandardCharsets.US_ASCII))
Request.parse(new InfoHash(new byte[InfoHash.SIZE]), is)
Request.parseContentRequest(new InfoHash(new byte[InfoHash.SIZE]), is)
assert false
} catch (IOException expected) {}
}

View File

@@ -19,7 +19,7 @@ class UploaderTest {
InputStream is
OutputStream os
Request request
ContentRequest request
Uploader uploader
byte[] inFile
@@ -52,7 +52,7 @@ class UploaderTest {
}
private void startUpload() {
uploader = new Uploader(file, request, endpoint)
uploader = new ContentUploader(file, request, endpoint)
uploadThread = new Thread(uploader.respond() as Runnable)
uploadThread.setDaemon(true)
uploadThread.start()
@@ -77,7 +77,7 @@ class UploaderTest {
@Test
public void testSmallFile() {
fillFile(20)
request = new Request(range : new Range(0,19))
request = new ContentRequest(range : new Range(0,19))
startUpload()
assert "200 OK" == readUntilRN()
assert "Content-Range: 0-19" == readUntilRN()
@@ -92,7 +92,7 @@ class UploaderTest {
@Test
public void testRequestMiddle() {
fillFile(20)
request = new Request(range : new Range(5,15))
request = new ContentRequest(range : new Range(5,15))
startUpload()
assert "200 OK" == readUntilRN()
assert "Content-Range: 5-15" == readUntilRN()
@@ -108,7 +108,7 @@ class UploaderTest {
@Test
public void testOutOfRange() {
fillFile(20)
request = new Request(range : new Range(0,20))
request = new ContentRequest(range : new Range(0,20))
startUpload()
assert "416 Range Not Satisfiable" == readUntilRN()
assert "" == readUntilRN()
@@ -118,7 +118,7 @@ class UploaderTest {
public void testLargeFile() {
final int length = 0x1 << 14
fillFile(length)
request = new Request(range : new Range(0, length - 1))
request = new ContentRequest(range : new Range(0, length - 1))
startUpload()
readUntilRN()
readUntilRN()

View File

@@ -49,7 +49,7 @@ Files are transferred over HTTP1.1 protocol with some custom headers added for d
### Mesh management
Download mesh management is identical to Gnutella, except instead of ip addresses MuWire personas are used. [More information](http://rfc-gnutella.sourceforge.net/developer/tmp/download-mesh.html)
Download mesh management is a simplified version of Gnutella's "Alternate Location" system. For more information see the "download-mesh" document.
### In-Network updates

15
doc/download-mesh.md Normal file
View File

@@ -0,0 +1,15 @@
# Download Mesh / Partial Sharing
MuWire uses a system similar to Gnutella's "Alternate Location" download mesh management system, however it is simplified to account for I2P's strengths and borrows a bit from BitTorrent's "Have" message.
### "X-Have" header
With every request a downloader makes it sends an "X-Have" header containing the Base64-encoded representation of a bitfield where bits set to 1 represent pieces of the file that the downloader already has. To make partial file sharing possible, if the uploader does not have the complete file it also sends this header in every response. If the header is missing it is assumed the uploader has the complete file.
### "X-Alt" header
The uploader can recommend other uploaders to the downloader via the "X-Alt" header. The format of this header is a comma-separated list of Base64-encoded Personas that have previously reported having at least one piece of the file to the uploader via the "X-Have" header.
### Differences from Gnutella
Unlike Gnutella the uploader is the sole repository where possible sources of the file are tracked. There is no negative "X-Nalt" header to prevent attacking the download mesh by mass downvoting of sources.

View File

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

View File

@@ -98,6 +98,9 @@ class Ready extends AbstractLifecycleHandler {
"Can't connect to I2P router", JOptionPane.WARNING_MESSAGE)
System.exit(0)
}
Runtime.getRuntime().addShutdownHook({
core.shutdown()
})
core.startServices()
application.context.put("muwire-settings", props)
application.context.put("core",core)

View File

@@ -137,6 +137,8 @@ class MainFrameModel {
core.eventBus.register(FileUnsharedEvent.class, this)
timer.schedule({
if (core.shutdown.get())
return
int retryInterval = core.muOptions.downloadRetryInterval
if (retryInterval > 0) {
retryInterval *= 60000
@@ -282,8 +284,10 @@ class MainFrameModel {
updateTablePreservingSelection("trusted-table")
updateTablePreservingSelection("distrusted-table")
results.values().each {
it.view.pane.getClientProperty("results-table")?.model.fireTableDataChanged()
results.values().each { MVCGroup group ->
if (group.alive) {
group.view.pane.getClientProperty("results-table")?.model.fireTableDataChanged()
}
}
}
}

View File

@@ -58,7 +58,8 @@ class MainFrameView {
builder.with {
application(size : [1024,768], id: 'main-frame',
locationRelativeTo : null,
title: application.configuration['application.title'] + " " + metadata["application.version"],
title: application.configuration['application.title'] + " " +
metadata["application.version"] + " revision " + metadata["build.revision"],
iconImage: imageIcon('/griffon-icon-48x48.png').image,
iconImages: [imageIcon('/griffon-icon-48x48.png').image,
imageIcon('/griffon-icon-32x32.png').image,