Compare commits

..

19 Commits

Author SHA1 Message Date
Zlatin Balevsky
656b62fc2e 0.0.4 with download retry 2019-06-01 18:31:36 +01:00
Zlatin Balevsky
13b3f0f63b retry implemented 2019-06-01 18:30:30 +01:00
Zlatin Balevsky
98ea8154a5 store done pieces on disk to enable resume 2019-06-01 18:09:14 +01:00
Zlatin Balevsky
82377aa9df hook up cancel button 2019-06-01 17:44:52 +01:00
Zlatin Balevsky
bd2368e23a cancelled downloader state 2019-06-01 17:31:18 +01:00
Zlatin Balevsky
70078c309b add cancel and retry buttons, not hooked up yet 2019-06-01 17:30:29 +01:00
Zlatin Balevsky
15a0eda713 preserve selection in downloads table 2019-06-01 17:09:23 +01:00
Zlatin Balevsky
9645716e18 prevent rare stacktraces on shutdown 2019-06-01 16:55:37 +01:00
Zlatin Balevsky
03d6af39ed icon for closing tabs 2019-06-01 16:43:05 +01:00
Zlatin Balevsky
9435cb003b Show warning if cannot find I2P router 2019-06-01 16:36:23 +01:00
Zlatin Balevsky
63399803d5 ui tweaks 2019-06-01 15:59:55 +01:00
Zlatin Balevsky
4d6541030f disable system l&f on osx 2019-06-01 14:55:17 +01:00
Zlatin Balevsky
16c51e7cd6 add a failed download state 2019-06-01 14:14:20 +01:00
Zlatin Balevsky
9d75550b6f do not show local searches in monitor 2019-06-01 13:48:12 +01:00
Zlatin Balevsky
1996681677 incoming searches monitor 2019-06-01 13:44:46 +01:00
Zlatin Balevsky
9dac1891b2 connection monitor 2019-06-01 13:32:40 +01:00
Zlatin Balevsky
1255ac936b close connections on shutdown 2019-06-01 13:04:22 +01:00
Zlatin Balevsky
2db3276b07 fix rare NPE on shutdown 2019-06-01 13:03:42 +01:00
Zlatin Balevsky
7e3b0795af disable buttons if no row is selected 2019-06-01 12:23:20 +01:00
19 changed files with 324 additions and 39 deletions

View File

@@ -54,7 +54,8 @@ public class Core {
final EventBus eventBus
final Persona me
final File home
private final TrustService trustService
private final PersisterService persisterService
private final HostCache hostCache
@@ -64,7 +65,8 @@ public class Core {
private final ConnectionEstablisher connectionEstablisher
private final HasherService hasherService
public Core(MuWireSettings props, File home) {
public Core(MuWireSettings props, File home) {
this.home = home
log.info "Initializing I2P context"
I2PAppContext.getGlobalContext().logManager()
I2PAppContext.getGlobalContext()._logManager = new MuWireLogManager()
@@ -164,7 +166,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"))
eventBus.register(UIDownloadEvent.class, downloadManager)
log.info("initializing upload manager")
@@ -196,6 +198,10 @@ public class Core {
connectionEstablisher.start()
hostCache.waitForLoad()
}
public void shutdown() {
connectionManager.shutdown()
}
static main(args) {
def home = System.getProperty("user.home") + File.separator + ".MuWire"

View File

@@ -82,7 +82,6 @@ abstract class Connection implements Closeable {
read()
}
} catch (SocketTimeoutException e) {
close()
} catch (Exception e) {
log.log(Level.WARNING,"unhandled exception in reader",e)
} finally {

View File

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

@@ -11,10 +11,13 @@ public class DownloadManager {
private final EventBus eventBus
private final I2PConnector connector
private final Executor executor
private final File incompletes
public DownloadManager(EventBus eventBus, I2PConnector connector) {
public DownloadManager(EventBus eventBus, I2PConnector connector, File incompletes) {
this.eventBus = eventBus
this.connector = connector
this.incompletes = incompletes
incompletes.mkdir()
this.executor = Executors.newCachedThreadPool({ r ->
Thread rv = new Thread(r)
rv.setName("download-worker")
@@ -25,9 +28,14 @@ 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 downloader = new Downloader(this, e.target, e.result.size,
e.result.infohash, e.result.pieceSize, connector, e.result.sender.destination,
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

@@ -127,11 +127,8 @@ 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)
} finally {

View File

@@ -2,14 +2,20 @@ package com.muwire.core.download
import com.muwire.core.InfoHash
import com.muwire.core.connection.Endpoint
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 final DownloadManager downloadManager
private final File file
private final Pieces pieces
private final long length
@@ -18,17 +24,24 @@ public class Downloader {
private final I2PConnector connector
private final Destination destination
private final int nPieces
private final File piecesFile
private Endpoint endpoint
private volatile DownloadSession currentSession
private volatile DownloadState currentState
private volatile boolean cancelled
private volatile Thread downloadThread
public Downloader(File file, long length, InfoHash infoHash, int pieceSizePow2, I2PConnector connector, Destination destination) {
public Downloader(DownloadManager downloadManager, File file, long length, InfoHash infoHash,
int pieceSizePow2, I2PConnector connector, Destination destination,
File incompletes) {
this.downloadManager = downloadManager
this.file = file
this.infoHash = infoHash
this.length = length
this.connector = connector
this.destination = destination
this.piecesFile = new File(incompletes, file.getName()+".pieces")
this.pieceSize = 1 << pieceSizePow2
int nPieces
@@ -43,14 +56,45 @@ public class Downloader {
}
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()
downloadThread = Thread.currentThread()
Endpoint endpoint = null
try {
endpoint = connector.connect(destination)
currentState = DownloadState.DOWNLOADING
while(!pieces.isComplete()) {
currentSession = new DownloadSession(pieces, infoHash, endpoint, file, pieceSize, length)
currentSession.request()
writePieces()
}
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()
}
}
void readPieces() {
if (!piecesFile.exists())
return
piecesFile.withReader {
int piece = Integer.parseInt(it.readLine())
pieces.markDownloaded(piece)
}
}
void writePieces() {
piecesFile.withPrintWriter { writer ->
pieces.getDownloaded().each { piece ->
writer.println(piece)
}
}
currentState = DownloadState.FINISHED
endpoint.close()
}
public long donePieces() {
@@ -66,4 +110,13 @@ public class Downloader {
public DownloadState getCurrentState() {
currentState
}
public void cancel() {
cancelled = true
downloadThread?.interrupt()
}
public void resume() {
downloadManager.resume(this)
}
}

View File

@@ -33,6 +33,14 @@ class Pieces {
}
}
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)
}

View File

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

View File

@@ -49,10 +49,16 @@ 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()
@@ -78,6 +84,18 @@ class MainFrameController {
core.eventBus.publish( new TrustEvent(destination : result.sender.destination, level : TrustLevel.DISTRUSTED))
}
@ControllerAction
void cancel() {
def downloader = selectedDownload()
downloader.cancel()
}
@ControllerAction
void resume() {
def downloader = selectedDownload()
downloader.resume()
}
void mvcGroupInit(Map<String, String> args) {
application.addPropertyChangeListener("core", {e->
core = e.getNewValue()

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

@@ -16,6 +16,7 @@ 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 {
@@ -81,8 +82,15 @@ class Ready extends AbstractLifecycleHandler {
props.write(it)
}
}
Core core = new Core(props, home)
Core core
try {
core = new Core(props, home)
} 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

@@ -15,6 +15,7 @@ import com.muwire.core.download.DownloadStartedEvent
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
@@ -27,6 +28,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,11 +39,17 @@ 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()
@Observable int connections
@Observable String me
@Observable boolean searchButtonsEnabled
@Observable boolean cancelButtonEnabled
@Observable boolean retryButtonEnabled
private final Set<InfoHash> infoHashes = new HashSet<>()
@@ -61,12 +69,19 @@ class MainFrameModel {
core.eventBus.register(UploadEvent.class, this)
core.eventBus.register(UploadFinishedEvent.class, this)
core.eventBus.register(TrustEvent.class, this)
core.eventBus.register(QueryEvent.class, this)
})
Timer timer = new Timer("download-pumper", true)
timer.schedule({
runInsideUIAsync {
builder.getVariable("downloads-table").model.fireTableDataChanged()
builder.getVariable("uploads-table").model.fireTableDataChanged()
if (!mvcGroup.alive)
return
builder.getVariable("uploads-table")?.model.fireTableDataChanged()
def downloadTable = builder.getVariable("downloads-table")
int selectedRow = downloadTable.getSelectedRow()
downloadTable.model.fireTableDataChanged()
downloadTable.selectionModel.setSelectionInterval(selectedRow,selectedRow)
}
}, 1000, 1000)
}
@@ -83,14 +98,23 @@ class MainFrameModel {
}
void onConnectionEvent(ConnectionEvent e) {
if (e.getStatus() != ConnectionAttemptStatus.SUCCESSFUL)
return
runInsideUIAsync {
connections = core.connectionManager.getConnections().size()
connectionList.add(e.endpoint.destination)
JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged()
}
}
void onDisconnectionEvent(DisconnectionEvent e) {
runInsideUIAsync {
connections = core.connectionManager.getConnections().size()
connectionList.remove(e.destination)
JTable table = builder.getVariable("connections-table")
table.model.fireTableDataChanged()
}
}
@@ -140,4 +164,24 @@ class MainFrameModel {
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(search)
while(searches.size() > 200)
searches.removeLast()
JTable table = builder.getVariable("searches-table")
table.model.fireTableDataChanged()
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

View File

@@ -9,9 +9,11 @@ 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.download.Downloader
import com.muwire.core.files.FileSharedEvent
import java.awt.BorderLayout
@@ -48,6 +50,7 @@ class MainFrameView {
gridLayout(rows:1, cols: 2)
button(text: "Searches", actionPerformed : showSearchWindow)
button(text: "Uploads", actionPerformed : showUploadsWindow)
button(text: "Monitor", actionPerformed : showMonitorWindow)
}
panel(constraints: BorderLayout.CENTER) {
borderLayout()
@@ -69,9 +72,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) {
@@ -94,6 +97,10 @@ class MainFrameView {
}
}
}
panel (constraints : BorderLayout.SOUTH) {
button(text: "Cancel", enabled : bind {model.cancelButtonEnabled }, cancelAction )
button(text: "Retry", enabled : bind {model.retryButtonEnabled}, resumeAction)
}
}
}
}
@@ -102,7 +109,7 @@ 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") {
@@ -115,7 +122,9 @@ class MainFrameView {
}
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) {
@@ -132,6 +141,35 @@ class MainFrameView {
}
}
}
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 : { it })
}
}
}
}
}
}
panel (border: etchedBorder(), constraints : BorderLayout.SOUTH) {
borderLayout()
@@ -145,6 +183,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 +218,11 @@ class MainFrameView {
cardsPanel.getLayout().show(cardsPanel, "uploads window")
}
def showMonitorWindow = {
def cardsPanel = builder.getVariable("cards-panel")
cardsPanel.getLayout().show(cardsPanel,"monitor window")
}
def shareFiles = {
def chooser = new JFileChooser()
chooser.setDialogTitle("Select file or directory to share")

View File

@@ -4,6 +4,8 @@ import griffon.core.artifact.GriffonView
import griffon.core.mvc.MVCGroup
import griffon.inject.MVCMember
import griffon.metadata.ArtifactProviderFor
import javax.swing.ListSelectionModel
import javax.swing.SwingConstants
import java.awt.BorderLayout
@@ -39,6 +41,12 @@ class SearchTabView {
this.pane = pane
this.pane.putClientProperty("mvc-group", mvcGroup)
this.pane.putClientProperty("results-table",resultsTable)
def selectionModel = resultsTable.getSelectionModel()
selectionModel.setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
selectionModel.addListSelectionListener( {
mvcGroup.parentGroup.model.searchButtonsEnabled = true
})
}
}
@@ -55,7 +63,7 @@ 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 )
}
}
@@ -66,6 +74,7 @@ class SearchTabView {
def closeTab = {
int index = parent.indexOfTab(searchTerms)
parent.removeTabAt(index)
mvcGroup.parentGroup.model.searchButtonsEnabled = false
mvcGroup.destroy()
}
}