Integrate UpdateFX with MockUpdateProcess

This commit is contained in:
Manfred Karrer 2014-12-17 17:41:58 +01:00
parent 2e97e900e5
commit 426b38180c
10 changed files with 605 additions and 131 deletions

View file

@ -45,7 +45,9 @@ public class BitsquareEnvironment extends StandardEnvironment {
public static final String APP_VERSION_KEY = "app.version"; public static final String APP_VERSION_KEY = "app.version";
// TODO what is the difference to APP_DATA_DIR ?
public static final String USER_DATA_DIR_KEY = "user.data.dir"; public static final String USER_DATA_DIR_KEY = "user.data.dir";
public static final String DEFAULT_USER_DATA_DIR = defaultUserDataDir(); public static final String DEFAULT_USER_DATA_DIR = defaultUserDataDir();
public static final String APP_NAME_KEY = "app.name"; public static final String APP_NAME_KEY = "app.name";

View file

@ -49,12 +49,17 @@ import javafx.scene.image.*;
import javafx.scene.input.*; import javafx.scene.input.*;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.util.FileSystemUtils; import org.springframework.util.FileSystemUtils;
import static io.bitsquare.app.BitsquareEnvironment.*; import static io.bitsquare.app.BitsquareEnvironment.*;
public class BitsquareApp extends Application { public class BitsquareApp extends Application {
private static final Logger log = LoggerFactory.getLogger(BitsquareAppMain.class);
private static Environment env; private static Environment env;
private BitsquareAppModule bitsquareAppModule; private BitsquareAppModule bitsquareAppModule;
@ -66,6 +71,9 @@ public class BitsquareApp extends Application {
@Override @Override
public void start(Stage primaryStage) throws IOException { public void start(Stage primaryStage) throws IOException {
// For some reason the JavaFX launch process results in us losing the thread context class loader: reset it.
Thread.currentThread().setContextClassLoader(BitsquareApp.class.getClassLoader());
bitsquareAppModule = new BitsquareAppModule(env, primaryStage); bitsquareAppModule = new BitsquareAppModule(env, primaryStage);
injector = Guice.createInjector(bitsquareAppModule); injector = Guice.createInjector(bitsquareAppModule);
injector.getInstance(InjectorViewFactory.class).setInjector(injector); injector.getInstance(InjectorViewFactory.class).setInjector(injector);
@ -139,8 +147,7 @@ public class BitsquareApp extends Application {
else else
iconPath = "/images/task_bar_icon_linux.png"; iconPath = "/images/task_bar_icon_linux.png";
if (iconPath != null) primaryStage.getIcons().add(new Image(getClass().getResourceAsStream(iconPath)));
primaryStage.getIcons().add(new Image(getClass().getResourceAsStream(iconPath)));
// make the UI visible // make the UI visible

View file

@ -24,6 +24,12 @@ import io.bitsquare.network.BootstrapNodes;
import io.bitsquare.network.Node; import io.bitsquare.network.Node;
import io.bitsquare.util.joptsimple.EnumValueConverter; import io.bitsquare.util.joptsimple.EnumValueConverter;
import java.io.File;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vinumeris.updatefx.UpdateFX;
import joptsimple.OptionParser; import joptsimple.OptionParser;
import joptsimple.OptionSet; import joptsimple.OptionSet;
@ -33,8 +39,17 @@ import static io.bitsquare.network.Node.*;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
public class BitsquareAppMain extends BitsquareExecutable { public class BitsquareAppMain extends BitsquareExecutable {
private static final Logger log = LoggerFactory.getLogger(BitsquareAppMain.class);
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
// We don't want to do the whole arg parsing/setup here as that might easily change in update versions
// So we only handle the absolute minimum which is APP_NAME and USER_DATA_DIR
// TODO Not impl. yet, just use default for first testings
UpdateFX.bootstrap(BitsquareAppMain.class, new File(BitsquareEnvironment.DEFAULT_APP_DATA_DIR).toPath(), args);
}
// That will be called from UpdateFX after updates are checked
public static void realMain(String[] args) throws Exception {
new BitsquareAppMain().execute(args); new BitsquareAppMain().execute(args);
} }

View file

@ -61,6 +61,11 @@ class BitsquareAppModule extends BitsquareModule {
bindConstant().annotatedWith(named(Persistence.PREFIX_KEY)).to(env.getRequiredProperty(Persistence.PREFIX_KEY)); bindConstant().annotatedWith(named(Persistence.PREFIX_KEY)).to(env.getRequiredProperty(Persistence.PREFIX_KEY));
bind(Persistence.class).asEagerSingleton(); bind(Persistence.class).asEagerSingleton();
// TODO UpdateFXHelper needs Environment. Should we just expose the 2 properties needed?
bind(Environment.class).toInstance(env);
// for temp testing with mock
bind(UpdateProcess.class).to(MockUpdateProcess.class).asEagerSingleton();
install(messageModule()); install(messageModule());
install(bitcoinModule()); install(bitcoinModule());
install(cryptoModule()); install(cryptoModule());

View file

@ -0,0 +1,154 @@
/*
* This file is part of Bitsquare.
*
* Bitsquare is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.app.gui;
import org.bitcoinj.utils.BriefLogFormatter;
import com.google.common.util.concurrent.Uninterruptibles;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.FileHandler;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vinumeris.updatefx.AppDirectory;
import com.vinumeris.updatefx.Crypto;
import com.vinumeris.updatefx.UpdateFX;
import com.vinumeris.updatefx.UpdateSummary;
import com.vinumeris.updatefx.Updater;
import org.bouncycastle.math.ec.ECPoint;
// TODO remove it after we have impl. UpdateFX.
// Let it here for reference and for easier test setup for the moment.
public class ExampleApp extends Application {
private static final Logger log = LoggerFactory.getLogger(ExampleApp.class);
public static int VERSION = 3;
public static void main(String[] args) throws IOException {
// We want to store updates in our app dir so must init that here.
AppDirectory.initAppDir("UpdateFX Example App");
setupLogging();
// re-enter at realMain, but possibly running a newer version of the software i.e. after this point the
// rest of this code may be ignored.
UpdateFX.bootstrap(ExampleApp.class, AppDirectory.dir(), args);
}
public static void realMain(String[] args) {
launch(args);
}
private static java.util.logging.Logger logger;
private static void setupLogging() throws IOException {
logger = java.util.logging.Logger.getLogger("");
logger.getHandlers()[0].setFormatter(new BriefLogFormatter());
FileHandler handler = new FileHandler(AppDirectory.dir().resolve("log.txt").toString(), true);
handler.setFormatter(new BriefLogFormatter());
logger.addHandler(handler);
}
@Override
public void start(Stage primaryStage) throws Exception {
// For some reason the JavaFX launch process results in us losing the thread context class loader: reset it.
Thread.currentThread().setContextClassLoader(ExampleApp.class.getClassLoader());
// Must be done twice for the times when we come here via realMain.
AppDirectory.initAppDir("UpdateFX Example App");
log.info("Hello World! This is version " + VERSION);
ProgressIndicator indicator = showGiantProgressWheel(primaryStage);
List<ECPoint> pubkeys = Crypto.decode("028B41BDDCDCAD97B6AE088FEECA16DC369353B717E13319370C729CB97D677A11",
// wallet_1
"031E3D80F21A4D10D385A32ABEDC300DACBEDBC839FBA58376FBD5D791D806BA68"); // wallet
Updater updater = new Updater("http://localhost:8000/", "ExampleApp/" + VERSION, VERSION,
AppDirectory.dir(), UpdateFX.findCodePath(ExampleApp.class),
pubkeys, 1) {
@Override
protected void updateProgress(long workDone, long max) {
super.updateProgress(workDone, max);
// Give UI a chance to show.
Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
}
};
indicator.progressProperty().bind(updater.progressProperty());
log.info("Checking for updates!");
updater.setOnSucceeded(event -> {
try {
UpdateSummary summary = updater.get();
if (summary.descriptions.size() > 0) {
log.info("One liner: {}", summary.descriptions.get(0).getOneLiner());
log.info("{}", summary.descriptions.get(0).getDescription());
}
if (summary.highestVersion > VERSION) {
log.info("Restarting to get version " + summary.highestVersion);
if (UpdateFX.getVersionPin(AppDirectory.dir()) == 0)
UpdateFX.restartApp();
}
} catch (Throwable e) {
log.error("oops", e);
}
});
updater.setOnFailed(event -> {
log.error("Update error: {}", updater.getException());
updater.getException().printStackTrace();
});
indicator.setOnMouseClicked(ev -> UpdateFX.restartApp());
new Thread(updater, "UpdateFX Thread").start();
primaryStage.show();
}
private ProgressIndicator showGiantProgressWheel(Stage stage) {
ProgressIndicator indicator = new ProgressIndicator();
BorderPane borderPane = new BorderPane(indicator);
borderPane.setMinWidth(640);
borderPane.setMinHeight(480);
Button pinButton = new Button();
pinButton.setText("Pin to version 1");
pinButton.setOnAction(event -> {
UpdateFX.pinToVersion(AppDirectory.dir(), 1);
UpdateFX.restartApp();
});
HBox box = new HBox(new Label("Version " + VERSION), pinButton);
box.setSpacing(10);
box.setAlignment(Pos.CENTER_LEFT);
box.setPadding(new Insets(10));
borderPane.setTop(box);
Scene scene = new Scene(borderPane);
stage.setScene(scene);
return indicator;
}
}

View file

@ -0,0 +1,56 @@
/*
* This file is part of Bitsquare.
*
* Bitsquare is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.app.gui;
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
public class MockUpdateProcess extends UpdateProcess {
private static final Logger log = LoggerFactory.getLogger(MockUpdateProcess.class);
@Inject
public MockUpdateProcess(Environment environment) {
super(environment);
}
@Override
protected void init(Environment environment) {
/* timeoutTimer.stop();
state.set(State.UPDATE_AVAILABLE);*/
state.set(State.UP_TO_DATE);
timeoutTimer.stop();
process.onCompleted();
/* state.set(State.FAILURE);
errorMessage = "dummy exc.";
timeoutTimer.stop();
process.onCompleted();*/
}
@Override
public void restart() {
log.debug("restart requested");
}
}

View file

@ -0,0 +1,176 @@
/*
* This file is part of Bitsquare.
*
* Bitsquare is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.app.gui;
import io.bitsquare.app.BitsquareEnvironment;
import io.bitsquare.util.Utilities;
import com.google.inject.Inject;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Function;
import javafx.animation.AnimationTimer;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.vinumeris.updatefx.Crypto;
import com.vinumeris.updatefx.UpdateFX;
import com.vinumeris.updatefx.UpdateSummary;
import com.vinumeris.updatefx.Updater;
import org.bouncycastle.math.ec.ECPoint;
import org.springframework.core.env.Environment;
import rx.Observable;
import rx.subjects.BehaviorSubject;
import rx.subjects.Subject;
public class UpdateProcess {
private static final Logger log = LoggerFactory.getLogger(UpdateProcess.class);
private static final int VERSION = 1;
private static final List<ECPoint> UPDATE_SIGNING_KEYS = Crypto.decode(
"028B41BDDCDCAD97B6AE088FEECA16DC369353B717E13319370C729CB97D677A11",
"031E3D80F21A4D10D385A32ABEDC300DACBEDBC839FBA58376FBD5D791D806BA68"
);
private static final int UPDATE_SIGNING_THRESHOLD = 1;
private static final String UPDATES_BASE_URL = "http://localhost:8000/";
private static final Path ROOT_CLASS_PATH = UpdateFX.findCodePath(BitsquareAppMain.class);
public enum State {
CHECK_FOR_UPDATES,
UPDATE_AVAILABLE,
UP_TO_DATE,
FAILURE
}
public final ObjectProperty<State> state = new SimpleObjectProperty<>(State.CHECK_FOR_UPDATES);
protected String errorMessage;
protected final Subject<State, State> process = BehaviorSubject.create();
protected final AnimationTimer timeoutTimer;
@Inject
public UpdateProcess(Environment environment) {
// process.timeout() will cause an error state back but we dont want to break startup in case of an update
// timeout
timeoutTimer = Utilities.setTimeout(10000, new Function<AnimationTimer, Void>() {
@Override
public Void apply(AnimationTimer animationTimer) {
process.onCompleted();
return null;
}
});
timeoutTimer.start();
init(environment);
}
public void restart() {
UpdateFX.restartApp();
}
public Observable<State> getProcess() {
return process.asObservable();
}
public String getErrorMessage() {
return errorMessage;
}
protected void init(Environment environment) {
log.info("version " + VERSION);
String agent = environment.getProperty(BitsquareEnvironment.APP_NAME_KEY) + VERSION;
Path dataDirPath = new File(environment.getProperty(BitsquareEnvironment.APP_DATA_DIR_KEY)).toPath();
Updater updater = new Updater(UPDATES_BASE_URL, agent, VERSION, dataDirPath, ROOT_CLASS_PATH,
UPDATE_SIGNING_KEYS, UPDATE_SIGNING_THRESHOLD) {
@Override
protected void updateProgress(long workDone, long max) {
log.debug("updateProgress " + workDone + "/" + max);
super.updateProgress(workDone, max);
}
};
updater.progressProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) {
log.trace("progressProperty newValue = " + newValue);
}
});
log.info("Checking for updates!");
updater.setOnSucceeded(event -> {
try {
UpdateSummary summary = updater.get();
if (summary.descriptions.size() > 0) {
log.info("One liner: {}", summary.descriptions.get(0).getOneLiner());
log.info("{}", summary.descriptions.get(0).getDescription());
}
if (summary.highestVersion > VERSION) {
state.set(State.UPDATE_AVAILABLE);
}
else if (summary.highestVersion == VERSION) {
state.set(State.UP_TO_DATE);
timeoutTimer.stop();
process.onCompleted();
}
/* if (summary.highestVersion > VERSION) {
log.info("Restarting to get version " + summary.highestVersion);
if (UpdateFX.getVersionPin(dataDirPath) == 0)
UpdateFX.restartApp();
}*/
} catch (Throwable e) {
log.error("Exception at processing UpdateSummary: " + e.getMessage());
// we treat errors as update not as critical errors to prevent startup,
// so we use state.onCompleted() instead of state.onError()
errorMessage = "Exception at processing UpdateSummary: " + e.getMessage();
state.set(State.FAILURE);
timeoutTimer.stop();
process.onCompleted();
}
});
updater.setOnFailed(event -> {
log.error("Update failed: " + updater.getException());
updater.getException().printStackTrace();
// we treat errors as update not as critical errors to prevent startup,
// so we use state.onCompleted() instead of state.onError()
errorMessage = "Update failed: " + updater.getException();
state.set(State.FAILURE);
timeoutTimer.stop();
process.onCompleted();
});
Thread thread = new Thread(updater, "Online update check");
thread.setDaemon(true);
thread.start();
}
}

View file

@ -152,7 +152,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
root.getChildren().addAll(baseApplicationContainer, splashScreen); root.getChildren().addAll(baseApplicationContainer, splashScreen);
model.isReadyForMainScreen.addListener((ov, oldValue, newValue) -> { model.showAppScreen.addListener((ov, oldValue, newValue) -> {
if (newValue) { if (newValue) {
bankAccountComboBoxHolder.getChildren().setAll(createBankAccountComboBox()); bankAccountComboBoxHolder.getChildren().setAll(createBankAccountComboBox());
@ -184,15 +184,13 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
Pane notification = new Pane(); Pane notification = new Pane();
notification.relocate(30, 9); notification.relocate(30, 9);
notification.setMouseTransparent(true); notification.setMouseTransparent(true);
notification.setVisible(model.numPendingTrades.get() > 0);
notification.setEffect(new DropShadow(4, 1, 2, Color.GREY)); notification.setEffect(new DropShadow(4, 1, 2, Color.GREY));
notification.getChildren().addAll(icon, numPendingTradesLabel); notification.getChildren().addAll(icon, numPendingTradesLabel);
notification.visibleProperty().bind(model.showPendingTradesNotification);
portfolioButtonHolder.getChildren().add(notification); portfolioButtonHolder.getChildren().add(notification);
model.numPendingTrades.addListener((ov, oldValue, newValue) -> { model.showPendingTradesNotification.addListener((ov, oldValue, newValue) -> {
notification.setVisible((int) newValue > 0); if (newValue)
if ((int) newValue > 0)
SystemNotification.openInfoNotification(title, "You got a new trade message."); SystemNotification.openInfoNotification(title, "You got a new trade message.");
}); });
} }
@ -200,17 +198,17 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
private VBox createSplashScreen() { private VBox createSplashScreen() {
VBox vBox = new VBox(); VBox vBox = new VBox();
vBox.setAlignment(Pos.CENTER); vBox.setAlignment(Pos.CENTER);
vBox.setSpacing(10); vBox.setSpacing(0);
vBox.setId("splash"); vBox.setId("splash");
ImageView logo = new ImageView(); ImageView logo = new ImageView();
logo.setId("image-splash-logo"); logo.setId("image-splash-logo");
Label blockchainSyncLabel = new Label(); Label blockchainSyncLabel = new Label();
blockchainSyncLabel.textProperty().bind(model.blockchainSyncState); blockchainSyncLabel.textProperty().bind(model.blockchainSyncInfo);
model.walletServiceErrorMsg.addListener((ov, oldValue, newValue) -> { model.walletServiceErrorMsg.addListener((ov, oldValue, newValue) -> {
blockchainSyncLabel.setId("splash-error-state-msg"); blockchainSyncLabel.setId("splash-error-state-msg");
Popups.openErrorPopup("Error", "An error occurred at startup. \n\nError message:\n" + Popups.openErrorPopup("Error", "Connecting to the bitcoin network failed. \n\nReason: " +
newValue); newValue);
}); });
@ -238,7 +236,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
HBox blockchainSyncBox = new HBox(); HBox blockchainSyncBox = new HBox();
blockchainSyncBox.setSpacing(10); blockchainSyncBox.setSpacing(10);
blockchainSyncBox.setAlignment(Pos.CENTER); blockchainSyncBox.setAlignment(Pos.CENTER);
blockchainSyncBox.setPadding(new Insets(60, 0, 0, 0)); blockchainSyncBox.setPadding(new Insets(40, 0, 0, 0));
blockchainSyncBox.setPrefHeight(50); blockchainSyncBox.setPrefHeight(50);
blockchainSyncBox.getChildren().addAll(blockchainSyncLabel, blockchainSyncIndicator, blockchainSyncBox.getChildren().addAll(blockchainSyncLabel, blockchainSyncIndicator,
blockchainSyncIcon, bitcoinNetworkLabel); blockchainSyncIcon, bitcoinNetworkLabel);
@ -247,20 +245,18 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
bootstrapStateLabel.setWrapText(true); bootstrapStateLabel.setWrapText(true);
bootstrapStateLabel.setMaxWidth(500); bootstrapStateLabel.setMaxWidth(500);
bootstrapStateLabel.setTextAlignment(TextAlignment.CENTER); bootstrapStateLabel.setTextAlignment(TextAlignment.CENTER);
bootstrapStateLabel.textProperty().bind(model.bootstrapStateText); bootstrapStateLabel.textProperty().bind(model.bootstrapInfo);
ProgressIndicator bootstrapIndicator = new ProgressIndicator(); ProgressIndicator bootstrapIndicator = new ProgressIndicator();
bootstrapIndicator.setMaxSize(24, 24); bootstrapIndicator.setMaxSize(24, 24);
bootstrapIndicator.progressProperty().bind(model.bootstrapProgress); bootstrapIndicator.progressProperty().bind(model.bootstrapProgress);
model.bootstrapFailed.addListener((ov, oldValue, newValue) -> { model.bootstrapErrorMsg.addListener((ov, oldValue, newValue) -> {
if (newValue) { bootstrapStateLabel.setId("splash-error-state-msg");
bootstrapStateLabel.setId("splash-error-state-msg"); bootstrapIndicator.setVisible(false);
bootstrapIndicator.setVisible(false);
Popups.openErrorPopup("Error", "Connecting to the Bitsquare network failed. \n\nReason: " + Popups.openErrorPopup("Error", "Connecting to the Bitsquare network failed. \n\nReason: " +
model.bootstrapErrorMsg.get()); model.bootstrapErrorMsg.get());
}
}); });
ImageView bootstrapIcon = new ImageView(); ImageView bootstrapIcon = new ImageView();
@ -279,11 +275,35 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
HBox bootstrapBox = new HBox(); HBox bootstrapBox = new HBox();
bootstrapBox.setSpacing(10); bootstrapBox.setSpacing(10);
bootstrapBox.setAlignment(Pos.CENTER); bootstrapBox.setAlignment(Pos.CENTER);
bootstrapBox.setPadding(new Insets(10, 0, 0, 0));
bootstrapBox.setPrefHeight(50); bootstrapBox.setPrefHeight(50);
bootstrapBox.getChildren().addAll(bootstrapStateLabel, bootstrapIndicator, bootstrapIcon); bootstrapBox.getChildren().addAll(bootstrapStateLabel, bootstrapIndicator, bootstrapIcon);
vBox.getChildren().addAll(logo, blockchainSyncBox, bootstrapBox); // software update
Label updateInfoLabel = new Label();
updateInfoLabel.setTextAlignment(TextAlignment.RIGHT);
updateInfoLabel.textProperty().bind(model.updateInfo);
Button restartButton = new Button("Restart");
restartButton.setDefaultButton(true);
restartButton.visibleProperty().bind(model.showRestartButton);
restartButton.managedProperty().bind(model.showRestartButton);
restartButton.setOnAction(e -> model.restart());
ImageView updateIcon = new ImageView();
updateIcon.setId(model.updateIconId.get());
model.updateIconId.addListener((ov, oldValue, newValue) -> {
updateIcon.setId(newValue);
updateIcon.setVisible(true);
updateIcon.setManaged(true);
});
HBox updateBox = new HBox();
updateBox.setSpacing(10);
updateBox.setAlignment(Pos.CENTER);
updateBox.setPrefHeight(20);
updateBox.getChildren().addAll(updateInfoLabel, restartButton, updateIcon);
vBox.getChildren().addAll(logo, blockchainSyncBox, bootstrapBox, updateBox);
return vBox; return vBox;
} }

View file

@ -18,13 +18,13 @@
package io.bitsquare.gui.main; package io.bitsquare.gui.main;
import io.bitsquare.account.AccountSettings; import io.bitsquare.account.AccountSettings;
import io.bitsquare.app.gui.UpdateProcess;
import io.bitsquare.arbitrator.Arbitrator; import io.bitsquare.arbitrator.Arbitrator;
import io.bitsquare.arbitrator.Reputation; import io.bitsquare.arbitrator.Reputation;
import io.bitsquare.bank.BankAccount; import io.bitsquare.bank.BankAccount;
import io.bitsquare.bank.BankAccountType; import io.bitsquare.bank.BankAccountType;
import io.bitsquare.btc.BitcoinNetwork; import io.bitsquare.btc.BitcoinNetwork;
import io.bitsquare.btc.WalletService; import io.bitsquare.btc.WalletService;
import io.bitsquare.gui.components.Popups;
import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.gui.util.BSFormatter;
import io.bitsquare.locale.CountryUtil; import io.bitsquare.locale.CountryUtil;
import io.bitsquare.locale.LanguageUtil; import io.bitsquare.locale.LanguageUtil;
@ -35,7 +35,6 @@ import io.bitsquare.trade.Trade;
import io.bitsquare.trade.TradeManager; import io.bitsquare.trade.TradeManager;
import io.bitsquare.user.User; import io.bitsquare.user.User;
import io.bitsquare.util.DSAKeyUtil; import io.bitsquare.util.DSAKeyUtil;
import io.bitsquare.util.Utilities;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey; import org.bitcoinj.core.ECKey;
@ -47,18 +46,16 @@ import java.util.ArrayList;
import java.util.Currency; import java.util.Currency;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.TimeoutException;
import viewfx.model.ViewModel; import viewfx.model.ViewModel;
import javafx.animation.AnimationTimer;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty; import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
@ -75,106 +72,63 @@ import rx.Observable;
class MainViewModel implements ViewModel { class MainViewModel implements ViewModel {
private static final Logger log = LoggerFactory.getLogger(MainViewModel.class); private static final Logger log = LoggerFactory.getLogger(MainViewModel.class);
final DoubleProperty networkSyncProgress = new SimpleDoubleProperty(-1); // BTC network
final IntegerProperty numPendingTrades = new SimpleIntegerProperty(0); final StringProperty blockchainSyncInfo = new SimpleStringProperty("Initializing");
final StringProperty numPendingTradesAsString = new SimpleStringProperty();
final ObjectProperty<BootstrapState> bootstrapState = new SimpleObjectProperty<>();
final StringProperty bootstrapStateText = new SimpleStringProperty();
final ObjectProperty walletServiceException = new SimpleObjectProperty<Throwable>();
final StringProperty bankAccountsComboBoxPrompt = new SimpleStringProperty();
final BooleanProperty bankAccountsComboBoxDisable = new SimpleBooleanProperty();
final StringProperty blockchainSyncState = new SimpleStringProperty("Initializing");
final DoubleProperty blockchainSyncProgress = new SimpleDoubleProperty(-1); final DoubleProperty blockchainSyncProgress = new SimpleDoubleProperty(-1);
final BooleanProperty blockchainSyncIndicatorVisible = new SimpleBooleanProperty(true);
final StringProperty blockchainSyncIconId = new SimpleStringProperty();
final StringProperty walletServiceErrorMsg = new SimpleStringProperty(); final StringProperty walletServiceErrorMsg = new SimpleStringProperty();
final BooleanProperty isReadyForMainScreen = new SimpleBooleanProperty(); final StringProperty blockchainSyncIconId = new SimpleStringProperty();
// P2P network
final StringProperty bootstrapInfo = new SimpleStringProperty();
final DoubleProperty bootstrapProgress = new SimpleDoubleProperty(-1); final DoubleProperty bootstrapProgress = new SimpleDoubleProperty(-1);
final BooleanProperty bootstrapFailed = new SimpleBooleanProperty();
final StringProperty bootstrapErrorMsg = new SimpleStringProperty(); final StringProperty bootstrapErrorMsg = new SimpleStringProperty();
final StringProperty bootstrapIconId = new SimpleStringProperty(); final StringProperty bootstrapIconId = new SimpleStringProperty();
final StringProperty featureNotImplementedWarning = new SimpleStringProperty(); // software update
final StringProperty updateInfo = new SimpleStringProperty();
final BooleanProperty showRestartButton = new SimpleBooleanProperty(false);
final StringProperty updateIconId = new SimpleStringProperty();
final StringProperty bankAccountsComboBoxPrompt = new SimpleStringProperty();
final BooleanProperty bankAccountsComboBoxDisable = new SimpleBooleanProperty();
final ObjectProperty<BankAccount> currentBankAccount = new SimpleObjectProperty<>(); final ObjectProperty<BankAccount> currentBankAccount = new SimpleObjectProperty<>();
final BooleanProperty showAppScreen = new SimpleBooleanProperty();
final StringProperty featureNotImplementedWarning = new SimpleStringProperty();
final StringProperty numPendingTradesAsString = new SimpleStringProperty();
final BooleanProperty showPendingTradesNotification = new SimpleBooleanProperty();
final String bitcoinNetworkAsString; final String bitcoinNetworkAsString;
private final User user; private final User user;
private final WalletService walletService; private final WalletService walletService;
private final MessageService messageService; private final MessageService messageService;
private final TradeManager tradeManager; private final TradeManager tradeManager;
private UpdateProcess updateProcess;
private final BSFormatter formatter; private final BSFormatter formatter;
private Persistence persistence; private Persistence persistence;
private AccountSettings accountSettings; private AccountSettings accountSettings;
private AnimationTimer bitcoinNetworkTimeout;
@Inject @Inject
public MainViewModel(User user, WalletService walletService, MessageService messageService, public MainViewModel(User user, WalletService walletService, MessageService messageService,
TradeManager tradeManager, BitcoinNetwork bitcoinNetwork, BSFormatter formatter, TradeManager tradeManager, BitcoinNetwork bitcoinNetwork, UpdateProcess updateProcess,
Persistence persistence, AccountSettings accountSettings) { BSFormatter formatter, Persistence persistence, AccountSettings accountSettings) {
this.user = user; this.user = user;
this.walletService = walletService; this.walletService = walletService;
this.messageService = messageService; this.messageService = messageService;
this.tradeManager = tradeManager; this.tradeManager = tradeManager;
this.updateProcess = updateProcess;
this.formatter = formatter; this.formatter = formatter;
this.persistence = persistence; this.persistence = persistence;
this.accountSettings = accountSettings; this.accountSettings = accountSettings;
bitcoinNetworkAsString = bitcoinNetwork.toString(); bitcoinNetworkAsString = bitcoinNetwork.toString();
updateProcess.state.addListener((observableValue, oldValue, newValue) -> applyUpdateState(newValue));
applyUpdateState(updateProcess.state.get());
user.getCurrentBankAccount().addListener((observable, oldValue, newValue) -> persistence.write(user)); user.getCurrentBankAccount().addListener((observable, oldValue, newValue) -> persistence.write(user));
currentBankAccount.bind(user.currentBankAccountProperty()); currentBankAccount.bind(user.currentBankAccountProperty());
bootstrapState.addListener((ov, oldValue, newValue) -> {
if (newValue == BootstrapState.DISCOVERY_DIRECT_SUCCEEDED ||
newValue == BootstrapState.DISCOVERY_MANUAL_PORT_FORWARDING_SUCCEEDED ||
newValue == BootstrapState.DISCOVERY_AUTO_PORT_FORWARDING_SUCCEEDED ||
newValue == BootstrapState.RELAY_SUCCEEDED) {
bootstrapStateText.set("Successfully connected to P2P network: " + newValue.getMessage());
bootstrapProgress.set(1);
if (newValue == BootstrapState.DISCOVERY_DIRECT_SUCCEEDED)
bootstrapIconId.set("image-connection-direct");
else if (newValue == BootstrapState.DISCOVERY_MANUAL_PORT_FORWARDING_SUCCEEDED ||
newValue == BootstrapState.DISCOVERY_AUTO_PORT_FORWARDING_SUCCEEDED)
bootstrapIconId.set("image-connection-nat");
else if (newValue == BootstrapState.RELAY_SUCCEEDED)
bootstrapIconId.set("image-connection-relay");
}
else if (newValue == BootstrapState.PEER_CREATION_FAILED ||
newValue == BootstrapState.DISCOVERY_FAILED ||
newValue == BootstrapState.DISCOVERY_AUTO_PORT_FORWARDING_FAILED ||
newValue == BootstrapState.RELAY_FAILED) {
bootstrapErrorMsg.set(newValue.getMessage());
bootstrapStateText.set("Connecting to the Bitsquare network failed.");
bootstrapProgress.set(0);
bootstrapFailed.set(true);
}
else {
bootstrapStateText.set("Connecting to the Bitsquare network: " + newValue.getMessage());
}
}
);
walletServiceException.addListener((ov, oldValue, newValue) -> {
blockchainSyncIndicatorVisible.set(false);
blockchainSyncState.set("Startup failed.");
walletServiceErrorMsg.set(((Throwable) newValue).getMessage());
});
networkSyncProgress.addListener((ov, oldValue, newValue) -> {
setNetworkSyncProgress((double) newValue);
if ((double) newValue >= 1)
blockchainSyncIconId.set("image-connection-synced");
});
setNetworkSyncProgress(networkSyncProgress.get());
user.getBankAccounts().addListener((ListChangeListener<BankAccount>) change -> { user.getBankAccounts().addListener((ListChangeListener<BankAccount>) change -> {
bankAccountsComboBoxDisable.set(change.getList().isEmpty()); bankAccountsComboBoxDisable.set(change.getList().isEmpty());
bankAccountsComboBoxPrompt.set(change.getList().isEmpty() ? "No accounts" : ""); bankAccountsComboBoxPrompt.set(change.getList().isEmpty() ? "No accounts" : "");
@ -182,57 +136,67 @@ class MainViewModel implements ViewModel {
bankAccountsComboBoxDisable.set(user.getBankAccounts().isEmpty()); bankAccountsComboBoxDisable.set(user.getBankAccounts().isEmpty());
bankAccountsComboBoxPrompt.set(user.getBankAccounts().isEmpty() ? "No accounts" : ""); bankAccountsComboBoxPrompt.set(user.getBankAccounts().isEmpty() ? "No accounts" : "");
tradeManager.featureNotImplementedWarningProperty().addListener((ov, oldValue, newValue) -> { tradeManager.featureNotImplementedWarningProperty().addListener((ov, oldValue, newValue) -> {
if (oldValue == null && newValue != null) { if (oldValue == null && newValue != null) {
featureNotImplementedWarning.set(newValue); featureNotImplementedWarning.set(newValue);
Popups.openWarningPopup(newValue);
tradeManager.setFeatureNotImplementedWarning(null); tradeManager.setFeatureNotImplementedWarning(null);
} }
}); });
} }
public void initBackend() { public void restart() {
bitcoinNetworkTimeout = Utilities.setTimeout(20000, animationTimer -> { updateProcess.restart();
Platform.runLater(() -> { }
networkSyncProgress.set(0);
blockchainSyncState.set("Connecting to the bitcoin network failed.");
Popups.openErrorPopup("Connecting to the bitcoin network failed",
"Please check your network connection.\n\n" +
"You must allow outgoing TCP connections to port 18333 for the bitcoin testnet.\n\n" +
"See https://github.com/bitsquare/bitsquare/wiki for instructions.");
});
return null;
});
public void initBackend() {
setBitcoinNetworkSyncProgress(-1);
walletService.getDownloadProgress().subscribe( walletService.getDownloadProgress().subscribe(
percentage -> Platform.runLater(() -> { percentage -> Platform.runLater(() -> {
if (percentage > 0) if (percentage > 0)
networkSyncProgress.set(percentage / 100.0); setBitcoinNetworkSyncProgress(percentage / 100.0);
}), }),
error -> log.error(error.toString()), error -> log.error(error.toString()),
() -> Platform.runLater(() -> networkSyncProgress.set(1.0))); () -> Platform.runLater(() -> setBitcoinNetworkSyncProgress(1.0)));
Observable<BootstrapState> message = messageService.init(); Observable<BootstrapState> message = messageService.init();
message.publish(); message.publish();
message.subscribe( message.subscribe(
state -> Platform.runLater(() -> bootstrapState.set(state)), state -> Platform.runLater(() -> setBootstrapState(state)),
error -> log.error(error.toString()), error -> Platform.runLater(() -> {
log.error(error.toString());
bootstrapErrorMsg.set(error.getMessage());
bootstrapInfo.set("Connecting to the Bitsquare network failed.");
bootstrapProgress.set(0);
}),
() -> log.trace("message completed")); () -> log.trace("message completed"));
Observable<Object> wallet = walletService.initialize(Platform::runLater); Observable<Object> wallet = walletService.initialize(Platform::runLater);
wallet.subscribe( wallet.subscribe(
next -> { next -> {
log.trace("wallet next");
}, },
error -> Platform.runLater(() -> walletServiceException.set(error)), error -> Platform.runLater(() -> {
log.trace("wallet error");
setWalletServiceException(error);
}),
() -> { () -> {
log.trace("wallet completed"); log.trace("wallet completed");
bitcoinNetworkTimeout.stop();
bitcoinNetworkTimeout = null;
}); });
Observable<?> backend = Observable.merge(message, wallet); Observable<UpdateProcess.State> updateProcess = this.updateProcess.getProcess();
backend.subscribe( updateProcess.subscribe(next -> {
log.trace("updateProcess next");
},
error -> {
log.trace("updateProcess error");
},
() -> {
log.trace("updateProcess completed");
});
Observable<?> backEnd = Observable.merge(message, wallet, updateProcess);
backEnd.subscribe(
next -> { next -> {
}, },
error -> log.error(error.toString()), error -> log.error(error.toString()),
@ -246,7 +210,7 @@ class MainViewModel implements ViewModel {
tradeManager.getPendingTrades().addListener( tradeManager.getPendingTrades().addListener(
(MapChangeListener<String, Trade>) change -> updateNumPendingTrades()); (MapChangeListener<String, Trade>) change -> updateNumPendingTrades());
updateNumPendingTrades(); updateNumPendingTrades();
isReadyForMainScreen.set(true); showAppScreen.set(true);
// For alpha version // For alpha version
// uses messageService, so don't call it before backend is ready // uses messageService, so don't call it before backend is ready
@ -270,6 +234,77 @@ class MainViewModel implements ViewModel {
} }
} }
private void applyUpdateState(UpdateProcess.State state) {
switch (state) {
case CHECK_FOR_UPDATES:
updateInfo.set("Checking for updates...");
updateIconId.set("image-update-in-progress");
break;
case UPDATE_AVAILABLE:
updateInfo.set("New update available. Please restart!");
updateIconId.set("image-update-available");
showRestartButton.set(true);
break;
case UP_TO_DATE:
updateInfo.set("Software is up to date.");
updateIconId.set("image-update-up-to-date");
break;
case FAILURE:
updateInfo.set(updateProcess.getErrorMessage());
updateIconId.set("image-update-failed");
break;
}
}
private void setBootstrapState(BootstrapState state) {
switch (state) {
case DISCOVERY_DIRECT_SUCCEEDED:
bootstrapIconId.set("image-connection-direct");
break;
case DISCOVERY_MANUAL_PORT_FORWARDING_SUCCEEDED:
case DISCOVERY_AUTO_PORT_FORWARDING_SUCCEEDED:
bootstrapIconId.set("image-connection-nat");
break;
case RELAY_SUCCEEDED:
bootstrapIconId.set("image-connection-relay");
break;
default:
bootstrapIconId.set(null);
break;
}
switch (state) {
case DISCOVERY_DIRECT_SUCCEEDED:
case DISCOVERY_MANUAL_PORT_FORWARDING_SUCCEEDED:
case DISCOVERY_AUTO_PORT_FORWARDING_SUCCEEDED:
case RELAY_SUCCEEDED:
bootstrapInfo.set("Successfully connected to P2P network: " + state.getMessage());
bootstrapProgress.set(1);
break;
default:
bootstrapInfo.set("Connecting to the Bitsquare network: " + state.getMessage());
bootstrapProgress.set(-1);
break;
}
}
private void setWalletServiceException(Throwable error) {
setBitcoinNetworkSyncProgress(0);
blockchainSyncInfo.set("Connecting to the bitcoin network failed.");
if (error instanceof TimeoutException) {
walletServiceErrorMsg.set("Please check your network connection.\n\n" +
"You must allow outgoing TCP connections to port 18333 for the bitcoin testnet.\n\n" +
"See https://github.com/bitsquare/bitsquare/wiki for instructions.");
}
else if (error.getMessage() != null) {
walletServiceErrorMsg.set(error.getMessage());
}
else {
walletServiceErrorMsg.set(error.toString());
}
}
public StringConverter<BankAccount> getBankAccountsConverter() { public StringConverter<BankAccount> getBankAccountsConverter() {
return new StringConverter<BankAccount>() { return new StringConverter<BankAccount>() {
@Override @Override
@ -293,21 +328,24 @@ class MainViewModel implements ViewModel {
} }
private void updateNumPendingTrades() { private void updateNumPendingTrades() {
numPendingTrades.set(tradeManager.getPendingTrades().size()); int numPendingTrades = tradeManager.getPendingTrades().size();
if (numPendingTrades.get() > 0) if (numPendingTrades > 0)
numPendingTradesAsString.set(String.valueOf(numPendingTrades.get())); numPendingTradesAsString.set(String.valueOf(numPendingTrades));
showPendingTradesNotification.set(numPendingTrades > 0);
} }
private void setNetworkSyncProgress(double value) { private void setBitcoinNetworkSyncProgress(double value) {
blockchainSyncProgress.set(value); blockchainSyncProgress.set(value);
if (value >= 1) if (value >= 1) {
blockchainSyncState.set("Blockchain synchronization complete."); blockchainSyncInfo.set("Blockchain synchronization complete.");
else if (value > 0.0) blockchainSyncIconId.set("image-connection-synced");
blockchainSyncState.set("Synchronizing blockchain: " + formatter.formatToPercent(value)); }
else else if (value > 0.0) {
blockchainSyncState.set("Connecting to the bitcoin network..."); blockchainSyncInfo.set("Synchronizing blockchain: " + formatter.formatToPercent(value));
}
blockchainSyncIndicatorVisible.set(value < 1); else {
blockchainSyncInfo.set("Connecting to the bitcoin network...");
}
} }
private void addMockArbitrator() { private void addMockArbitrator() {

View file

@ -27,6 +27,7 @@
<logger name="org.bitcoinj" level="INFO"/> <logger name="org.bitcoinj" level="INFO"/>
<logger name="net.tomp2p" level="INFO"/> <logger name="net.tomp2p" level="INFO"/>
<logger name="com.vinumeris.updatefx" level="TRACE"/>
<logger name="net.tomp2p.message.Encoder" level="WARN"/> <logger name="net.tomp2p.message.Encoder" level="WARN"/>