handle connection errors and incoming notifications

This commit is contained in:
Craig Raw 2020-06-09 09:59:36 +02:00
parent 3a65261326
commit 50ef1c1a07
5 changed files with 251 additions and 47 deletions

View file

@ -24,10 +24,7 @@ import com.sparrowwallet.sparrow.transaction.TransactionController;
import com.sparrowwallet.sparrow.wallet.WalletController; import com.sparrowwallet.sparrow.wallet.WalletController;
import com.sparrowwallet.sparrow.wallet.WalletForm; import com.sparrowwallet.sparrow.wallet.WalletForm;
import de.codecentric.centerdevice.MenuToolkit; import de.codecentric.centerdevice.MenuToolkit;
import javafx.animation.Animation; import javafx.animation.*;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.concurrent.Worker; import javafx.concurrent.Worker;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
@ -53,7 +50,6 @@ public class AppController implements Initializable {
private static final int SERVER_PING_PERIOD = 10 * 1000; private static final int SERVER_PING_PERIOD = 10 * 1000;
private static final int ENUMERATE_HW_PERIOD = 30 * 1000; private static final int ENUMERATE_HW_PERIOD = 30 * 1000;
private static final String TRANSACTION_TAB_TYPE = "transaction";
public static final String DRAG_OVER_CLASS = "drag-over"; public static final String DRAG_OVER_CLASS = "drag-over";
@FXML @FXML
@ -77,9 +73,12 @@ public class AppController implements Initializable {
@FXML @FXML
private UnlabeledToggleSwitch serverToggle; private UnlabeledToggleSwitch serverToggle;
//Determines if a change in serverToggle changes the offline/online mode
private boolean changeMode = true;
private Timeline statusTimeline; private Timeline statusTimeline;
private ElectrumServer.PingService pingService; private ElectrumServer.ConnectionService connectionService;
public static boolean showTxHexProperty; public static boolean showTxHexProperty;
@ -89,7 +88,7 @@ public class AppController implements Initializable {
} }
void initializeView() { void initializeView() {
setOsxApplicationMenu(); //setOsxApplicationMenu();
rootStack.setOnDragOver(event -> { rootStack.setOnDragOver(event -> {
if(event.getGestureSource() != rootStack && event.getDragboard().hasFiles()) { if(event.getGestureSource() != rootStack && event.getDragboard().hasFiles()) {
@ -140,44 +139,52 @@ public class AppController implements Initializable {
exportWallet.setDisable(true); exportWallet.setDisable(true);
serverToggle.selectedProperty().addListener((observable, oldValue, newValue) -> { serverToggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
Config.get().setMode(newValue ? Mode.ONLINE : Mode.OFFLINE); if(changeMode) {
if(newValue) { Config.get().setMode(newValue ? Mode.ONLINE : Mode.OFFLINE);
if(pingService.getState() == Worker.State.CANCELLED) { if(newValue) {
pingService.reset(); if(connectionService.getState() == Worker.State.CANCELLED) {
} connectionService.reset();
}
if(!pingService.isRunning()) { if(!connectionService.isRunning()) {
pingService.start(); connectionService.start();
}
} else {
connectionService.cancel();
} }
} else {
pingService.cancel();
} }
}); });
pingService = createPingService(); connectionService = createConnectionService();
Config config = Config.get(); Config config = Config.get();
if(config.getMode() == Mode.ONLINE && config.getElectrumServer() != null && !config.getElectrumServer().isEmpty()) { if(config.getMode() == Mode.ONLINE && config.getElectrumServer() != null && !config.getElectrumServer().isEmpty()) {
pingService.start(); connectionService.start();
} }
} }
private ElectrumServer.PingService createPingService() { private ElectrumServer.ConnectionService createConnectionService() {
ElectrumServer.PingService pingService = new ElectrumServer.PingService(); ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService();
pingService.setPeriod(new Duration(SERVER_PING_PERIOD)); connectionService.setPeriod(new Duration(SERVER_PING_PERIOD));
pingService.setOnSucceeded(successEvent -> { connectionService.setRestartOnFailure(true);
connectionService.setOnSucceeded(successEvent -> {
changeMode = false;
serverToggle.setSelected(true); serverToggle.setSelected(true);
if(pingService.getValue() != null) { changeMode = true;
statusBar.setText("Connected: " + pingService.getValue().split(System.lineSeparator(), 2)[0]);
} else { if(connectionService.getValue() != null) {
statusBar.setText(""); EventManager.get().post(connectionService.getValue());
} }
}); });
pingService.setOnFailed(failEvent -> { connectionService.setOnFailed(failEvent -> {
changeMode = false;
serverToggle.setSelected(false); serverToggle.setSelected(false);
statusBar.setText(failEvent.getSource().getException().getMessage()); changeMode = true;
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
}); });
return pingService; return connectionService;
} }
private void setOsxApplicationMenu() { private void setOsxApplicationMenu() {
@ -594,6 +601,19 @@ public class AppController implements Initializable {
exportWallet.setDisable(!event.getWallet().isValid()); exportWallet.setDisable(!event.getWallet().isValid());
} }
@Subscribe
public void statusUpdated(StatusEvent event) {
statusBar.setText(event.getStatus());
PauseTransition wait = new PauseTransition(Duration.seconds(10));
wait.setOnFinished((e) -> {
if(statusBar.getText().equals(event.getStatus())) {
statusBar.setText("");
}
});
wait.play();
}
@Subscribe @Subscribe
public void timedWorker(TimedEvent event) { public void timedWorker(TimedEvent event) {
if(event.getTimeMills() == 0) { if(event.getTimeMills() == 0) {
@ -643,4 +663,18 @@ public class AppController implements Initializable {
usbStatus.setDevices(event.getDevices()); usbStatus.setDevices(event.getDevices());
} }
} }
@Subscribe
public void newConnection(ConnectionEvent event) {
String banner = event.getServerBanner();
String status = "Connected: " + (banner == null ? "Server" : banner.split(System.lineSeparator(), 2)[0]) + " at height " + event.getBlockHeight();
EventManager.get().post(new StatusEvent(status));
}
@Subscribe
public void connectionFailed(ConnectionFailedEvent event) {
String reason = event.getException().getCause() != null ? event.getException().getCause().getMessage() : event.getException().getMessage();
String status = "Connection error: " + reason;
EventManager.get().post(new StatusEvent(status));
}
} }

View file

@ -0,0 +1,35 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.BlockHeader;
import java.util.List;
public class ConnectionEvent {
private final List<String> serverVersion;
private final String serverBanner;
private final int blockHeight;
private final BlockHeader blockHeader;
public ConnectionEvent(List<String> serverVersion, String serverBanner, int blockHeight, BlockHeader blockHeader) {
this.serverVersion = serverVersion;
this.serverBanner = serverBanner;
this.blockHeight = blockHeight;
this.blockHeader = blockHeader;
}
public List<String> getServerVersion() {
return serverVersion;
}
public String getServerBanner() {
return serverBanner;
}
public int getBlockHeight() {
return blockHeight;
}
public BlockHeader getBlockHeader() {
return blockHeader;
}
}

View file

@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
public class ConnectionFailedEvent {
private final Throwable exception;
public ConnectionFailedEvent(Throwable exception) {
this.exception = exception;
}
public Throwable getException() {
return exception;
}
}

View file

@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
public class StatusEvent {
private final String status;
public StatusEvent(String status) {
this.status = status;
}
public String getStatus() {
return status;
}
}

View file

@ -7,6 +7,7 @@ import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.event.ConnectionEvent;
import javafx.concurrent.ScheduledService; import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service; import javafx.concurrent.Service;
import javafx.concurrent.Task; import javafx.concurrent.Task;
@ -24,6 +25,7 @@ import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.*; import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class ElectrumServer { public class ElectrumServer {
@ -75,6 +77,11 @@ public class ElectrumServer {
return transport; return transport;
} }
public void connect() throws ServerException {
TcpTransport tcpTransport = (TcpTransport)getTransport();
tcpTransport.connect();
}
public void ping() throws ServerException { public void ping() throws ServerException {
JsonRpcClient client = new JsonRpcClient(getTransport()); JsonRpcClient client = new JsonRpcClient(getTransport());
client.createRequest().method("server.ping").id(1).executeNullable(); client.createRequest().method("server.ping").id(1).executeNullable();
@ -90,6 +97,11 @@ public class ElectrumServer {
return client.createRequest().returnAs(String.class).method("server.banner").id(1).execute(); return client.createRequest().returnAs(String.class).method("server.banner").id(1).execute();
} }
public BlockHeaderTip subscribeBlockHeaders() throws ServerException {
JsonRpcClient client = new JsonRpcClient(getTransport());
return client.createRequest().returnAs(BlockHeaderTip.class).method("blockchain.headers.subscribe").id(1).execute();
}
public static synchronized void closeActiveConnection() throws ServerException { public static synchronized void closeActiveConnection() throws ServerException {
try { try {
if(transport != null) { if(transport != null) {
@ -368,6 +380,16 @@ public class ElectrumServer {
} }
} }
private static class BlockHeaderTip {
public int height;
public String hex;
public BlockHeader getBlockHeader() {
byte[] blockHeaderBytes = Utils.hexToBytes(hex);
return new BlockHeader(blockHeaderBytes);
}
}
public static class TcpTransport implements Transport, Closeable { public static class TcpTransport implements Transport, Closeable {
public static final int DEFAULT_PORT = 50001; public static final int DEFAULT_PORT = 50001;
@ -376,6 +398,12 @@ public class ElectrumServer {
private Socket socket; private Socket socket;
private String response;
private final ReentrantLock clientRequestLock = new ReentrantLock();
private boolean running = false;
private boolean reading = true;
public TcpTransport(HostAndPort server) { public TcpTransport(HostAndPort server) {
this.server = server; this.server = server;
this.socketFactory = SocketFactory.getDefault(); this.socketFactory = SocketFactory.getDefault();
@ -383,27 +411,62 @@ public class ElectrumServer {
@Override @Override
public @NotNull String pass(@NotNull String request) throws IOException { public @NotNull String pass(@NotNull String request) throws IOException {
if(socket == null) { clientRequestLock.lock();
socket = createSocket();
}
try { try {
writeRequest(socket, request); writeRequest(request);
} catch (IOException e) { return readResponse();
socket = createSocket(); } finally {
writeRequest(socket, request); clientRequestLock.unlock();
} }
return readResponse(socket);
} }
private void writeRequest(Socket socket, String request) throws IOException { private void writeRequest(String request) throws IOException {
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))); PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())));
out.println(request); out.println(request);
out.flush(); out.flush();
} }
private String readResponse(Socket socket) throws IOException { private synchronized String readResponse() {
while(reading) {
try {
wait();
} catch (InterruptedException e) {
//Restore interrupt status and continue
Thread.currentThread().interrupt();
}
}
reading = true;
notifyAll();
return response;
}
public synchronized void readInputLoop() throws ServerException {
while(running) {
try {
String received = readInputStream();
if(received.contains("method")) {
//Handle notification
System.out.println("Notification: " + received);
} else {
response = received;
reading = false;
notifyAll();
wait();
}
} catch(InterruptedException e) {
//Restore interrupt status and continue
Thread.currentThread().interrupt();
} catch(IOException e) {
if(running) {
throw new ServerException(e);
}
}
}
}
protected String readInputStream() throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String response = in.readLine(); String response = in.readLine();
@ -414,6 +477,15 @@ public class ElectrumServer {
return response; return response;
} }
public void connect() throws ServerException {
try {
socket = createSocket();
running = true;
} catch (IOException e) {
throw new ServerException(e);
}
}
protected Socket createSocket() throws IOException { protected Socket createSocket() throws IOException {
return socketFactory.createSocket(server.getHost(), server.getPortOrDefault(DEFAULT_PORT)); return socketFactory.createSocket(server.getHost(), server.getPortOrDefault(DEFAULT_PORT));
} }
@ -421,6 +493,7 @@ public class ElectrumServer {
@Override @Override
public void close() throws IOException { public void close() throws IOException {
if(socket != null) { if(socket != null) {
running = false;
socket.close(); socket.close();
} }
} }
@ -527,20 +600,38 @@ public class ElectrumServer {
} }
} }
public static class PingService extends ScheduledService<String> { public static class ConnectionService extends ScheduledService<ConnectionEvent> implements Thread.UncaughtExceptionHandler {
private boolean firstCall = true; private boolean firstCall = true;
private Thread reader;
private Throwable lastReaderException;
@Override @Override
protected Task<String> createTask() { protected Task<ConnectionEvent> createTask() {
return new Task<>() { return new Task<>() {
protected String call() throws ServerException { protected ConnectionEvent call() throws ServerException {
ElectrumServer electrumServer = new ElectrumServer(); ElectrumServer electrumServer = new ElectrumServer();
if(firstCall) { if(firstCall) {
electrumServer.getServerVersion(); electrumServer.connect();
reader = new Thread(new ReadRunnable());
reader.setDaemon(true);
reader.setUncaughtExceptionHandler(ConnectionService.this);
reader.start();
List<String> serverVersion = electrumServer.getServerVersion();
firstCall = false; firstCall = false;
return electrumServer.getServerBanner();
BlockHeaderTip tip = electrumServer.subscribeBlockHeaders();
String banner = electrumServer.getServerBanner();
return new ConnectionEvent(serverVersion, banner, tip.height, tip.getBlockHeader());
} else { } else {
electrumServer.ping(); if(reader.isAlive()) {
electrumServer.ping();
} else {
firstCall = true;
throw new ServerException("Connection to server failed", lastReaderException);
}
} }
return null; return null;
@ -563,6 +654,24 @@ public class ElectrumServer {
public void reset() { public void reset() {
super.reset(); super.reset();
firstCall = true; firstCall = true;
lastReaderException = null;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
this.lastReaderException = e;
}
}
public static class ReadRunnable implements Runnable {
@Override
public void run() {
try {
TcpTransport tcpTransport = (TcpTransport)getTransport();
tcpTransport.readInputLoop();
} catch (ServerException e) {
throw new RuntimeException(e.getCause() != null ? e.getCause() : e);
}
} }
} }